Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
a8cb33d
Extract validation into Objects
charjr Jan 2, 2024
2be79af
Document what validation is performed
charjr Jan 16, 2024
801351a
Appease PHPStan
charjr Jan 16, 2024
c10dbf0
Change fields on Partial PathItem
charjr Jan 16, 2024
aee5d2c
Validate "paths" is set on V3.0 OpenAPI Objects
charjr Jan 16, 2024
1a158b8
Add convenience methods to Warnings
charjr Jan 17, 2024
a71d20a
Invalidate duplicate Parameters in Path Item V3.0
charjr Jan 17, 2024
c82bd2f
Add DocBlocks for V3.0. Value Object fields
charjr Jan 17, 2024
2f94f3b
Refactor V3.0. Parameter
charjr Jan 17, 2024
03f386f
Add Server Objects
charjr Jan 18, 2024
c842ddf
Rename infection.json to infection.json5
charjr Mar 7, 2024
3aef647
Warn against equivalent parameter names in different case
charjr Mar 7, 2024
c965556
Add todo for guidance on how server urls should be treated
charjr Mar 7, 2024
29dd360
Test V3.0 PathItem warns against having no Operations
charjr Mar 26, 2024
867388f
Rename V3.0. PathItem test for clarity
charjr Mar 26, 2024
6409213
Remove warning on V3.0. PathItem
charjr Mar 26, 2024
76965e6
Test V3.0. Server contains correct ServerVariables
charjr Mar 26, 2024
9190a06
Ensure properties on V3.0. PathItem match OpenAPI behaviour
charjr Mar 26, 2024
df0876d
Add findWarningsByCode to Warnings
charjr Mar 26, 2024
cfe70b3
Warn against trailing "/" in Server urls
charjr Mar 26, 2024
179c95e
Remove redundant exception in MembraneReader
charjr Mar 27, 2024
8f9f5f5
Simplify test for V30 OpenAPI
charjr Mar 27, 2024
19f64a7
Correct namespace for MembraneReaderTest
charjr Mar 27, 2024
64dbb4e
Test isRedundant() on Method Enum
charjr Mar 27, 2024
9b3b374
Improve coverage of Readers handling Server Variables
charjr Mar 27, 2024
d25a6d9
Remove redundant hasVariables method from Server
charjr Mar 27, 2024
bf79bb2
Test Parameter can have any valid style per location
charjr Mar 27, 2024
84cd681
Test Server throws Exception for ServerVariables without names
charjr Mar 27, 2024
b34e870
Test PathItem and Operation warn against duplicate Servers
charjr Mar 27, 2024
6bc3a4e
Add IsIdentical and isSimilar to V3.0. Parameter
charjr Mar 27, 2024
eb9eaf1
Test Parameter will set a default Style
charjr Mar 27, 2024
8582c5c
Test that Parameter can check if it is identical or similary named
charjr Mar 27, 2024
6bcb846
Refactor Operation and PathItem
charjr Mar 27, 2024
0bb8abe
Test Operation and PathItem warn about similar Parameters
charjr Mar 27, 2024
9381763
Remove redundant Exception constructor
charjr Mar 27, 2024
435bd1f
Test Parameter correctly defaults optional fields
charjr Mar 27, 2024
305d515
Exclude src/Exception from Infection
charjr Mar 27, 2024
cca9ae9
Move Warning constructor to top of class
charjr Mar 27, 2024
17e7970
Refactor Operation
charjr Mar 27, 2024
5479487
Rename Warnings methods
charjr Mar 28, 2024
aecf322
Extend canItBeThisType Schema method
charjr Mar 28, 2024
c0a8e3e
Update handling of conflicting Styles
charjr Mar 28, 2024
f57fa3b
Move Validated Object Enums
charjr Mar 28, 2024
19a26e3
Annotate that complex subSchemas cannot be empty
charjr Mar 28, 2024
9455309
Add Type Enum
charjr Mar 28, 2024
d41d8d9
Move logic to related Enums
charjr Apr 10, 2024
a764020
Move MethodTest to correct directory
charjr Apr 10, 2024
95e61a3
Add Schema methods for checking Type
charjr Apr 11, 2024
26f1711
Add Factory tests for invalid OpenAPI specs
charjr Apr 11, 2024
fe5d01c
Test logic in Enums
charjr Apr 12, 2024
4695778
Test Schema Warnings
charjr Apr 12, 2024
1227f07
Refactor Parameter Warning
charjr Apr 12, 2024
07e4308
Test Parameter handles different explodes and deepObject
charjr Apr 12, 2024
1c5648d
Remove unnecessary Exceptions
charjr Apr 12, 2024
aa21a88
Test Schema checks for Invalid Types
charjr Apr 12, 2024
8fb9df9
Instantiate Warnings in Constructor
charjr Apr 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"autoload-dev": {
"psr-4": {
"Membrane\\OpenAPIReader\\Tests\\Fixtures\\Helper\\": "tests/fixtures/Helper",
"Membrane\\OpenAPIReader\\Tests\\Fixtures\\": "tests/fixtures/",
"Membrane\\OpenAPIReader\\Tests\\": "tests/"
}
Expand All @@ -19,7 +20,7 @@
},
"require-dev": {
"phpunit/phpunit": "^10.1",
"phpstan/phpstan": "^1.10.19",
"phpstan/phpstan": "^1.10.56",
"squizlabs/php_codesniffer": "^3.7",
"mikey179/vfsstream": "^1.6.7",
"infection/infection": "^0.27.0"
Expand Down
58 changes: 58 additions & 0 deletions docs/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Validation Performed By OpenAPI Reader

## Additional Requirements For Membrane.

### Specify an OperationId

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`.

This is used for identification of all available operations across your OpenAPI.

### Unambiguous Query Strings

For query parameters (i.e. `in:query`) with a `schema` that allows compound types i.e. `array` or `objects`
there are certain combinations of `style` and `explode` that do not use the parameter's `name`.

These combinations are:
- `type:object` with `style:form` and `explode:true`
- `type:object` or `type:array` with `style:spaceDelimited`
- `type:object` or `type:array` with `style:pipeDelimited`

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.

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.

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.

## Version 3.0.X

### OpenAPI Object

- [An OpenAPI Object requires an `openapi` field](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields).
- [An OpenAPI Object requires an `info` field](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields).
- [The Info Object requires a `title`](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-1).
- [The Info Object requires a `version`](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-1).
- [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).
### Path Item

- [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).
- [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)

### Operation

- [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)

### Parameter

- 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).
- 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).
- `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).
- [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).
- 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).
- [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).
- if `content` is specified, it MUST contain exactly one Media Type
- A Parameter's MediaType MUST contain a schema.

### Schema

- [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).
11 changes: 7 additions & 4 deletions infection.json5
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
"source": {
"directories": [
"src"
],
"excludes": [
"Exception", // We don't need the Exception messages being altered
]
},
minCoveredMsi: 90,
minMsi: 80,
"minCoveredMsi": 90,
"minMsi": 80,
"mutators": {
"@default": true,
},
"@default": true
}
}
95 changes: 95 additions & 0 deletions src/CebeReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIReader;

use cebe\{openapi as Cebe, openapi\exceptions as CebeException, openapi\spec as CebeSpec};
use Closure;
use Membrane\OpenAPIReader\Exception\{CannotRead, CannotSupport, InvalidOpenAPI};
use Membrane\OpenAPIReader\Factory\V30\FromCebe;
use Symfony\Component\Yaml\Exception\ParseException;
use TypeError;

class CebeReader
{
/** @param OpenAPIVersion[] $supportedVersions */
public function __construct(
private readonly array $supportedVersions,
) {
if (empty($this->supportedVersions)) {
throw CannotSupport::noSupportedVersions();
}
(fn (OpenAPIVersion ...$versions) => null)(...$this->supportedVersions);
}

public function readFromAbsoluteFilePath(string $absoluteFilePath, ?FileFormat $fileFormat = null): CebeSpec\OpenApi
{
file_exists($absoluteFilePath) ?: throw CannotRead::fileNotFound($absoluteFilePath);

$fileFormat ??= FileFormat::fromFileExtension(pathinfo($absoluteFilePath, PATHINFO_EXTENSION));

try {
$openAPI = $this->getCebeObject(match ($fileFormat) {
FileFormat::Json => fn() => Cebe\Reader::readFromJsonFile($absoluteFilePath),
FileFormat::Yaml => fn() => Cebe\Reader::readFromYamlFile($absoluteFilePath),
default => throw CannotRead::unrecognizedFileFormat($absoluteFilePath)
});
} catch (CebeException\UnresolvableReferenceException $e) {
throw CannotRead::unresolvedReference($e);
}

$this->validate($openAPI);

return $openAPI;
}

public function readFromString(string $openAPI, FileFormat $fileFormat): CebeSpec\OpenApi
{
if (preg_match('#\s*[\'\"]?\$ref[\'\"]?\s*:\s*[\'\"]?[^\s\'\"\#]#', $openAPI)) {
throw CannotRead::cannotResolveExternalReferencesFromString();
}

$openAPI = $this->getCebeObject(match ($fileFormat) {
FileFormat::Json => fn() => Cebe\Reader::readFromJson($openAPI),
FileFormat::Yaml => fn() => Cebe\Reader::readFromYaml($openAPI),
});

try {
$openAPI->resolveReferences(new Cebe\ReferenceContext($openAPI, '/tmp'));
} catch (CebeException\UnresolvableReferenceException $e) {
throw CannotRead::unresolvedReference($e);
}

$this->validate($openAPI);

return $openAPI;
}

/** @param Closure():CebeSpec\OpenApi $readOpenAPI */
private function getCebeObject(Closure $readOpenAPI): CebeSpec\OpenApi
{
try {
return $readOpenAPI();
} catch (TypeError | CebeException\TypeErrorException | ParseException $e) {
throw CannotRead::invalidFormatting($e);
}
}

private function validate(CebeSpec\OpenApi $openAPI): void
{
$this->isVersionSupported($openAPI->openapi) ?: throw CannotSupport::unsupportedVersion($openAPI->openapi);

/** Currently only 3.0 Validated Objects exist */
if (OpenAPIVersion::fromString($openAPI->openapi) === OpenAPIVersion::Version_3_0) {
FromCebe::createOpenAPI($openAPI);
}

$openAPI->validate() ?: throw InvalidOpenAPI::failedCebeValidation(...$openAPI->getErrors());
}

private function isVersionSupported(string $version): bool
{
return in_array(OpenAPIVersion::fromString($version), $this->supportedVersions, true);
}
}
31 changes: 22 additions & 9 deletions src/Exception/CannotSupport.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@
/*
* This exception occurs if your Open API is readable but cannot be supported by Membrane.
*/

final class CannotSupport extends RuntimeException
{
public const UNSUPPORTED_METHOD = 0;
public const UNSUPPORTED_VERSION = 1;
public const MISSING_OPERATION_ID = 2;
public const UNSUPPORTED_VERSION = 0;
public const MISSING_OPERATION_ID = 1;
public const MISSING_TYPE_DECLARATION = 2;
public const AMBIGUOUS_RESOLUTION = 3;
public const CANNOT_PARSE = 4;

public static function unsupportedMethod(string $pathUrl, string $method): self
public static function membraneReaderOnlySupportsv30(): self
{
$message = <<<TEXT
Membrane does not currently support the method: '$method'.
Found on Path: '$pathUrl'
TEXT;
return new self($message, self::UNSUPPORTED_METHOD);
$message = 'MembraneReader currently only supports Version 3.0.X';
return new self($message, self::UNSUPPORTED_VERSION);
}

public static function noSupportedVersions(): self
Expand All @@ -46,4 +46,17 @@ public static function missingOperationId(string $pathUrl, string $method): self
TEXT;
return new self($message, self::MISSING_OPERATION_ID);
}

public static function conflictingParameterStyles(string ...$parameters): self
{
$message = sprintf(
<<<'TEXT'
The following parameters lead to ambiguous resolution:
%s
TEXT,
implode(",\n", $parameters)
);

return new self($message, self::AMBIGUOUS_RESOLUTION);
}
}
Loading