diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b6b7af6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,45 @@ +name: "Tests" + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + tests-on-linux: + name: "Tests on linux" + runs-on: "ubuntu-latest" + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + include: + - php-version: "8.5" + experimental: false + #composer-options: "--ignore-platform-reqs" + + steps: + - name: "Checkout code" + uses: "actions/checkout@v7" + - name: "Setup PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + extensions: mbstring + ini-values: "memory_limit=-1" + tools: composer:v2 + coverage: none + - name: "Install dependencies" + uses: nick-invision/retry@v4.0.0 + with: + timeout_minutes: 5 + max_attempts: 5 + command: >- + composer install + --prefer-dist + --no-interaction + --no-progress + ${{ matrix.composer-options }} + - name: "Run tests" + run: "composer phpunit" diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f2c6a..df1cd21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### 4.1.0 (2026-07-??) + +#### Changes + +* Removed dependency on `cebe/php-openapi`. + +-------------------------------------------------------- + ### 4.0.1 (2026-01-05) #### Bugfixes diff --git a/README.md b/README.md index 79f2a1f..c464a02 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # OpenApi +[![Tests](https://github.com/mako-framework/open-api/actions/workflows/tests.yml/badge.svg)](https://github.com/mako-framework/open-api/actions/workflows/tests.yml) [![Static analysis](https://github.com/mako-framework/open-api/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/mako-framework/open-api/actions/workflows/static-analysis.yml) The `mako/open-api` package allows you to generate an [OpenApi](https://www.openapis.org) specification by documenting your code using PHP [attributes](https://www.php.net/manual/en/language.attributes.php), and to generate routes from an OpenAPI specification. diff --git a/composer.json b/composer.json index 0ed7b4a..ff2c716 100644 --- a/composer.json +++ b/composer.json @@ -15,14 +15,21 @@ "mako\\openapi\\": "src" } }, + "autoload-dev": { + "psr-4": { + "mako\\openapi\\tests\\": "tests" + } + }, "require": { "php": ">=8.5.0", "zircote/swagger-php": "^5.7", - "cebe/php-openapi": "^1.8" + "symfony/yaml": "^8.1" }, "require-dev": { "mako/framework": "^12.0.0", - "phpstan/phpstan": "^2.1.33" + "mockery/mockery": "^1.6.12", + "phpstan/phpstan": "^2.1.33", + "phpunit/phpunit": "^13.2" }, "minimum-stability": "dev", "prefer-stable": true, @@ -32,8 +39,10 @@ } }, "scripts": { + "phpunit": "phpunit --display-incomplete --display-skipped --display-deprecations --display-errors --display-notices --display-warning --display-phpunit-deprecations", "phpstan": "phpstan analyze src --no-progress --memory-limit=-1 -c phpstan.neon", "qa": [ + "@phpunit", "@phpstan" ] } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ca1f90f..8b13789 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,6 +1 @@ -parameters: - ignoreErrors: - - - message: "#Access to an undefined property cebe\\\\openapi\\\\SpecObjectInterface::\\$paths#" - count: 1 - path: src/generators/routing/Generator.php + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..9bd80dd --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests/unit + + + diff --git a/src/generators/routing/Generator.php b/src/generators/routing/Generator.php index f3c881a..194f950 100644 --- a/src/generators/routing/Generator.php +++ b/src/generators/routing/Generator.php @@ -7,11 +7,14 @@ namespace mako\openapi\generators\routing; -use cebe\openapi\Reader; -use cebe\openapi\SpecObjectInterface; use Closure; +use mako\openapi\parser\Operation; +use mako\openapi\parser\Parameter; +use mako\openapi\parser\Parser; +use Symfony\Component\Yaml\Yaml; use function explode; +use function in_array; use function str_replace; use function strpos; @@ -20,6 +23,18 @@ */ abstract class Generator { + /** + * Route methods. + */ + protected const array ROUTE_METHODS = [ + 'delete', + 'get', + 'patch', + 'post', + 'put', + 'query', + ]; + /** * Parameter patterns. * @@ -37,6 +52,8 @@ abstract class Generator 'byte' => '(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})', ], + // Number formats + 'number' => [ '_' => '-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?', 'float' => '-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?', @@ -60,22 +77,20 @@ abstract class Generator ]; /** - * Merges path and operation parameters. + * Returns the route path. * - * @param \cebe\openapi\spec\Parameter[]|\cebe\openapi\spec\Reference[] $pathParameters - * @param \cebe\openapi\spec\Parameter[]|\cebe\openapi\spec\Reference[] $operationParameters + * @param string $path + * @param Parameter[] $parameters */ - public function mergeParameters(array $pathParameters, array $operationParameters): array + protected function getRoutePath(string $path, array $parameters): string { - $merged = []; - - foreach ([$pathParameters, $operationParameters] as $parameters) { - foreach ($parameters as $parameter) { - $merged[$parameter->name] = $parameter; + foreach ($parameters as $parameter) { + if ($parameter->required === false) { + $path = str_replace("{{$parameter->name}}", "{{$parameter->name}}?", $path); } } - return $merged; + return $path; } /** @@ -90,39 +105,28 @@ protected function getRouteAction(string $operationId): array|string return $operationId; } - /** - * Returns the route path. - * - * @param string $path - * @param \cebe\openapi\spec\Parameter[]|\cebe\openapi\spec\Reference[] $parameters - */ - protected function getRoutePath(string $path, array $parameters): string - { - foreach ($parameters as $parameter) { - if ($parameter->in === 'path' && $parameter->required === false) { - $path = str_replace("{{$parameter->name}}", "{{$parameter->name}}?", $path); - } - } - - return $path; - } - /** * Returns route parameter patterns. * - * @param \cebe\openapi\spec\Parameter[]|\cebe\openapi\spec\Reference[] $parameters + * @param Parameter[] $parameters */ protected function getRoutePatterns(array $parameters): array { $patterns = []; foreach ($parameters as $parameter) { - if ($parameter->in === 'path') { - if ($parameter->schema->type === 'string' && !empty($parameter->schema->pattern)) { - $patterns[$parameter->name] = $parameter->schema->pattern; + if (isset($parameter->schema['type'])) { + $type = $parameter->schema['type']; + + if ($type === 'string' && !empty($parameter->schema['pattern'])) { + $patterns[$parameter->name] = $parameter->schema['pattern']; } - elseif (isset($this->parameterPatterns[$parameter->schema->type][$parameter->schema->format ?? '_'])) { - $patterns[$parameter->name] = $this->parameterPatterns[$parameter->schema->type][$parameter->schema->format ?? '_']; + else { + $format = $parameter->schema['format'] ?? '_'; + + if (isset($this->parameterPatterns[$type][$format])) { + $patterns[$parameter->name] = $this->parameterPatterns[$type][$format]; + } } } } @@ -138,26 +142,22 @@ abstract protected function registerRoute(string $method, string $path, array|Cl /** * Generates routes. * - * @param \cebe\openapi\spec\OpenApi|SpecObjectInterface $openApi OpenApi object instance + * @param Operation[] $spec */ - protected function generateRoutes(SpecObjectInterface $openApi): void + protected function generateRoutes(array $spec): void { - $methods = ['get', 'post', 'put', 'patch', 'delete']; - - foreach ($openApi->paths as $path => $definition) { - foreach ($methods as $method) { - if ($definition->{$method} !== null) { - $parameters = $this->mergeParameters($definition->parameters, $definition->{$method}->parameters); - - $this->registerRoute( - $method, - $this->getRoutePath($path, $parameters), - $this->getRouteAction($definition->{$method}->operationId), - $definition->{$method}->operationId, - $this->getRoutePatterns($parameters), - ); - } + foreach ($spec as $operation) { + if (!in_array($operation->method, static::ROUTE_METHODS, true)) { + continue; } + + $this->registerRoute( + $operation->method, + $this->getRoutePath($operation->path, $operation->pathParameters), + $this->getRouteAction($operation->operationId), + $operation->operationId, + $this->getRoutePatterns($operation->pathParameters) + ); } } @@ -166,7 +166,7 @@ protected function generateRoutes(SpecObjectInterface $openApi): void */ public function generateFromYamlFile(string $fileName): void { - $this->generateRoutes(Reader::readFromYamlFile($fileName)); + $this->generateRoutes((new Parser(Yaml::parseFile($fileName)))->parse()); } /** @@ -174,6 +174,6 @@ public function generateFromYamlFile(string $fileName): void */ public function generateFromYaml(string $yaml): void { - $this->generateRoutes(Reader::readFromYaml($yaml)); + $this->generateRoutes((new Parser(Yaml::parse($yaml)))->parse()); } } diff --git a/src/parser/Operation.php b/src/parser/Operation.php new file mode 100644 index 0000000..8e1910d --- /dev/null +++ b/src/parser/Operation.php @@ -0,0 +1,33 @@ +referenceResolver = new ReferenceResolver($this->spec); + } + + /** + * Normalize parameter. + */ + protected function normalizeParameter(array $param): array + { + if (isset($param['$ref'])) { + $param = $this->referenceResolver->resolve($param['$ref']); + } + + return $param; + } + + /** + * Resolves $ref inside schema recursively. + */ + protected function resolveSchema(mixed $schema): mixed + { + if (!is_array($schema)) { + return $schema; + } + + if (isset($schema['$ref'])) { + return $this->referenceResolver->resolve($schema['$ref']); + } + + foreach ($schema as $key => $value) { + $schema[$key] = $this->resolveSchema($value); + } + + return $schema; + } + + /** + * Parses parameters. + */ + protected function parseParameters(array $pathParams, array $methodParams): array + { + $indexed = []; + + $process = function (array $params) use (&$indexed): void { + foreach ($params as $param) { + + $param = $this->normalizeParameter($param); + + if (!in_array($param['in'], self::PARAMETER_BUCKETS, true)) { + continue; + } + + $indexed["{$param['name']}:{$param['in']}"] = [ + 'name' => $param['name'], + 'in' => $param['in'], + 'required' => $param['required'] ?? ($param['in'] === 'path' ? true : false), + 'schema' => $this->resolveSchema($param['schema'] ?? []), + ]; + } + }; + + $process($pathParams); + $process($methodParams); + + $path = []; + $query = []; + $cookie = []; + $header = []; + + foreach ($indexed as $param) { + $object = new Parameter( + $param['name'], + $param['required'], + $param['schema'] + ); + + ${$param['in']}[] = $object; + } + + return [$path, $query, $cookie, $header]; + } + + /** + * Parses spec into operation objects. + * + * @return Operation[] + */ + public function parse(): array + { + $operations = []; + + if (!isset($this->spec['paths']) || !is_array($this->spec['paths'])) { + throw new ParserException("The OpenAPI document does not contain a valid 'paths' section."); + } + + foreach ($this->spec['paths'] as $path => $pathSpec) { + foreach (static::METHODS as $method) { + if (!isset($pathSpec[$method])) { + continue; + } + + $methodSpec = $pathSpec[$method]; + + [$pathParameters, $queryParameters, $cookies, $headers] = $this->parseParameters( + $pathSpec['parameters'] ?? [], + $methodSpec['parameters'] ?? [] + ); + + $operations[] = new Operation( + $path, + $method, + $methodSpec['operationId'] ?? throw new ParserException("Missing required 'operationId' for [ {$method}:{$path} ]."), + $pathParameters, + $queryParameters, + $cookies, + $headers + ); + } + } + + return $operations; + } +} diff --git a/src/parser/ReferenceResolver.php b/src/parser/ReferenceResolver.php new file mode 100644 index 0000000..bf034df --- /dev/null +++ b/src/parser/ReferenceResolver.php @@ -0,0 +1,120 @@ +spec; + + foreach ($path as $segment) { + if (!isset($node[$segment])) { + throw new ParserException(sprintf('Invalid reference [ %s ]. The reference does not exist.', $ref)); + } + + $node = $node[$segment]; + } + + return $node; + } + + /** + * Recursively resolve nested $refs inside arrays. + */ + protected function resolveNested(mixed $node): mixed + { + if (is_array($node)) { + + if (isset($node['$ref'])) { + return $this->resolve($node['$ref']); + } + + foreach ($node as $key => $value) { + $node[$key] = $this->resolveNested($value); + } + } + + return $node; + } + + /** + * Resolve a $ref recursively. + */ + public function resolve(string $ref): mixed + { + // Check if reference is local + + if (!str_starts_with($ref, '#/')) { + throw new ParserException(sprintf('Invalid reference [ %s ]. Only local references supported.', $ref)); + } + + // Prevent circular references + + if (in_array($ref, $this->stack, true)) { + throw new ParserException(sprintf('Invalid reference [ %s ]. Circular reference detected.', $ref)); + } + + // Return from cache if previously resolved + + if (isset($this->cache[$ref])) { + return $this->cache[$ref]; + } + + // Resolve and cache for later use and return + + $this->stack[] = $ref; + + try { + $node = $this->traverse($ref); + + $node = $this->resolveNested($node); + + return $this->cache[$ref] = $node; + } + finally { + array_pop($this->stack); + } + } +} diff --git a/src/parser/exceptions/ParserException.php b/src/parser/exceptions/ParserException.php new file mode 100644 index 0000000..4b46a08 --- /dev/null +++ b/src/parser/exceptions/ParserException.php @@ -0,0 +1,18 @@ +parse(); + + $this->assertCount(4, $parsed); + + foreach ($parsed as $operation) { + $this->assertInstanceOf(Operation::class, $operation); + } + + // 1 + + $this->assertSame('/', $parsed[0]->path); + $this->assertSame('get', $parsed[0]->method); + $this->assertSame('app\http\controllers\RootGet', $parsed[0]->operationId); + + // 2 + + $this->assertSame('/', $parsed[1]->path); + $this->assertSame('post', $parsed[1]->method); + $this->assertSame('app\http\controllers\RootPost', $parsed[1]->operationId); + + // 3 + + $this->assertSame('/foo', $parsed[2]->path); + $this->assertSame('get', $parsed[2]->method); + $this->assertSame('app\http\controllers\FooGet', $parsed[2]->operationId); + + // 4 + + $this->assertSame('/foo', $parsed[3]->path); + $this->assertSame('post', $parsed[3]->method); + $this->assertSame('app\http\controllers\FooPost', $parsed[3]->operationId); + } + + /** + * + */ + public function testParameters(): void + { + $parser = new Parser(Yaml::parseFile(__DIR__ . '/fixtures/parameters.yaml')); + + $parsed = $parser->parse(); + + $this->assertCount(1, $parsed); + + $this->assertSame('/articles/{articleId}/{slug}', $parsed[0]->path); + $this->assertSame('get', $parsed[0]->method); + $this->assertSame('app\http\controllers\ArticlesGet', $parsed[0]->operationId); + + $this->assertCount(2, $parsed[0]->pathParameters); + $this->assertCount(3, $parsed[0]->queryParameters); + $this->assertCount(0, $parsed[0]->cookies); + $this->assertCount(0, $parsed[0]->headers); + + // Path parameters + + $this->assertSame('articleId', $parsed[0]->pathParameters[0]->name); + $this->assertTrue($parsed[0]->pathParameters[0]->required); + $this->assertSame('integer', $parsed[0]->pathParameters[0]->schema['type']); + $this->assertSame('auto-increment', $parsed[0]->pathParameters[0]->schema['format']); + + $this->assertSame('slug', $parsed[0]->pathParameters[1]->name); + $this->assertFalse($parsed[0]->pathParameters[1]->required); + $this->assertSame('string', $parsed[0]->pathParameters[1]->schema['type']); + + // Query parameters + + $this->assertSame('q1', $parsed[0]->queryParameters[0]->name); + $this->assertFalse($parsed[0]->queryParameters[0]->required); + $this->assertSame('boolean', $parsed[0]->queryParameters[0]->schema['type']); + + $this->assertSame('q2', $parsed[0]->queryParameters[1]->name); + $this->assertTrue($parsed[0]->queryParameters[1]->required); + $this->assertSame('boolean', $parsed[0]->queryParameters[1]->schema['type']); + + $this->assertSame('q3', $parsed[0]->queryParameters[2]->name); + $this->assertFalse($parsed[0]->queryParameters[2]->required); + $this->assertSame('boolean', $parsed[0]->queryParameters[2]->schema['type']); + } + + /** + * + */ + public function testReferences(): void + { + $parser = new Parser(Yaml::parseFile(__DIR__ . '/fixtures/references.yaml')); + + $parsed = $parser->parse(); + + $this->assertCount(1, $parsed); + + $this->assertSame('/foobar/{id}', $parsed[0]->path); + $this->assertSame('get', $parsed[0]->method); + $this->assertSame('app\http\controllers\user\FoobarGet', $parsed[0]->operationId); + + $this->assertCount(1, $parsed[0]->pathParameters); + $this->assertCount(0, $parsed[0]->queryParameters); + $this->assertCount(0, $parsed[0]->cookies); + $this->assertCount(0, $parsed[0]->headers); + + // Path parameters + + $this->assertSame('id', $parsed[0]->pathParameters[0]->name); + $this->assertTrue($parsed[0]->pathParameters[0]->required); + $this->assertSame('string', $parsed[0]->pathParameters[0]->schema['type']); + $this->assertSame('foobar', $parsed[0]->pathParameters[0]->schema['format']); + } + + /** + * + */ + public function testMissingPaths(): void + { + $this->expectException(ParserException::class); + $this->expectExceptionMessageIs("The OpenAPI document does not contain a valid 'paths' section."); + + $parser = new Parser(Yaml::parseFile(__DIR__ . '/fixtures/missing-paths.yaml')); + + $parser->parse(); + } + + /** + * + */ + public function testMissingOperationId(): void + { + $this->expectException(ParserException::class); + $this->expectExceptionMessageIs("Missing required 'operationId' for [ get:/ ]."); + + $parser = new Parser(Yaml::parseFile(__DIR__ . '/fixtures/missing-operation-id.yaml')); + + $parser->parse(); + } +} diff --git a/tests/unit/parser/ReferenceResolverTest.php b/tests/unit/parser/ReferenceResolverTest.php new file mode 100644 index 0000000..a118de2 --- /dev/null +++ b/tests/unit/parser/ReferenceResolverTest.php @@ -0,0 +1,124 @@ +resolve('#/components/schemas/IdSchema'); + + $this->assertSame('string', $resolved['type']); + $this->assertSame('foobar', $resolved['format']); + } + + /** + * + */ + public function testResolveNestedReference(): void + { + $referenceResolver = new ReferenceResolver(Yaml::parseFile(__DIR__ . '/fixtures/references.yaml')); + + $resolved = $referenceResolver->resolve('#/components/parameters/IdParam'); + + $this->assertSame('id', $resolved['name']); + $this->assertSame('path', $resolved['in']); + $this->assertTrue($resolved['required']); + $this->assertSame('string', $resolved['schema']['type']); + $this->assertSame('foobar', $resolved['schema']['format']); + } + + /** + * + */ + public function testInvalidReference(): void + { + $this->expectException(ParserException::class); + $this->expectExceptionMessageIs('Invalid reference [ #/components/schemas/InvalidSchema ]. The reference does not exist.'); + + $referenceResolver = new ReferenceResolver(Yaml::parseFile(__DIR__ . '/fixtures/references.yaml')); + + $referenceResolver->resolve('#/components/schemas/InvalidSchema'); + } + + /** + * + */ + public function testCircularReference(): void + { + $this->expectException(ParserException::class); + $this->expectExceptionMessageIs('Invalid reference [ #/components/schemas/CircularRefSchema ]. Circular reference detected.'); + + $referenceResolver = new ReferenceResolver(Yaml::parseFile(__DIR__ . '/fixtures/invalid-references.yaml')); + + $referenceResolver->resolve('#/components/parameters/CircularRefParam'); + } + + /** + * + */ + public function testNonLocalReference(): void + { + $this->expectException(ParserException::class); + $this->expectExceptionMessageIs('Invalid reference [ ./external.yaml#/components/schemas/IdSchema ]. Only local references supported.'); + + $referenceResolver = new ReferenceResolver(Yaml::parseFile(__DIR__ . '/fixtures/references.yaml')); + + $referenceResolver->resolve('./external.yaml#/components/schemas/IdSchema'); + } + + /** + * + */ + public function testFailedResolutionDoesNotLeaveReferenceOnStack(): void + { + $referenceResolver = new ReferenceResolver(Yaml::parseFile(__DIR__ . '/fixtures/broken-reference.yaml')); + + // First resolution fails because DoesNotExist is missing + + try { + $referenceResolver->resolve('#/components/schemas/DoesNotExist'); + + $this->fail('Expected exception was not thrown.'); + } + catch (ParserException $e) { + $this->assertSame( + 'Invalid reference [ #/components/schemas/DoesNotExist ]. The reference does not exist.', + $e->getMessage() + ); + } + + // Second resolution should fail for the exact same reason. + // Without the finally/array_pop cleanup it would instead + // report a circular reference on DoesNotExist. + + try { + $referenceResolver->resolve('#/components/schemas/DoesNotExist'); + + $this->fail('Expected exception was not thrown.'); + } + catch (ParserException $e) { + $this->assertSame( + 'Invalid reference [ #/components/schemas/DoesNotExist ]. The reference does not exist.', + $e->getMessage() + ); + } + } +} diff --git a/tests/unit/parser/fixtures/basic.yaml b/tests/unit/parser/fixtures/basic.yaml new file mode 100644 index 0000000..3d94654 --- /dev/null +++ b/tests/unit/parser/fixtures/basic.yaml @@ -0,0 +1,28 @@ +openapi: 3.0.0 +info: + title: 'Example API' + version: 1.0.0 +paths: + /: + get: + operationId: app\http\controllers\RootGet + responses: + '200': + description: 'root:get' + post: + operationId: app\http\controllers\RootPost + responses: + '200': + description: 'root:post' + /foo: + get: + operationId: app\http\controllers\FooGet + responses: + '200': + description: 'foo:get' + post: + operationId: app\http\controllers\FooPost + responses: + '200': + description: 'foo:post' + diff --git a/tests/unit/parser/fixtures/broken-reference.yaml b/tests/unit/parser/fixtures/broken-reference.yaml new file mode 100644 index 0000000..af4d640 --- /dev/null +++ b/tests/unit/parser/fixtures/broken-reference.yaml @@ -0,0 +1,21 @@ +openapi: 3.0.0 +info: + title: 'Example API' + version: 1.0.0 +paths: + '/foobar/{id}': + parameters: + - $ref: '#/components/parameters/IdParam' + get: + summary: Returns current user information. + operationId: 'app\http\controllers\user\FoobarGet' + responses: + '200': +components: + parameters: + IdParam: + name: id + in: path + required: true + schema: + $ref: '#/components/schemas/DoesNotExist' diff --git a/tests/unit/parser/fixtures/invalid-references.yaml b/tests/unit/parser/fixtures/invalid-references.yaml new file mode 100644 index 0000000..9053f07 --- /dev/null +++ b/tests/unit/parser/fixtures/invalid-references.yaml @@ -0,0 +1,31 @@ +openapi: 3.0.0 +info: + title: 'Example API' + version: 1.0.0 +paths: + '/foobar/{id}': + parameters: + - $ref: '#/components/parameters/IdParam' + - $ref: '#/components/parameters/CircularRefParam' + get: + summary: Returns current user information. + operationId: 'app\http\controllers\user\FoobarGet' + responses: + '200': +components: + parameters: + IdParam: + name: id + in: path + required: true + schema: + $ref: './external.yaml#/components/schemas/IdSchema' # non-local reference + CircularRefParam: + name: self + in: query + required: false + schema: + $ref: '#/components/schemas/CircularRefSchema' + schemas: + CircularRefSchema: + $ref: '#/components/schemas/CircularRefSchema' # self-referencing reference diff --git a/tests/unit/parser/fixtures/missing-operation-id.yaml b/tests/unit/parser/fixtures/missing-operation-id.yaml new file mode 100644 index 0000000..0bb888a --- /dev/null +++ b/tests/unit/parser/fixtures/missing-operation-id.yaml @@ -0,0 +1,11 @@ +openapi: 3.0.0 +info: + title: 'Example API' + version: 1.0.0 +paths: + /: + get: + responses: + '200': + description: 'root:get' + diff --git a/tests/unit/parser/fixtures/missing-paths.yaml b/tests/unit/parser/fixtures/missing-paths.yaml new file mode 100644 index 0000000..def510c --- /dev/null +++ b/tests/unit/parser/fixtures/missing-paths.yaml @@ -0,0 +1,5 @@ +openapi: 3.0.0 +info: + title: 'Example API' + version: 1.0.0 + diff --git a/tests/unit/parser/fixtures/parameters.yaml b/tests/unit/parser/fixtures/parameters.yaml new file mode 100644 index 0000000..fbd1ac6 --- /dev/null +++ b/tests/unit/parser/fixtures/parameters.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.0 +info: + title: 'Example API' + version: 1.0.0 +paths: + /articles/{articleId}/{slug}: + parameters: + - name: articleId + in: path + required: true + schema: + type: integer + format: auto-increment + - name: slug + in: path + required: true + schema: + type: string + get: + operationId: app\http\controllers\ArticlesGet + parameters: + - name: slug + in: path + required: false + schema: + type: string + - name: q1 + in: query + required: false + schema: + type: boolean + - name: q2 + in: query + required: true + schema: + type: boolean + - name: q3 + in: query + schema: + type: boolean + responses: + '200': + description: 'root:get' + diff --git a/tests/unit/parser/fixtures/references.yaml b/tests/unit/parser/fixtures/references.yaml new file mode 100644 index 0000000..68f8682 --- /dev/null +++ b/tests/unit/parser/fixtures/references.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.0 +info: + title: 'Example API' + version: 1.0.0 +paths: + '/foobar/{id}': + parameters: + - $ref: '#/components/parameters/IdParam' + get: + summary: Returns current user information. + operationId: 'app\http\controllers\user\FoobarGet' + responses: + '200': +components: + parameters: + IdParam: + name: id + in: path + required: true + schema: + $ref: '#/components/schemas/IdSchema' + schemas: + IdSchema: + type: string + format: foobar