diff --git a/API.php b/API.php index a75ba6e..6861d94 100644 --- a/API.php +++ b/API.php @@ -10,15 +10,82 @@ namespace Piwik\Plugins\OpenApiDocs; use Piwik\Piwik; +use Piwik\Plugin\Manager; use Piwik\Plugins\OpenApiDocs\Specs\SpecGenerator; /** * API for plugin OpenApiDocs * + * Exposes endpoints to fetch pre-generated OpenAPI specs or generate plugin-specific + * OpenAPI docs on demand. + * * @method static \Piwik\Plugins\OpenApiDocs\API getInstance() */ class API extends \Piwik\Plugin\API { + /** + * Get the pre-generated single OpenAPI spec file if it exists. This endpoint only reads + * the generated JSON file and does not trigger spec generation. + * + * /index.php?module=API&method=OpenApiDocs.getMatomoOpenApiSpec + * + * @param string $format Output format. Only `json` is supported. + * @return array The decoded OpenAPI specification payload. + * @throws \Exception If the file is missing, unreadable, or contains invalid JSON. + */ + public function getMatomoOpenApiSpec(string $format = 'json'): array + { + Piwik::checkUserHasSomeViewAccess(); + + if (strtolower($format) !== 'json') { + throw new \Exception( + Piwik::translate( + 'General_ExceptionInvalidReportRendererFormat', + [$format, 'json'] + ) + ); + } + + $filePath = $this->getMatomoSpecFilePath(); + + if (!$this->isSpecFileReadable($filePath)) { + throw new \Exception('OpenAPI spec file was not found. Generate it first via openapidocs:generate-spec-file.'); + } + + $specContents = $this->readSpecFile($filePath); + if ($specContents === false) { + throw new \Exception('OpenAPI spec file could not be read.'); + } + + $decodedSpec = json_decode($specContents, true); + if (!is_array($decodedSpec) || json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('OpenAPI spec file contains invalid JSON.'); + } + + return $decodedSpec; + } + + protected function getMatomoSpecFilePath(): string + { + $currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs'); + + return $currentPluginDir . OpenApiDocs::GENERATED_SPECS_PATH . 'matomo_openapi_spec_v' . OpenApiDocs::DEFAULT_SPEC_VERSION . '.json'; + } + + protected function isSpecFileReadable(string $filePath): bool + { + return is_file($filePath) && is_readable($filePath); + } + + /** + * @param string $filePath + * @return string|false + */ + protected function readSpecFile(string $filePath) + { + return file_get_contents($filePath); + } + /** * Get the generated API documentation data for the specified plugin. * diff --git a/CHANGELOG.md b/CHANGELOG.md index 895de6f..893dcbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## Changelog -5.0.2-b1 - 2026-02-16 +5.0.2-b1 - 2026-03-16 - Added support for string literal union types +- Added API endpoint to retrieve static matomo swagger file 5.0.1-b1 - 2026-02-16 - Added class and function level docs diff --git a/tests/Unit/APITest.php b/tests/Unit/APITest.php new file mode 100644 index 0000000..a2b3c54 --- /dev/null +++ b/tests/Unit/APITest.php @@ -0,0 +1,96 @@ +originalAccess = Access::getInstance(); + StaticContainer::getContainer()->set(Access::class, new FakeAccess(false, [], [1], 'viewUser')); + } + + protected function tearDown(): void + { + StaticContainer::getContainer()->set(Access::class, $this->originalAccess); + + parent::tearDown(); + } + + public function testGetMatomoOpenApiSpecReturnsDecodedJson() + { + $expectedSpec = [ + 'openapi' => '3.1.0', + 'info' => [ + 'title' => 'Matomo Reporting API', + 'version' => '1.0.0', + ], + ]; + + $api = $this->buildApiMock(true, json_encode($expectedSpec)); + + $result = $api->getMatomoOpenApiSpec(); + + $this->assertSame($expectedSpec, $result); + } + + public function testGetMatomoOpenApiSpecThrowsExceptionWhenFileMissing() + { + $api = $this->buildApiMock(false); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('OpenAPI spec file was not found'); + + $api->getMatomoOpenApiSpec(); + } + + public function testGetMatomoOpenApiSpecThrowsExceptionWhenJsonIsInvalid() + { + $api = $this->buildApiMock(true, '{invalid json}'); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('OpenAPI spec file contains invalid JSON'); + + $api->getMatomoOpenApiSpec(); + } + + + private function buildApiMock(bool $isReadable, $fileContents = false): API + { + $api = $this->getMockBuilder(API::class) + ->onlyMethods(['getMatomoSpecFilePath', 'isSpecFileReadable', 'readSpecFile']) + ->getMock(); + + $api->method('getMatomoSpecFilePath')->willReturn('/tmp/matomo_openapi_spec_v1.0.0.json'); + $api->method('isSpecFileReadable')->willReturn($isReadable); + $api->method('readSpecFile')->willReturn($fileContents); + + return $api; + } +}