Skip to content

Commit fe9b540

Browse files
authored
Merge pull request #11 from matomo-org/adding-more-test-cases
Adding more test cases and making more improvements
2 parents 5bcff25 + a9462c3 commit fe9b540

11 files changed

Lines changed: 369 additions & 103 deletions

File tree

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ vendor/**/composer.lock
1313
/vue/dist/*.common.js
1414
/vue/dist/*.map
1515
/vue/dist/*.development.*
16+
/tmp/specs/*
17+
!/tmp/specs/.gitkeep
18+
/tmp/annotations/*
19+
!/tmp/annotations/.gitkeep
20+
/tmp/responses/*
21+
!/tmp/responses/.gitkeep

Annotations/AnnotationGenerator.php

Lines changed: 210 additions & 67 deletions
Large diffs are not rendered by default.

OpenApiDocs.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111

1212
class OpenApiDocs extends \Piwik\Plugin
1313
{
14+
public const DEFAULT_SPEC_VERSION = '1.0.0';
15+
public const GENERATED_ANNOTATIONS_PATH = '/tmp/annotations/';
16+
public const EXAMPLE_RESPONSES_PATH = '/tmp/responses/';
17+
public const GENERATED_SPECS_PATH = '/tmp/specs/';
18+
1419
public function registerEvents()
1520
{
1621
return [];

Specs/SpecGenerator.php

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use Piwik\Log\LoggerInterface;
1616
use Piwik\Log\NullLogger;
1717
use Piwik\Plugin\Manager;
18-
use Piwik\Plugins\OpenApiDocs\Annotations\AnnotationGenerator;
18+
use Piwik\Plugins\OpenApiDocs\OpenApiDocs;
1919
use Piwik\SettingsPiwik;
2020
use Piwik\Validators\BaseValidator;
2121
use Piwik\Validators\NotEmpty;
@@ -30,55 +30,83 @@ public function __construct()
3030
}
3131
}
3232

33-
public function generatePluginDoc(string $pluginName, string $format = 'json', string $version = '1.0.0', bool $writeToFile = false): string
33+
/**
34+
* Generate an OpenAPI spec for a single plugin.
35+
*
36+
* @param string $pluginName
37+
* @param string $format
38+
* @param string $version
39+
* @param bool $writeToFile
40+
*
41+
* @return string
42+
* @throws \Exception
43+
*/
44+
public function generatePluginDoc(string $pluginName, string $format = 'json', string $version = OpenApiDocs::DEFAULT_SPEC_VERSION, bool $writeToFile = false): string
3445
{
3546
BaseValidator::check('plugin', $pluginName, [new NotEmpty()]);
36-
Manager::getInstance()->checkIsPluginActivated($pluginName);
3747

48+
return $this->generateSpec(explode(',', $pluginName), $format, $version, $writeToFile);
49+
}
50+
51+
/**
52+
* Generate an OpenAPI spec for one or more plugins.
53+
*
54+
* @param array $pluginNames
55+
* @param string $format
56+
* @param string $version
57+
* @param bool $writeToFile
58+
*
59+
* @return string
60+
* @throws \Piwik\Exception\DI\DependencyException
61+
* @throws \Piwik\Exception\DI\NotFoundException
62+
* @throws \Piwik\Exception\PluginDeactivatedException
63+
*/
64+
public function generateSpec(array $pluginNames, string $format = 'json', string $version = OpenApiDocs::DEFAULT_SPEC_VERSION, bool $writeToFile = false): string
65+
{
66+
BaseValidator::check('pluginNames', $pluginNames, [new NotEmpty()]);
3867
$currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs');
39-
$pluginDir = Manager::getInstance()::getPluginDirectory($pluginName);
40-
$pluginSpecDir = $pluginDir . '/OpenApi/Specs';
41-
$pluginSpecPath = $pluginSpecDir . '/' . $pluginName . '_v' . $version . '.' . strtolower($format);
42-
// If the directory doesn't exist yet, create it
43-
if ($writeToFile && !is_dir($pluginSpecDir)) {
44-
mkdir($pluginSpecDir, 0777, true);
45-
}
4668

47-
// Check if the API class has been annotated and use the generated annotations file if it hasn't
48-
$pluginAnnotationsSource = $pluginDir . '/API.php';
49-
$openapi = (new Generator(StaticContainer::get(NullLogger::class)))->generate([
50-
$pluginAnnotationsSource,
51-
]);
52-
if (trim($openapi->toYaml()) === 'openapi: ' . OpenApi::DEFAULT_VERSION) {
53-
$pluginAnnotationDir = $pluginDir . '/OpenApi/Annotations';
54-
$pluginAnnotationPath = $pluginAnnotationDir . '/GeneratedAnnotations.php';
55-
$pluginAnnotationsSource = $pluginAnnotationPath;
56-
// If the generated file doesn't exist yet, generate one
57-
if (!is_dir($pluginAnnotationDir) || !file_exists($pluginAnnotationPath)) {
58-
(StaticContainer::get(AnnotationGenerator::class))->generatePluginApiAnnotations($pluginName, true);
69+
$pluginDirs = [];
70+
foreach ($pluginNames as $pluginName) {
71+
BaseValidator::check('pluginName', $pluginName, [new NotEmpty()]);
72+
Manager::getInstance()->checkIsPluginActivated($pluginName);
73+
74+
$pluginDir = Manager::getInstance()::getPluginDirectory($pluginName);
75+
$pluginAnnotationsSource = $pluginDir . '/API.php';
76+
$openapi = (new Generator(StaticContainer::get(NullLogger::class)))->generate([
77+
$pluginAnnotationsSource,
78+
]);
79+
if (trim($openapi->toYaml()) === 'openapi: ' . OpenApi::DEFAULT_VERSION) {
80+
throw new \Exception("The $pluginName plugin's API class does not appear to be annotated yet.");
5981
}
82+
$pluginDirs[$pluginName] = $pluginAnnotationsSource;
6083
}
6184

6285
$generator = new Generator(StaticContainer::get(LoggerInterface::class));
63-
64-
$openapi = $generator->generate([
86+
$openapi = $generator->generate(array_merge([
6587
$currentPluginDir . '/Annotations/GlobalApiComponents.php',
66-
$pluginAnnotationsSource,
67-
]);
88+
], $pluginDirs));
6889

69-
// Update title with plugin name
70-
$openapi->info->title .= ' for ' . $pluginName . ' plugin';
90+
$specFileBaseName = 'matomo';
91+
// If there's only one plugin, name the spec after the plugin
92+
if (count($pluginNames) === 1) {
93+
// Update title with plugin name
94+
$openapi->info->title .= ' for ' . $pluginNames[0] . ' plugin';
95+
$specFileBaseName = $pluginNames[0];
96+
}
7197

72-
$openapi->info->version = $version ?: '1.0.0';
98+
$openapi->info->version = $version ?: OpenApiDocs::DEFAULT_SPEC_VERSION;
7399

74100
// Remove the current server so that it isn't used when saving the spec file. It should only leave demo
75101
if ($writeToFile && is_array($openapi->servers) && count($openapi->servers) > 1) {
76102
unset($openapi->servers[0]);
77103
$openapi->servers = array_values($openapi->servers);
78104
}
79105

80-
$specContents = strtolower($format) === 'yaml' ? $openapi->toYaml() : $openapi->toJson();
106+
$lowercaseFormat = strtolower($format);
107+
$specContents = $lowercaseFormat === 'yaml' ? $openapi->toYaml() : $openapi->toJson();
81108
if ($writeToFile) {
109+
$pluginSpecPath = $currentPluginDir . OpenApiDocs::GENERATED_SPECS_PATH . $specFileBaseName . '_openapi_spec_v' . $version . '.' . $lowercaseFormat;
82110
file_put_contents($pluginSpecPath, $specContents);
83111
}
84112

phpcs.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<exclude-pattern>tests/javascript/*</exclude-pattern>
1111
<exclude-pattern>*/vendor/*</exclude-pattern>
12+
<exclude-pattern>*/tmp/*</exclude-pattern>
1213

1314
<rule ref="Matomo"></rule>
1415

tests/Resources/ExampleResponsesNormalised/ExamplesSchemasByType.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,19 @@
4040
"json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":[\"type=\\\"object\\\",\",\"@OA\\\\Property(property=\\\"severity\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"tag\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"datetime\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"requestId\\\", type=\\\"string\\\")\",\"@OA\\\\Property(property=\\\"message\\\", type=\\\"string\\\")\"]}}}"
4141
},
4242
"LogViewer.getAvailableLogReaders": {
43-
"xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"string\\\"\"]}}]}",
43+
"xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\"]}}]}",
4444
"json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
4545
},
4646
"LogViewer.getConfiguredLogReaders": {
4747
"xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
4848
"json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
4949
},
5050
"LogViewer.getLogConfig": {
51-
"xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":[\"property=\\\"log_writers\\\",\",\"type=\\\"object\\\",\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"string\\\"\"]}}]}]}",
51+
"xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":[\"property=\\\"log_writers\\\",\",\"type=\\\"object\\\",\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\"]}}]}]}",
5252
"json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"object\\\",\",\"@OA\\\\Property\":[\"property=\\\"log_writers\\\",\",\"type=\\\"array\\\",\",\"@OA\\\\Items()\"],\"1\":\"@OA\\\\Property(property=\\\"log_level\\\", type=\\\"string\\\")\",\"2\":\"@OA\\\\Property(property=\\\"logger_file_path\\\", type=\\\"string\\\")\",\"3\":\"@OA\\\\Property(property=\\\"logger_syslog_ident\\\", type=\\\"string\\\")\"}}"
5353
},
5454
"CustomAlerts.getAlert": {
55-
"xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\"]}",
55+
"xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":[\"property=\\\"report_mediums\\\",\",\"type=\\\"object\\\",\"]},{\"@OA\\\\Property\":[\"property=\\\"id_sites\\\",\",\"type=\\\"object\\\",\"]}]}",
5656
"json": "{\"@OA\\\\Schema\":[\"type=\\\"array\\\",\",\"@OA\\\\Items()\"]}"
5757
},
5858
"CustomAlerts.getAlerts": {
@@ -68,7 +68,7 @@
6868
"json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"@OA\\\\Property(property=\\\"idtriggered\\\", type=\\\"integer\\\")\",\"2\":\"@OA\\\\Property(property=\\\"idalert\\\", type=\\\"integer\\\")\",\"3\":\"@OA\\\\Property(property=\\\"idsite\\\", type=\\\"integer\\\")\",\"4\":\"@OA\\\\Property(property=\\\"ts_triggered\\\", type=\\\"string\\\")\",\"5\":\"@OA\\\\Property(property=\\\"ts_last_sent\\\", type=\\\"string\\\")\",\"6\":\"@OA\\\\Property(property=\\\"value_old\\\", type=\\\"string\\\")\",\"7\":\"@OA\\\\Property(property=\\\"value_new\\\", type=\\\"string\\\")\",\"8\":\"@OA\\\\Property(property=\\\"name\\\", type=\\\"string\\\")\",\"9\":\"@OA\\\\Property(property=\\\"login\\\", type=\\\"string\\\")\",\"10\":\"@OA\\\\Property(property=\\\"period\\\", type=\\\"string\\\")\",\"11\":\"@OA\\\\Property(property=\\\"report\\\", type=\\\"string\\\")\",\"12\":\"@OA\\\\Property(property=\\\"report_condition\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"13\":\"@OA\\\\Property(property=\\\"report_matched\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"@OA\\\\Property\":[\"property=\\\"id_sites\\\",\",\"type=\\\"array\\\",\",\"@OA\\\\Items()\"],\"14\":\"@OA\\\\Property(property=\\\"metric\\\", type=\\\"string\\\")\",\"15\":\"@OA\\\\Property(property=\\\"metric_condition\\\", type=\\\"string\\\")\",\"16\":\"@OA\\\\Property(property=\\\"metric_matched\\\", type=\\\"integer\\\")\",\"17\":\"@OA\\\\Property(property=\\\"compared_to\\\", type=\\\"integer\\\")\",\"18\":\"@OA\\\\Property(property=\\\"email_me\\\", type=\\\"integer\\\")\",\"19\":\"@OA\\\\Property(property=\\\"slack_channel_id\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\"}}}}"
6969
},
7070
"CustomDimensions.getCustomDimension": {
71-
"xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"string\\\"\"]}}]}",
71+
"xml": "{\"@OA\\\\Schema\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"result\\\"),\",{\"@OA\\\\Property\":{\"0\":\"property=\\\"row\\\",\",\"1\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":[\"type=\\\"object\\\",\",\"@OA\\\\Xml(name=\\\"row\\\"),\",\"additionalProperties=true,\"]}}]}",
7272
"json": "{\"@OA\\\\Schema\":{\"0\":\"type=\\\"array\\\",\",\"@OA\\\\Items\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"additionalProperties=true,\",\"@OA\\\\Property\":{\"0\":\"type=\\\"object\\\",\",\"1\":\"@OA\\\\Property(property=\\\"label\\\", type=\\\"string\\\")\",\"2\":\"@OA\\\\Property(property=\\\"nb_uniq_visitors\\\", type=\\\"string\\\")\",\"3\":\"@OA\\\\Property(property=\\\"nb_visits\\\", type=\\\"string\\\")\",\"4\":\"@OA\\\\Property(property=\\\"nb_actions\\\", type=\\\"string\\\")\",\"5\":\"@OA\\\\Property(property=\\\"max_actions\\\", type=\\\"integer\\\")\",\"6\":\"@OA\\\\Property(property=\\\"sum_visit_length\\\", type=\\\"string\\\")\",\"7\":\"@OA\\\\Property(property=\\\"bounce_count\\\", type=\\\"string\\\")\",\"8\":\"@OA\\\\Property(property=\\\"nb_visits_converted\\\", type=\\\"string\\\")\",\"@OA\\\\Property\":{\"0\":\"property=\\\"goals\\\",\",\"1\":\"type=\\\"object\\\",\",\"@OA\\\\Property\":[\"property=\\\"idgoal=8\\\",\",\"type=\\\"object\\\",\",\"@OA\\\\Property(property=\\\"nb_conversions\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"nb_visits_converted\\\", type=\\\"integer\\\")\",\"@OA\\\\Property(property=\\\"revenue\\\", type=\\\"integer\\\")\"]},\"9\":\"@OA\\\\Property(property=\\\"nb_conversions\\\", type=\\\"integer\\\")\",\"10\":\"@OA\\\\Property(property=\\\"revenue\\\", type=\\\"integer\\\")\",\"11\":\"@OA\\\\Property(property=\\\"avg_time_on_site\\\", type=\\\"integer\\\")\",\"12\":\"@OA\\\\Property(property=\\\"bounce_rate\\\", type=\\\"string\\\")\",\"13\":\"@OA\\\\Property(property=\\\"nb_actions_per_visit\\\", type={\\\"string\\\", \\\"number\\\", \\\"integer\\\", \\\"boolean\\\", \\\"array\\\", \\\"object\\\", \\\"null\\\"})\",\"14\":\"@OA\\\\Property(property=\\\"segment\\\", type=\\\"string\\\")\"}}}}"
7373
},
7474
"CustomDimensions.getConfiguredCustomDimensions": {

tests/Unit/AnnotationGeneratorTest.php

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,56 @@ public function getTestDataForBuildParameterAnnotationData(): iterable
442442
'default' => 'Piwik\API\NoDefaultValue',
443443
'example' => '',
444444
]];
445+
yield 'should show one type when docInfo has two types and one is bool' => ['someParam', [], [
446+
'type' => 'string|bool',
447+
], [
448+
'name' => 'someParam',
449+
'types' => ['string' => null],
450+
'description' => '',
451+
'required' => 'true',
452+
'default' => 'Piwik\API\NoDefaultValue',
453+
'example' => '',
454+
]];
455+
yield 'should remove bool type when docInfo has more than 2 types and one is bool' => ['someParam', [], [
456+
'type' => 'string|int|bool',
457+
], [
458+
'name' => 'someParam',
459+
'types' => ['string' => null, 'integer' => null],
460+
'description' => '',
461+
'required' => 'true',
462+
'default' => 'Piwik\API\NoDefaultValue',
463+
'example' => '',
464+
]];
465+
yield 'should remove bool type regardless of spacing around pipe' => ['someParam', [], [
466+
'type' => 'string | int | bool',
467+
], [
468+
'name' => 'someParam',
469+
'types' => ['string' => null, 'integer' => null],
470+
'description' => '',
471+
'required' => 'true',
472+
'default' => 'Piwik\API\NoDefaultValue',
473+
'example' => '',
474+
]];
475+
yield 'should remove bool type regardless of spacing and order' => ['someParam', [], [
476+
'type' => 'bool | string',
477+
], [
478+
'name' => 'someParam',
479+
'types' => ['string' => null],
480+
'description' => '',
481+
'required' => 'true',
482+
'default' => 'Piwik\API\NoDefaultValue',
483+
'example' => '',
484+
]];
485+
yield 'should remove bool type even when type hints are wrapped by parenthesis' => ['someParam', [], [
486+
'type' => '(bool | string)',
487+
], [
488+
'name' => 'someParam',
489+
'types' => ['string' => null],
490+
'description' => '',
491+
'required' => 'true',
492+
'default' => 'Piwik\API\NoDefaultValue',
493+
'example' => '',
494+
]];
445495
yield 'should allow multiple types when metadata type is string' => ['someParam', [
446496
'type' => 'string',
447497
], [
@@ -454,6 +504,31 @@ public function getTestDataForBuildParameterAnnotationData(): iterable
454504
'default' => 'Piwik\API\NoDefaultValue',
455505
'example' => '',
456506
]];
507+
yield 'should allow multiple types when metadata type is bool and doc type is piped' => ['someParam', [
508+
'type' => 'bool',
509+
], [
510+
'type' => 'string|int|bool',
511+
], [
512+
'name' => 'someParam',
513+
'types' => ['string' => null, 'integer' => null],
514+
'description' => '',
515+
'required' => 'true',
516+
'default' => 'Piwik\API\NoDefaultValue',
517+
'example' => '',
518+
]];
519+
yield 'should allow multiple types when metadata type is bool and doc type is piped even if default is bool' => ['someParam', [
520+
'type' => 'bool',
521+
'default' => false,
522+
], [
523+
'type' => 'string|int|bool',
524+
], [
525+
'name' => 'someParam',
526+
'types' => ['string' => null, 'integer' => null],
527+
'description' => '',
528+
'required' => 'false',
529+
'default' => 'false',
530+
'example' => '',
531+
]];
457532
yield 'should not allow multiple types when metadata type is specified' => ['someParam', [
458533
'type' => 'integer',
459534
], [
@@ -661,8 +736,16 @@ public function testBuildPropertyAnnotationFromJsonExample(): void
661736

662737
public function testBuildSchemaAnnotationFromXmlExample(): void
663738
{
664-
// TODO - buildSchemaAnnotationFromXmlExample method
665-
$this->expectNotToPerformAssertions();
739+
$normalisedMap = $this->getExampleResponsesMap();
740+
$schemasMap = $this->getExampleResponsesMap(true);
741+
foreach (self::EXAMPLE_API_ENDPOINTS as $endpoint) {
742+
$normalisedString = $normalisedMap[$endpoint]['xml'] ?? '';
743+
$this->assertNotEmpty($normalisedString, 'The normalised example response should not be empty for endpoint: ' . $endpoint);
744+
$normalisedObject = json_decode($normalisedString, true) ?? [];
745+
$this->assertNotEmpty($normalisedObject, 'The decoded example response should not be empty for endpoint: ' . $endpoint);
746+
$expected = json_decode($schemasMap[$endpoint]['xml'] ?? '', true) ?? [];
747+
$this->assertEquals($expected, $this->annotationGenerator->buildSchemaAnnotationFromXmlExample($normalisedObject), "The XML schema was not as expected for endpoint $endpoint.");
748+
}
666749
}
667750

668751
public function testBuildPropertyAnnotationFromXmlExample(): void

tmp/.gitkeep

Whitespace-only changes.

tmp/annotations/.gitkeep

Whitespace-only changes.

tmp/responses/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)