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
+[](https://github.com/mako-framework/open-api/actions/workflows/tests.yml)
[](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