Skip to content

Commit ef539f3

Browse files
authored
Merge pull request #7 from membrane-php/own-api-objects
Validation with own API value objects
2 parents 191662d + 8fb9df9 commit ef539f3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+7236
-189
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"autoload-dev": {
1111
"psr-4": {
12+
"Membrane\\OpenAPIReader\\Tests\\Fixtures\\Helper\\": "tests/fixtures/Helper",
1213
"Membrane\\OpenAPIReader\\Tests\\Fixtures\\": "tests/fixtures/",
1314
"Membrane\\OpenAPIReader\\Tests\\": "tests/"
1415
}
@@ -19,7 +20,7 @@
1920
},
2021
"require-dev": {
2122
"phpunit/phpunit": "^10.1",
22-
"phpstan/phpstan": "^1.10.19",
23+
"phpstan/phpstan": "^1.10.56",
2324
"squizlabs/php_codesniffer": "^3.7",
2425
"mikey179/vfsstream": "^1.6.7",
2526
"infection/infection": "^0.27.0"

docs/validation.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Validation Performed By OpenAPI Reader
2+
3+
## Additional Requirements For Membrane.
4+
5+
### Specify an OperationId
6+
7+
Membrane requires all Operations to set a [unique](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-8) `operationId`.
8+
9+
This is used for identification of all available operations across your OpenAPI.
10+
11+
### Unambiguous Query Strings
12+
13+
For query parameters (i.e. `in:query`) with a `schema` that allows compound types i.e. `array` or `objects`
14+
there are certain combinations of `style` and `explode` that do not use the parameter's `name`.
15+
16+
These combinations are:
17+
- `type:object` with `style:form` and `explode:true`
18+
- `type:object` or `type:array` with `style:spaceDelimited`
19+
- `type:object` or `type:array` with `style:pipeDelimited`
20+
21+
If an operation only has one query parameter (i.e. `in:query`) then this is fine. Membrane can safely assume the entire string belongs to that one parameter.
22+
23+
If an operation contains two query parameters, both of which do not use the parameter's name; Membrane cannot ascertain which parameter relates to which part of the query string.
24+
25+
This ambiguity leads to multiple "correct" ways to interpret the query string. Making it impossible to safely assume Membrane has validated it. Therefore, only one parameter, with one of the above combinations, is allowed on any given Operation.
26+
27+
## Version 3.0.X
28+
29+
### OpenAPI Object
30+
31+
- [An OpenAPI Object requires an `openapi` field](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields).
32+
- [An OpenAPI Object requires an `info` field](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields).
33+
- [The Info Object requires a `title`](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-1).
34+
- [The Info Object requires a `version`](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-1).
35+
- [All Path Items must be mapped to by their relative endpoint](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#paths-object).
36+
### Path Item
37+
38+
- [All Operations MUST be mapped to by a method](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-7).
39+
- [Parameters must be unique. Uniqueness is defined by a combination of "name" and "in".](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-7)
40+
41+
### Operation
42+
43+
- [Parameters must be unique. Uniqueness is defined by a combination of "name" and "in".](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-8)
44+
45+
### Parameter
46+
47+
- A Parameter [MUST contain a `name` field](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-10).
48+
- A Parameter [MUST contain an `in` field](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-10).
49+
- `in` [MUST be set to `path`, `query`, `header` or `cookie`](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-10).
50+
- [if `in:path` then the Parameter MUST specify `required:true`](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-10).
51+
- if `style` is specified, [acceptable values depend on the value of `in`](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#style-values).
52+
- [A Parameter MUST contain a `schema` or `content`, but not both](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-10).
53+
- if `content` is specified, it MUST contain exactly one Media Type
54+
- A Parameter's MediaType MUST contain a schema.
55+
56+
### Schema
57+
58+
- [If allOf, anyOf or oneOf are set; They MUST not be empty](https://json-schema.org/draft/2020-12/json-schema-core#section-10.2).

infection.json5

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
"source": {
44
"directories": [
55
"src"
6+
],
7+
"excludes": [
8+
"Exception", // We don't need the Exception messages being altered
69
]
710
},
8-
minCoveredMsi: 90,
9-
minMsi: 80,
11+
"minCoveredMsi": 90,
12+
"minMsi": 80,
1013
"mutators": {
11-
"@default": true,
12-
},
14+
"@default": true
15+
}
1316
}

src/CebeReader.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Membrane\OpenAPIReader;
6+
7+
use cebe\{openapi as Cebe, openapi\exceptions as CebeException, openapi\spec as CebeSpec};
8+
use Closure;
9+
use Membrane\OpenAPIReader\Exception\{CannotRead, CannotSupport, InvalidOpenAPI};
10+
use Membrane\OpenAPIReader\Factory\V30\FromCebe;
11+
use Symfony\Component\Yaml\Exception\ParseException;
12+
use TypeError;
13+
14+
class CebeReader
15+
{
16+
/** @param OpenAPIVersion[] $supportedVersions */
17+
public function __construct(
18+
private readonly array $supportedVersions,
19+
) {
20+
if (empty($this->supportedVersions)) {
21+
throw CannotSupport::noSupportedVersions();
22+
}
23+
(fn (OpenAPIVersion ...$versions) => null)(...$this->supportedVersions);
24+
}
25+
26+
public function readFromAbsoluteFilePath(string $absoluteFilePath, ?FileFormat $fileFormat = null): CebeSpec\OpenApi
27+
{
28+
file_exists($absoluteFilePath) ?: throw CannotRead::fileNotFound($absoluteFilePath);
29+
30+
$fileFormat ??= FileFormat::fromFileExtension(pathinfo($absoluteFilePath, PATHINFO_EXTENSION));
31+
32+
try {
33+
$openAPI = $this->getCebeObject(match ($fileFormat) {
34+
FileFormat::Json => fn() => Cebe\Reader::readFromJsonFile($absoluteFilePath),
35+
FileFormat::Yaml => fn() => Cebe\Reader::readFromYamlFile($absoluteFilePath),
36+
default => throw CannotRead::unrecognizedFileFormat($absoluteFilePath)
37+
});
38+
} catch (CebeException\UnresolvableReferenceException $e) {
39+
throw CannotRead::unresolvedReference($e);
40+
}
41+
42+
$this->validate($openAPI);
43+
44+
return $openAPI;
45+
}
46+
47+
public function readFromString(string $openAPI, FileFormat $fileFormat): CebeSpec\OpenApi
48+
{
49+
if (preg_match('#\s*[\'\"]?\$ref[\'\"]?\s*:\s*[\'\"]?[^\s\'\"\#]#', $openAPI)) {
50+
throw CannotRead::cannotResolveExternalReferencesFromString();
51+
}
52+
53+
$openAPI = $this->getCebeObject(match ($fileFormat) {
54+
FileFormat::Json => fn() => Cebe\Reader::readFromJson($openAPI),
55+
FileFormat::Yaml => fn() => Cebe\Reader::readFromYaml($openAPI),
56+
});
57+
58+
try {
59+
$openAPI->resolveReferences(new Cebe\ReferenceContext($openAPI, '/tmp'));
60+
} catch (CebeException\UnresolvableReferenceException $e) {
61+
throw CannotRead::unresolvedReference($e);
62+
}
63+
64+
$this->validate($openAPI);
65+
66+
return $openAPI;
67+
}
68+
69+
/** @param Closure():CebeSpec\OpenApi $readOpenAPI */
70+
private function getCebeObject(Closure $readOpenAPI): CebeSpec\OpenApi
71+
{
72+
try {
73+
return $readOpenAPI();
74+
} catch (TypeError | CebeException\TypeErrorException | ParseException $e) {
75+
throw CannotRead::invalidFormatting($e);
76+
}
77+
}
78+
79+
private function validate(CebeSpec\OpenApi $openAPI): void
80+
{
81+
$this->isVersionSupported($openAPI->openapi) ?: throw CannotSupport::unsupportedVersion($openAPI->openapi);
82+
83+
/** Currently only 3.0 Validated Objects exist */
84+
if (OpenAPIVersion::fromString($openAPI->openapi) === OpenAPIVersion::Version_3_0) {
85+
FromCebe::createOpenAPI($openAPI);
86+
}
87+
88+
$openAPI->validate() ?: throw InvalidOpenAPI::failedCebeValidation(...$openAPI->getErrors());
89+
}
90+
91+
private function isVersionSupported(string $version): bool
92+
{
93+
return in_array(OpenAPIVersion::fromString($version), $this->supportedVersions, true);
94+
}
95+
}

src/Exception/CannotSupport.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@
99
/*
1010
* This exception occurs if your Open API is readable but cannot be supported by Membrane.
1111
*/
12+
1213
final class CannotSupport extends RuntimeException
1314
{
14-
public const UNSUPPORTED_METHOD = 0;
15-
public const UNSUPPORTED_VERSION = 1;
16-
public const MISSING_OPERATION_ID = 2;
15+
public const UNSUPPORTED_VERSION = 0;
16+
public const MISSING_OPERATION_ID = 1;
17+
public const MISSING_TYPE_DECLARATION = 2;
18+
public const AMBIGUOUS_RESOLUTION = 3;
19+
public const CANNOT_PARSE = 4;
1720

18-
public static function unsupportedMethod(string $pathUrl, string $method): self
21+
public static function membraneReaderOnlySupportsv30(): self
1922
{
20-
$message = <<<TEXT
21-
Membrane does not currently support the method: '$method'.
22-
Found on Path: '$pathUrl'
23-
TEXT;
24-
return new self($message, self::UNSUPPORTED_METHOD);
23+
$message = 'MembraneReader currently only supports Version 3.0.X';
24+
return new self($message, self::UNSUPPORTED_VERSION);
2525
}
2626

2727
public static function noSupportedVersions(): self
@@ -46,4 +46,17 @@ public static function missingOperationId(string $pathUrl, string $method): self
4646
TEXT;
4747
return new self($message, self::MISSING_OPERATION_ID);
4848
}
49+
50+
public static function conflictingParameterStyles(string ...$parameters): self
51+
{
52+
$message = sprintf(
53+
<<<'TEXT'
54+
The following parameters lead to ambiguous resolution:
55+
%s
56+
TEXT,
57+
implode(",\n", $parameters)
58+
);
59+
60+
return new self($message, self::AMBIGUOUS_RESOLUTION);
61+
}
4962
}

0 commit comments

Comments
 (0)