Skip to content

Commit b5ecf24

Browse files
authored
Improve unit test coverage and fix minor bug (#14)
* Adding new test files * Adding testing examples and correcting some previous ones * Fixing small bug in generator and adjusting test case * Improving schema generation * Improving depth of generated schemas * Ignore deprecated endpoints * Fixed missing example files and endpoint sorting * Cleaned up using data providers * Implementing more test cases * Fixed schemas not including XML attributes * Cleaned up and improved testing of new XML attributes * Fix bug in XML attribute annotation generation
1 parent c082b2f commit b5ecf24

70 files changed

Lines changed: 31950 additions & 183 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Annotations/AnnotationGenerator.php

Lines changed: 123 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,12 @@ protected function buildAnnotationForMethod(array $rules, string $pluginName, \R
244244
$existing = $reflectionMethod->getDocComment();
245245
// Skip methods which have been marked as internal or auto annotations disabled
246246
if (
247-
$existing !== false && (stripos($existing, 'OA-AUTO:OFF') !== false
248-
|| stripos($existing, '@internal') !== false
249-
|| stripos($existing, '@hide') !== false)
247+
$existing !== false
248+
&& (
249+
stripos($existing, '@internal') !== false
250+
|| stripos($existing, '@hide') !== false
251+
|| stripos($existing, '@deprecated') !== false
252+
)
250253
) {
251254
return [];
252255
}
@@ -947,17 +950,23 @@ public function convertExampleXmlToObject(string $xml): array
947950
$root = new \SimpleXMLElement($xml);
948951

949952
$toArray = function (\SimpleXMLElement $node) use (&$toArray) {
950-
if (!count($node->children())) {
953+
if (!count($node->children()) && !count($node->attributes())) {
951954
return trim((string)$node);
952955
}
953-
// Group children by tag name; repeated names become arrays
956+
957+
// Handle any attributes
954958
$grouped = [];
959+
foreach ($node->attributes() as $attribute) {
960+
$grouped[OpenApiDocs::OA_XML_ATTRIBUTES_TEMP_PROPERTY_NAME][] = [$attribute->getName() => (string) $attribute];
961+
}
962+
963+
// Group children by tag name; repeated names become arrays
955964
foreach ($node->children() as $child) {
956965
$name = $child->getName();
957966
$grouped[$name][] = $toArray($child);
958967
}
959968
return array_map(function ($items) {
960-
return (count($items) === 1) ? $items[0] : $items;
969+
return (count($items) === 1) ? array_pop($items) : $items;
961970
}, $grouped);
962971
};
963972

@@ -1011,7 +1020,7 @@ protected function determineResponses(array $rules, string $plugin, string $meth
10111020

10121021
// If the return type is void, use the generic response type
10131022
if (empty($successArray['ref']) && !empty($returnType) && strval($returnType) === 'void') {
1014-
$successArray['ref'] = '#/components/responses/GenericSuccessNoBody';
1023+
$successArray['ref'] = '#/components/responses/GenericSuccess';
10151024
}
10161025

10171026
// If it's a generic type and there's no custom description, use one of the global generic responses
@@ -1133,8 +1142,13 @@ protected function buildMediaTypePropertiesArray(string $format, string $example
11331142
{
11341143
$contentType = $format === 'json' ? 'application/json' : ($format === 'xml' ? 'text/xml' : 'application/vnd.ms-excel');
11351144

1136-
$jsonSchema = $format === 'json' ? $this->buildSchemaAnnotationFromJsonExample(json_decode($exampleValue, true) ?? []) : [];
1137-
$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+
}
11381152

11391153
if (in_array($format, ['json', 'xml'])) {
11401154
// The annotation expects objects and not arrays, so replace [] with {}
@@ -1323,38 +1337,68 @@ public function buildPropertyAnnotationFromJsonExample(string $propName, array $
13231337
/**
13241338
* Take the deserialised structure of an XML node and build the lines of an OA\Schema annotation object for it.
13251339
*
1326-
* @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.
13271342
* @param string $root Name of the root element. The default is 'result'.
13281343
*
13291344
* @return array Collection of potentially nested arrays representing an OA\Property annotation object.
13301345
*/
1331-
public function buildSchemaAnnotationFromXmlExample(array $xmlArrayObject, string $root = 'result'): array
1346+
public function buildSchemaAnnotationFromXmlExample(array &$xmlArrayObject, string $root = 'result'): array
13321347
{
13331348
$lines = [
13341349
'type="object",',
13351350
sprintf('@OA\Xml(name="%s"),', $root),
13361351
];
13371352

1338-
foreach ($xmlArrayObject as $key => $value) {
1353+
foreach ($xmlArrayObject as $key => &$value) {
13391354
// If the value is not an array, skip
13401355
if (!is_array($value)) {
13411356
continue;
13421357
}
13431358

13441359
if (count($value) === 1) {
13451360
$keys = array_keys($value);
1346-
// Skip if it's not a named property
1361+
// Skip if it's not a named property and isn't an array
13471362
if (!is_string(reset($keys)) && !is_array(reset($value))) {
13481363
continue;
13491364
}
13501365
}
13511366

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

13551373
return ['@OA\Schema' => $lines];
13561374
}
13571375

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+
// Add the attributes as actual properties so that they are visible in the example
1390+
foreach ($value as $attributeName => $attributeValue) {
1391+
$decodedExampleValue[$attributeName] = $attributeValue;
1392+
}
1393+
continue;
1394+
}
1395+
1396+
if (is_array($value)) {
1397+
$this->removeTempOaXmlAttributeProperty($value);
1398+
}
1399+
}
1400+
}
1401+
13581402
/**
13591403
* Take the deserialised structure of an XML node and build the lines of an OA\Property annotation object for it.
13601404
*
@@ -1366,9 +1410,17 @@ public function buildSchemaAnnotationFromXmlExample(array $xmlArrayObject, strin
13661410
public function buildPropertyAnnotationFromXmlExample(string $propName, array $values): array
13671411
{
13681412
$type = 'object';
1413+
$originalValues = $values;
13691414
if ($propName === 'row') {
13701415
$type = 'array';
1371-
$values = is_array($values[0] ?? null) ? $values[0] : [];
1416+
// Merge the rows together to get as many properties as possible
1417+
$mergedValues = [];
1418+
foreach ($values as $value) {
1419+
if (is_array($value)) {
1420+
$mergedValues = array_merge($mergedValues, $value);
1421+
}
1422+
}
1423+
$values = $mergedValues;
13721424
}
13731425

13741426
// Set the common properties
@@ -1377,6 +1429,7 @@ public function buildPropertyAnnotationFromXmlExample(string $propName, array $v
13771429
sprintf('type="%s",', $type),
13781430
];
13791431

1432+
$hasAttributes = false;
13801433
$childLines = [];
13811434
// Recursively check if any of the children are arrays
13821435
foreach ($values as $key => $value) {
@@ -1385,6 +1438,13 @@ public function buildPropertyAnnotationFromXmlExample(string $propName, array $v
13851438
continue;
13861439
}
13871440

1441+
// Special handling for XML attributes
1442+
if ($key === OpenApiDocs::OA_XML_ATTRIBUTES_TEMP_PROPERTY_NAME) {
1443+
$hasAttributes = true;
1444+
$childLines = array_merge($childLines, $this->buildXmlAttributeSchemaLines($value));
1445+
continue;
1446+
}
1447+
13881448
// Handle nested arrays
13891449
if (!is_string($key)) {
13901450
if (!is_array(reset($value))) {
@@ -1408,8 +1468,8 @@ public function buildPropertyAnnotationFromXmlExample(string $propName, array $v
14081468
];
14091469

14101470
// Handle arrays of strings which don't have named properties
1411-
$keys = array_keys($values);
1412-
if (!is_string(reset($keys)) && count($values) === 1) {
1471+
$originalKeys = array_keys($originalValues);
1472+
if (!is_string(reset($originalKeys)) && !is_string(reset($values)) && !$hasAttributes) {
14131473
$itemProperties = ['type="string"'];
14141474
}
14151475

@@ -1419,6 +1479,43 @@ public function buildPropertyAnnotationFromXmlExample(string $propName, array $v
14191479
return ['@OA\Property' => array_merge($propertyLines, $childLines)];
14201480
}
14211481

1482+
/**
1483+
* Build the array of lines for the attribute properties of an XML schema annotation object. It accepts an array of
1484+
* arrays representing the attributes of an XML node. It can also handle a single array of key/value pairs.
1485+
*
1486+
* @param array $attributes Collection of attributes and values. E.g. [['key1' => 'value1'],['key2' => 'value2']] or
1487+
* ['key1' => 'value1', 'key2' => 'value2']
1488+
*
1489+
* @return array The lines defining the property annotation objects for the XML attributes.
1490+
* E.g. [['@OA\Property' => ['property="idgoal",', 'type="string",', '@OA\Xml(attribute=true),', 'example="2"']]]
1491+
*/
1492+
public function buildXmlAttributeSchemaLines(array $attributes): array
1493+
{
1494+
$attributeSchemaLines = [];
1495+
foreach ($attributes as $index => $attribute) {
1496+
$keys = is_array($attribute) ? array_keys($attribute) : [];
1497+
$key = count($keys) === 1 ? $keys[0] : $index;
1498+
$value = trim(is_array($attribute) ? $attribute[$key] ?? '' : $attribute);
1499+
// Allow attributes with empty values, but an attribute must always have a name
1500+
if (empty($key)) {
1501+
continue;
1502+
}
1503+
// Initialise with the lines that will always be present
1504+
$propertyLines = [
1505+
sprintf('property="%s",', $key),
1506+
'type="string",',
1507+
'@OA\Xml(attribute=true),',
1508+
];
1509+
// Add the example line if there's an actual value
1510+
if (!empty($value) || strlen($value) > 0) {
1511+
$propertyLines[] = sprintf('example="%s"', $value);
1512+
}
1513+
$attributeSchemaLines[] = ['@OA\Property' => $propertyLines];
1514+
}
1515+
1516+
return $attributeSchemaLines;
1517+
}
1518+
14221519
/**
14231520
* Take a list of lines and remove the trailing comma from the last line.
14241521
*
@@ -1463,6 +1560,9 @@ public function buildLinesForAnnotationObject(string $objectName, array $objectP
14631560

14641561
// If it's not an object, then it's an array of similarly named objects, like parameters
14651562
foreach ($property as $subPropIndex => $subProperty) {
1563+
if (!is_string($subPropIndex)) {
1564+
continue;
1565+
}
14661566
$lines = array_merge($lines, $this->buildLinesForAnnotationObject($subPropIndex, $subProperty, $indent + 1));
14671567
}
14681568
}
@@ -1549,12 +1649,13 @@ public function wrapStringWithQuotes(string $string, string $type, string $quote
15491649
*/
15501650
public function shouldIncludeDefault(string $type, string $default = NoDefaultValue::class): bool
15511651
{
1552-
if ($default === NoDefaultValue::class) {
1553-
return false;
1554-
}
1555-
1556-
// Don't use true or false for default if it's not a boolean type
1557-
if ($type !== 'boolean' && in_array(strtolower($default), ['false', 'true'])) {
1652+
if (
1653+
$default === NoDefaultValue::class
1654+
|| ($type === 'number' && !is_numeric($default))
1655+
|| ($type === 'integer' && !\ctype_digit($default))
1656+
|| ($type !== 'string' && $default === '')
1657+
|| ($type !== 'boolean' && in_array(strtolower($default), ['false', 'true']))
1658+
) {
15581659
return false;
15591660
}
15601661

Annotations/GlobalApiComponents.php

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,29 @@
5959
* additionalProperties=true,
6060
* @OA\Property(property="result", type="string", example="success"),
6161
* @OA\Property(property="message", type="string", example="ok"),
62-
* @OA\Property(property="code", type="integer", example="200")
62+
* @OA\Property(property="code", type="integer", example="200"),
63+
* example={"result":"success","message":"ok"}
64+
* )
65+
*
66+
* @OA\Schema(
67+
* schema="GenericSuccessXml",
68+
* type="object",
69+
* description="Generic Matomo success payload in XML.",
70+
* required={"success"},
71+
* additionalProperties=true,
72+
* @OA\Xml(name="result"),
73+
* @OA\Property(
74+
* property="success",
75+
* type="object",
76+
* @OA\Xml(name="success"),
77+
* @OA\Property(
78+
* property="message",
79+
* type="string",
80+
* @OA\Xml(attribute=true),
81+
* example="ok"
82+
* )
83+
* ),
84+
* example={"success":{"message":"ok"}}
6385
* )
6486
*
6587
* Generic Error object
@@ -160,9 +182,9 @@
160182
* response="GenericSuccess",
161183
* description="Generic 200 response",
162184
* @OA\JsonContent(ref="#/components/schemas/GenericSuccess"),
163-
* @OA\XmlContent(ref="#/components/schemas/GenericSuccess"),
164-
* @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Result: success"),
165-
* @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="success")
185+
* @OA\XmlContent(ref="#/components/schemas/GenericSuccessXml"),
186+
* @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Success:ok"),
187+
* @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="<!-- Success: ok -->")
166188
* )
167189
*
168190
* @OA\Response(

OpenApiDocs.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
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/';
19+
public const AVAILABLE_PROPERTY_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object', 'null'];
1820

1921
public function registerEvents()
2022
{
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"nb_uniq_visitors":8920,"nb_visits":9831,"nb_users":5,"nb_actions":29397,"max_actions":76,"bounce_count":5760,"sum_visit_length":2556198,"nb_visits_new":5,"nb_actions_new":8,"nb_uniq_visitors_new":5,"nb_users_new":0,"max_actions_new":4,"bounce_rate_new":"80%","nb_actions_per_visit_new":1.6,"avg_time_on_site_new":450,"nb_visits_returning":9829,"nb_actions_returning":29392,"nb_uniq_visitors_returning":8917,"nb_users_returning":5,"max_actions_returning":76,"bounce_rate_returning":"59%","nb_actions_per_visit_returning":3,"avg_time_on_site_returning":260,"Referrers_visitorsFromSearchEngines":6969,"Referrers_visitorsFromSocialNetworks":1111,"Referrers_visitorsFromAIAssistants":0,"Referrers_visitorsFromDirectEntry":957,"Referrers_visitorsFromWebsites":795,"Referrers_visitorsFromCampaigns":0,"Referrers_distinctSearchEngines":20,"Referrers_distinctSocialNetworks":5,"Referrers_distinctAIAssistants":0,"Referrers_distinctKeywords":96,"Referrers_distinctWebsites":4,"Referrers_distinctWebsitesUrls":277,"Referrers_distinctCampaigns":0,"PagePerformance_network_time":0,"PagePerformance_network_hits":0,"PagePerformance_servery_time":0,"PagePerformance_server_hits":0,"PagePerformance_transfer_time":0,"PagePerformance_transfer_hits":0,"PagePerformance_domprocessing_time":0,"PagePerformance_domprocessing_hits":0,"PagePerformance_domcompletion_time":0,"PagePerformance_domcompletion_hits":0,"PagePerformance_onload_time":0,"PagePerformance_onload_hits":0,"PagePerformance_pageload_time":0,"PagePerformance_pageload_hits":0,"avg_time_network":0,"avg_time_server":0,"avg_time_transfer":0,"avg_time_dom_processing":0,"avg_time_dom_completion":0,"avg_time_on_load":0,"avg_page_load_time":0,"nb_plays":184,"nb_unique_visitors_plays":147,"nb_impressions":1313,"nb_unique_visitors_impressions":1022,"nb_finishes":38,"sum_total_time_watched":48918,"sum_total_audio_plays":29,"sum_total_audio_impressions":66,"sum_total_video_plays":155,"sum_total_video_impressions":1247,"nb_conversions":962,"nb_visits_converted":915,"revenue":84869.05,"conversion_rate":"9.31%","nb_conversions_new_visit":0,"nb_visits_converted_new_visit":0,"revenue_new_visit":0,"conversion_rate_new_visit":"0%","nb_conversions_returning_visit":962,"nb_visits_converted_returning_visit":915,"revenue_returning_visit":84869.05,"conversion_rate_returning_visit":"9.31%","nb_form_views":11746,"nb_form_viewers":7731,"nb_form_starts":513,"nb_form_starters":432,"nb_form_submissions":578,"nb_form_submitters":557,"nb_form_resubmitters":15,"nb_form_conversions":136,"nb_crash_occurrences":950,"nb_visits_with_crash":812,"nb_ignored_crashes":0,"nb_uniq_crashes":13,"nb_new_crashes":0,"nb_disappeared_crashes":0,"nb_reappeared_crashes":0,"nb_pageviews":25071,"nb_uniq_pageviews":19429,"nb_downloads":22,"nb_uniq_downloads":20,"nb_outlinks":1378,"nb_uniq_outlinks":1296,"nb_searches":216,"nb_keywords":73,"hits":29816,"Referrers_visitorsFromDirectEntry_percent":"10%","Referrers_visitorsFromSearchEngines_percent":"71%","Referrers_visitorsFromAIAssistants_percent":"0%","Referrers_visitorsFromCampaigns_percent":"0%","Referrers_visitorsFromSocialNetworks_percent":"11%","Referrers_visitorsFromWebsites_percent":"8%","visits_crash_rate":0.08,"bounce_rate":"59%","nb_actions_per_visit":3,"avg_time_on_site":260,"form_starters_rate":"5.6%","form_submitter_rate":"128.9%","form_conversion_rate":"31.5%","form_resubmitters_rate":"2.7%","avg_form_time_hesitation":17.189,"avg_form_time_spent":109.457,"avg_form_time_to_first_submission":9.059,"avg_form_time_to_conversion":83.703,"play_rate":0.14,"finish_rate":0.21,"impression_rate":0.11}

0 commit comments

Comments
 (0)