Skip to content

Commit cdbffb5

Browse files
committed
Added general endpoint to retrieve specs, either by plugin name or all
1 parent 6c51982 commit cdbffb5

2 files changed

Lines changed: 94 additions & 27 deletions

File tree

API.php

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,24 @@
2424
class API extends \Piwik\Plugin\API
2525
{
2626
/**
27-
* Get the pre-generated single OpenAPI spec file if it exists. This endpoint only reads
27+
* Get a pre-generated OpenAPI spec file if it exists. This endpoint only reads
2828
* the generated JSON file and does not trigger spec generation.
2929
*
30-
* /index.php?module=API&method=OpenApiDocs.getMatomoOpenApiSpec
30+
* /index.php?module=API&method=OpenApiDocs.getOpenApiSpec&spec=matomo
3131
*
32+
* @param string $spec Spec identifier used in the generated filename. Use `matomo`
33+
* for the aggregate spec or a plugin name for a specific spec.
3234
* @param string $format Output format. Only `json` is supported.
3335
* @return array<string, mixed> The decoded OpenAPI specification payload.
3436
* @throws \Exception If the file is missing, unreadable, or contains invalid JSON.
3537
*/
36-
public function getMatomoOpenApiSpec(string $format = 'json'): array
38+
public function getOpenApiSpec(string $spec = 'matomo', string $format = 'json'): array
3739
{
3840
Piwik::checkUserHasSomeViewAccess();
3941

40-
if (strtolower($format) !== 'json') {
41-
throw new \Exception(
42-
Piwik::translate(
43-
'General_ExceptionInvalidReportRendererFormat',
44-
[$format, 'json']
45-
)
46-
);
47-
}
48-
49-
$filePath = $this->getMatomoSpecFilePath();
42+
$this->validateJsonFormat($format);
5043

44+
$filePath = $this->getSpecFilePath($spec);
5145
if (!$this->isSpecFileReadable($filePath)) {
5246
throw new \Exception('OpenAPI spec file was not found. Generate it first via openapidocs:generate-spec-file.');
5347
}
@@ -65,11 +59,11 @@ public function getMatomoOpenApiSpec(string $format = 'json'): array
6559
return $decodedSpec;
6660
}
6761

68-
protected function getMatomoSpecFilePath(): string
62+
protected function getSpecFilePath(string $spec): string
6963
{
7064
$currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs');
7165

72-
return $currentPluginDir . OpenApiDocs::GENERATED_SPECS_PATH . 'matomo_openapi_spec_v' . OpenApiDocs::DEFAULT_SPEC_VERSION . '.json';
66+
return $currentPluginDir . OpenApiDocs::GENERATED_SPECS_PATH . $spec . '_openapi_spec_v' . OpenApiDocs::DEFAULT_SPEC_VERSION . '.json';
7367
}
7468

7569
protected function isSpecFileReadable(string $filePath): bool
@@ -86,6 +80,18 @@ protected function readSpecFile(string $filePath)
8680
return file_get_contents($filePath);
8781
}
8882

83+
protected function validateJsonFormat(string $format): void
84+
{
85+
if (strtolower($format) !== 'json') {
86+
throw new \Exception(
87+
Piwik::translate(
88+
'General_ExceptionInvalidReportRendererFormat',
89+
[$format, 'json']
90+
)
91+
);
92+
}
93+
}
94+
8995
/**
9096
* Get the generated API documentation data for the specified plugin.
9197
*

tests/Unit/APITest.php

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ protected function tearDown(): void
4343
parent::tearDown();
4444
}
4545

46-
public function testGetMatomoOpenApiSpecReturnsDecodedJson()
46+
public function testGetOpenApiSpecReturnsDecodedJsonForMatomo()
4747
{
4848
$expectedSpec = [
4949
'openapi' => '3.1.0',
@@ -53,44 +53,105 @@ public function testGetMatomoOpenApiSpecReturnsDecodedJson()
5353
],
5454
];
5555

56-
$api = $this->buildApiMock(true, json_encode($expectedSpec));
56+
$api = $this->buildApiMock('/tmp/matomo_openapi_spec_v1.0.0.json', true, json_encode($expectedSpec));
5757

58-
$result = $api->getMatomoOpenApiSpec();
58+
$result = $api->getOpenApiSpec();
5959

6060
$this->assertSame($expectedSpec, $result);
6161
}
6262

63-
public function testGetMatomoOpenApiSpecThrowsExceptionWhenFileMissing()
63+
public function testGetOpenApiSpecReturnsDecodedJsonForPlugin()
6464
{
65-
$api = $this->buildApiMock(false);
65+
$expectedSpec = [
66+
'openapi' => '3.1.0',
67+
'info' => [
68+
'title' => 'Matomo Reporting API for CustomAlerts plugin',
69+
'version' => '1.0.0',
70+
],
71+
];
72+
73+
$api = $this->buildApiMock('/tmp/CustomAlerts_openapi_spec_v1.0.0.json', true, json_encode($expectedSpec));
74+
75+
$result = $api->getOpenApiSpec('CustomAlerts');
76+
77+
$this->assertSame($expectedSpec, $result);
78+
}
79+
80+
public function testGetOpenApiSpecThrowsExceptionWhenFileMissing()
81+
{
82+
$api = $this->buildApiMock('/tmp/matomo_openapi_spec_v1.0.0.json', false);
6683

6784
$this->expectException(\Exception::class);
6885
$this->expectExceptionMessage('OpenAPI spec file was not found');
6986

70-
$api->getMatomoOpenApiSpec();
87+
$api->getOpenApiSpec();
7188
}
7289

73-
public function testGetMatomoOpenApiSpecThrowsExceptionWhenJsonIsInvalid()
90+
public function testGetOpenApiSpecThrowsExceptionWhenJsonIsInvalid()
7491
{
75-
$api = $this->buildApiMock(true, '{invalid json}');
92+
$api = $this->buildApiMock('/tmp/matomo_openapi_spec_v1.0.0.json', true, '{invalid json}');
7693

7794
$this->expectException(\Exception::class);
7895
$this->expectExceptionMessage('OpenAPI spec file contains invalid JSON');
7996

80-
$api->getMatomoOpenApiSpec();
97+
$api->getOpenApiSpec();
8198
}
8299

100+
public function testGetOpenApiSpecThrowsExceptionWhenFormatIsInvalid()
101+
{
102+
$api = $this->buildApiMock('/tmp/CustomAlerts_openapi_spec_v1.0.0.json', true, '{}');
103+
104+
$this->expectException(\Exception::class);
105+
$this->expectExceptionMessage('General_ExceptionInvalidReportRendererFormat');
83106

84-
private function buildApiMock(bool $isReadable, $fileContents = false): API
107+
$api->getOpenApiSpec('CustomAlerts', 'yaml');
108+
}
109+
110+
public function testGetSpecFilePathUsesMatomoFileNameByDefault()
111+
{
112+
$api = new API();
113+
114+
$this->assertSame(
115+
PIWIK_INCLUDE_PATH . '/plugins/OpenApiDocs/tmp/specs/matomo_openapi_spec_v1.0.0.json',
116+
$this->callProtectedMethod($api, 'getSpecFilePath', ['matomo'])
117+
);
118+
}
119+
120+
public function testGetSpecFilePathUsesPluginSpecificFileName()
121+
{
122+
$api = new API();
123+
124+
$this->assertSame(
125+
PIWIK_INCLUDE_PATH . '/plugins/OpenApiDocs/tmp/specs/CustomAlerts_openapi_spec_v1.0.0.json',
126+
$this->callProtectedMethod($api, 'getSpecFilePath', ['CustomAlerts'])
127+
);
128+
}
129+
130+
131+
private function buildApiMock(string $filePath, bool $isReadable, $fileContents = false): API
85132
{
86133
$api = $this->getMockBuilder(API::class)
87-
->onlyMethods(['getMatomoSpecFilePath', 'isSpecFileReadable', 'readSpecFile'])
134+
->onlyMethods(['getSpecFilePath', 'isSpecFileReadable', 'readSpecFile'])
88135
->getMock();
89136

90-
$api->method('getMatomoSpecFilePath')->willReturn('/tmp/matomo_openapi_spec_v1.0.0.json');
137+
$api->method('getSpecFilePath')->willReturn($filePath);
91138
$api->method('isSpecFileReadable')->willReturn($isReadable);
92139
$api->method('readSpecFile')->willReturn($fileContents);
93140

94141
return $api;
95142
}
143+
144+
/**
145+
* @param object $object
146+
* @param string $methodName
147+
* @param array<int, mixed> $arguments
148+
* @return mixed
149+
*/
150+
private function callProtectedMethod($object, string $methodName, array $arguments = [])
151+
{
152+
$reflection = new \ReflectionMethod($object, $methodName);
153+
$reflection->setAccessible(true);
154+
155+
return $reflection->invokeArgs($object, $arguments);
156+
}
96157
}

0 commit comments

Comments
 (0)