Skip to content

Commit b2b8564

Browse files
committed
Cleaned up and improved testing of new XML attributes
1 parent a0e9471 commit b2b8564

3 files changed

Lines changed: 151 additions & 17 deletions

File tree

Annotations/AnnotationGenerator.php

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -957,7 +957,7 @@ public function convertExampleXmlToObject(string $xml): array
957957
// Handle any attributes
958958
$grouped = [];
959959
foreach ($node->attributes() as $attribute) {
960-
$grouped['oaXmlAttributes'][] = [$attribute->getName() => (string) $attribute];
960+
$grouped[OpenApiDocs::OA_XML_ATTRIBUTES_TEMP_PROPERTY_NAME][] = [$attribute->getName() => (string) $attribute];
961961
}
962962

963963
// Group children by tag name; repeated names become arrays
@@ -1142,8 +1142,13 @@ protected function buildMediaTypePropertiesArray(string $format, string $example
11421142
{
11431143
$contentType = $format === 'json' ? 'application/json' : ($format === 'xml' ? 'text/xml' : 'application/vnd.ms-excel');
11441144

1145-
$jsonSchema = $format === 'json' ? $this->buildSchemaAnnotationFromJsonExample(json_decode($exampleValue, true) ?? []) : [];
1146-
$xmlSchema = $format === 'xml' ? $this->buildSchemaAnnotationFromXmlExample(json_decode($exampleValue, true) ?? []) : [];
1145+
$decodedExampleValue = json_decode($exampleValue, true) ?? [];
1146+
$jsonSchema = $format === 'json' ? $this->buildSchemaAnnotationFromJsonExample($decodedExampleValue) : [];
1147+
$xmlSchema = $format === 'xml' ? $this->buildSchemaAnnotationFromXmlExample($decodedExampleValue) : [];
1148+
// If the XML example contains the temporary property to assist in building XML attributes in the schema, replace with newly encoded array with property removed
1149+
if ($format === 'xml' && strpos($exampleValue, OpenApiDocs::OA_XML_ATTRIBUTES_TEMP_PROPERTY_NAME) !== false) {
1150+
$exampleValue = json_encode($decodedExampleValue);
1151+
}
11471152

11481153
if (in_array($format, ['json', 'xml'])) {
11491154
// The annotation expects objects and not arrays, so replace [] with {}
@@ -1332,38 +1337,64 @@ public function buildPropertyAnnotationFromJsonExample(string $propName, array $
13321337
/**
13331338
* Take the deserialised structure of an XML node and build the lines of an OA\Schema annotation object for it.
13341339
*
1335-
* @param array $xmlArrayObject Nested array of properties of the XML node.
1340+
* @param array $xmlArrayObject Nested array of properties of the XML node. Passed by reference so that temporary
1341+
* properties can be removed before the example is included in the annotations.
13361342
* @param string $root Name of the root element. The default is 'result'.
13371343
*
13381344
* @return array Collection of potentially nested arrays representing an OA\Property annotation object.
13391345
*/
1340-
public function buildSchemaAnnotationFromXmlExample(array $xmlArrayObject, string $root = 'result'): array
1346+
public function buildSchemaAnnotationFromXmlExample(array &$xmlArrayObject, string $root = 'result'): array
13411347
{
13421348
$lines = [
13431349
'type="object",',
13441350
sprintf('@OA\Xml(name="%s"),', $root),
13451351
];
13461352

1347-
foreach ($xmlArrayObject as $key => $value) {
1353+
foreach ($xmlArrayObject as $key => &$value) {
13481354
// If the value is not an array, skip
13491355
if (!is_array($value)) {
13501356
continue;
13511357
}
13521358

13531359
if (count($value) === 1) {
13541360
$keys = array_keys($value);
1355-
// Skip if it's not a named property
1361+
// Skip if it's not a named property and isn't an array
13561362
if (!is_string(reset($keys)) && !is_array(reset($value))) {
13571363
continue;
13581364
}
13591365
}
13601366

13611367
$lines[] = $this->buildPropertyAnnotationFromXmlExample($key, $value);
1368+
1369+
// Recursively remove all instances of the temporary XML attributes property
1370+
$this->removeTempOaXmlAttributeProperty($value);
13621371
}
13631372

13641373
return ['@OA\Schema' => $lines];
13651374
}
13661375

1376+
/**
1377+
* Iterate over a nested array representing an example response object and recursively remove all occurrences of the
1378+
* temporary property used to help build the schema for XML attributes.
1379+
*
1380+
* @param array $decodedExampleValue The reference to the nested array to remove the temporary property from.
1381+
*
1382+
* @return void
1383+
*/
1384+
protected function removeTempOaXmlAttributeProperty(array &$decodedExampleValue): void
1385+
{
1386+
foreach ($decodedExampleValue as $key => &$value) {
1387+
if ($key === OpenApiDocs::OA_XML_ATTRIBUTES_TEMP_PROPERTY_NAME) {
1388+
unset($decodedExampleValue[$key]);
1389+
continue;
1390+
}
1391+
1392+
if (is_array($value)) {
1393+
$this->removeTempOaXmlAttributeProperty($value);
1394+
}
1395+
}
1396+
}
1397+
13671398
/**
13681399
* Take the deserialised structure of an XML node and build the lines of an OA\Property annotation object for it.
13691400
*
@@ -1380,10 +1411,9 @@ public function buildPropertyAnnotationFromXmlExample(string $propName, array $v
13801411
$type = 'array';
13811412
// Merge the rows together to get as many properties as possible
13821413
$mergedValues = [];
1383-
foreach ($values as $key => $value) {
1384-
$valueArray = is_array($value) ? $value : [$value];
1414+
foreach ($values as $value) {
13851415
if (is_array($value)) {
1386-
$mergedValues = array_merge($mergedValues, $valueArray);
1416+
$mergedValues = array_merge($mergedValues, $value);
13871417
}
13881418
}
13891419
$values = $mergedValues;
@@ -1405,7 +1435,7 @@ public function buildPropertyAnnotationFromXmlExample(string $propName, array $v
14051435
}
14061436

14071437
// Special handling for XML attributes
1408-
if ($key === 'oaXmlAttributes') {
1438+
if ($key === OpenApiDocs::OA_XML_ATTRIBUTES_TEMP_PROPERTY_NAME) {
14091439
$hasAttributes = true;
14101440
$childLines[] = $this->buildXmlAttributeSchemaLines($value);
14111441
continue;
@@ -1445,18 +1475,38 @@ public function buildPropertyAnnotationFromXmlExample(string $propName, array $v
14451475
return ['@OA\Property' => array_merge($propertyLines, $childLines)];
14461476
}
14471477

1478+
/**
1479+
* Build the array of lines for the attribute properties of an XML schema annotation object. It accepts an array of
1480+
* arrays representing the attributes of an XML node. It can also handle a single array of key/value pairs.
1481+
*
1482+
* @param array $attributes Collection of attributes and values. E.g. [['key1' => 'value1'],['key2' => 'value2']] or
1483+
* ['key1' => 'value1', 'key2' => 'value2']
1484+
*
1485+
* @return array The lines defining the property annotation objects for the XML attributes.
1486+
* E.g. [['@OA\Property' => ['property="idgoal",', 'type="string",', '@OA\Xml(attribute=true),', 'example="2"']]]
1487+
*/
14481488
public function buildXmlAttributeSchemaLines(array $attributes): array
14491489
{
14501490
$attributeSchemaLines = [];
14511491
foreach ($attributes as $index => $attribute) {
1452-
$key = is_array($attribute) ? array_keys($attribute)[0] : $index;
1453-
$value = is_array($attribute) ? $attribute[$key] : $attribute;
1454-
$attributeSchemaLines[] = ['@OA\Property' => [
1455-
"property=\"$key\",",
1492+
$keys = is_array($attribute) ? array_keys($attribute) : [];
1493+
$key = count($keys) === 1 ? $keys[0] : $index;
1494+
$value = trim(is_array($attribute) ? $attribute[$key] ?? '' : $attribute);
1495+
// Allow attributes with empty values, but an attribute must always have a name
1496+
if (empty($key)) {
1497+
continue;
1498+
}
1499+
// Initialise with the lines that will always be present
1500+
$propertyLines = [
1501+
sprintf('property="%s",', $key),
14561502
'type="string",',
14571503
'@OA\Xml(attribute=true),',
1458-
"example=\"$value\"",
1459-
]];
1504+
];
1505+
// Add the example line if there's an actual value
1506+
if (!empty($value) || strlen($value) > 0) {
1507+
$propertyLines[] = sprintf('example="%s"', $value);
1508+
}
1509+
$attributeSchemaLines[] = ['@OA\Property' => $propertyLines];
14601510
}
14611511

14621512
return $attributeSchemaLines;
@@ -1506,6 +1556,9 @@ public function buildLinesForAnnotationObject(string $objectName, array $objectP
15061556

15071557
// If it's not an object, then it's an array of similarly named objects, like parameters
15081558
foreach ($property as $subPropIndex => $subProperty) {
1559+
if (!is_string($subPropIndex)) {
1560+
continue;
1561+
}
15091562
$lines = array_merge($lines, $this->buildLinesForAnnotationObject($subPropIndex, $subProperty, $indent + 1));
15101563
}
15111564
}

OpenApiDocs.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
class OpenApiDocs extends \Piwik\Plugin
1313
{
1414
public const DEFAULT_SPEC_VERSION = '1.0.0';
15+
public const OA_XML_ATTRIBUTES_TEMP_PROPERTY_NAME = 'oaXmlAttributes';
1516
public const GENERATED_ANNOTATIONS_PATH = '/tmp/annotations/';
1617
public const EXAMPLE_RESPONSES_PATH = '/tmp/responses/';
1718
public const GENERATED_SPECS_PATH = '/tmp/specs/';

tests/Unit/AnnotationGeneratorTest.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,7 @@ public function testBuildSchemaAnnotationFromXmlExample(string $endpoint, array
914914
$this->assertNotEmpty($normalisedObject, 'The decoded example response should not be empty for endpoint: ' . $endpoint);
915915
$result = $this->annotationGenerator->buildSchemaAnnotationFromXmlExample($normalisedObject);
916916
$this->assertEquals(json_encode($expected), json_encode($result), "The XML schema was not as expected for endpoint $endpoint.");
917+
$this->assertStringNotContainsString(OpenApiDocs::OA_XML_ATTRIBUTES_TEMP_PROPERTY_NAME, json_encode($normalisedObject), "The XML example object should no longer contain the temp attribute property for endpoint $endpoint.");
917918
}
918919

919920
/**
@@ -937,6 +938,85 @@ public function testBuildPropertyAnnotationFromXmlExample(): void
937938
$this->expectNotToPerformAssertions();
938939
}
939940

941+
/**
942+
* @dataProvider getTestDataForTestBuildXmlAttributeSchemaLines
943+
*
944+
* @param array $attributes
945+
* @param array $expected
946+
*
947+
* @return void
948+
*/
949+
public function testBuildXmlAttributeSchemaLines(array $attributes, array $expected): void
950+
{
951+
$this->assertEquals($expected, $this->annotationGenerator->buildXmlAttributeSchemaLines($attributes));
952+
}
953+
954+
public static function getTestDataForTestBuildXmlAttributeSchemaLines(): iterable
955+
{
956+
yield 'should return empty array when attributes are empty' => [[], []];
957+
yield 'should return empty array when attributes are nested empty' => [[[]], []];
958+
yield 'should return empty array when no attributes have a name' => [[['' => 'value']], []];
959+
yield 'should return annotation array as long as the attribute has a name' => [
960+
['testAttribute' => ''],
961+
[['@OA\Property' => ['property="testAttribute",', 'type="string",', '@OA\Xml(attribute=true),']]],
962+
];
963+
yield 'should return annotation array as long as the attribute has a name even when nested' => [
964+
[['testAttribute' => '']],
965+
[['@OA\Property' => ['property="testAttribute",', 'type="string",', '@OA\Xml(attribute=true),']]],
966+
];
967+
yield 'should return annotation array with example when value is set' => [
968+
['testAttribute' => 'testValue'],
969+
[['@OA\Property' => ['property="testAttribute",', 'type="string",', '@OA\Xml(attribute=true),', 'example="testValue"']]],
970+
];
971+
yield 'should return annotation array with example when value is set when nested' => [
972+
[['testAttribute' => 'testValue']],
973+
[['@OA\Property' => ['property="testAttribute",', 'type="string",', '@OA\Xml(attribute=true),', 'example="testValue"']]],
974+
];
975+
yield 'should return multiple annotation arrays without example when value is not set' => [
976+
['testAttribute1' => '', 'testAttribute2' => ''],
977+
[
978+
['@OA\Property' => ['property="testAttribute1",', 'type="string",', '@OA\Xml(attribute=true),']],
979+
['@OA\Property' => ['property="testAttribute2",', 'type="string",', '@OA\Xml(attribute=true),']],
980+
],
981+
];
982+
yield 'should return multiple annotation arrays without example when value is not set when nested' => [
983+
[['testAttribute1' => ''], ['testAttribute2' => '']],
984+
[
985+
['@OA\Property' => ['property="testAttribute1",', 'type="string",', '@OA\Xml(attribute=true),']],
986+
['@OA\Property' => ['property="testAttribute2",', 'type="string",', '@OA\Xml(attribute=true),']],
987+
],
988+
];
989+
yield 'should return multiple annotation arrays with example when value is set' => [
990+
['testAttribute1' => 'testValue1', 'testAttribute2' => 'testValue2'],
991+
[
992+
['@OA\Property' => ['property="testAttribute1",', 'type="string",', '@OA\Xml(attribute=true),', 'example="testValue1"']],
993+
['@OA\Property' => ['property="testAttribute2",', 'type="string",', '@OA\Xml(attribute=true),', 'example="testValue2"']],
994+
],
995+
];
996+
yield 'should return multiple annotation arrays with example when value is set when nested' => [
997+
[['testAttribute1' => 'testValue1'], ['testAttribute2' => 'testValue2']],
998+
[
999+
['@OA\Property' => ['property="testAttribute1",', 'type="string",', '@OA\Xml(attribute=true),', 'example="testValue1"']],
1000+
['@OA\Property' => ['property="testAttribute2",', 'type="string",', '@OA\Xml(attribute=true),', 'example="testValue2"']],
1001+
],
1002+
];
1003+
yield 'should return multiple annotation arrays with example dependent on value' => [
1004+
['testAttribute1' => '', 'testAttribute2' => '', 'testAttribute3' => 'testValue3'],
1005+
[
1006+
['@OA\Property' => ['property="testAttribute1",', 'type="string",', '@OA\Xml(attribute=true),']],
1007+
['@OA\Property' => ['property="testAttribute2",', 'type="string",', '@OA\Xml(attribute=true),']],
1008+
['@OA\Property' => ['property="testAttribute3",', 'type="string",', '@OA\Xml(attribute=true),', 'example="testValue3"']],
1009+
],
1010+
];
1011+
yield 'should return multiple annotation arrays with example dependent on value when nested' => [
1012+
[['testAttribute1' => 'testValue1'], ['testAttribute2' => '']],
1013+
[
1014+
['@OA\Property' => ['property="testAttribute1",', 'type="string",', '@OA\Xml(attribute=true),', 'example="testValue1"']],
1015+
['@OA\Property' => ['property="testAttribute2",', 'type="string",', '@OA\Xml(attribute=true),']],
1016+
],
1017+
];
1018+
}
1019+
9401020
/**
9411021
* @dataProvider getTestDataForRemoveTrailingCommaFromLastLine
9421022
*

0 commit comments

Comments
 (0)