Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"require": {
"php": "^8.1.0",
"symfony/yaml": "^4 || ^5 || ^6 || ^7",
"devizzent/cebe-php-openapi": "^1.1.2"
},
"require-dev": {
Expand Down
97 changes: 97 additions & 0 deletions docs/object-simplifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# How The Reader Simplifies Your OpenAPI

Besides simply parsing your OpenAPI into `Validated` objects,
the reader is designed to _simplify the developer experience_ of other OpenAPI tools.

## Standard Simplifications

### Never Worry About Uninitialized Properties
Properties should be safe to access:
All properties have a value.
`null` represents an omitted field _if_ no other value can be safely assumed.

### Strong Typehints
Data structures should be easily discoverable.
All properties should have strong typehints.

## Opinionated Simplifications

### Narrow Schemas To False If Impossible To Pass
The `false` boolean schema explicitly states any input will fail.
The Reader will narrow schemas to `false` if it is proven impossible to pass.
This optimizes code that may otherwise validate input that will always fail.

#### Enum Specified Empty
Any schema specifying an empty `enum`, is narrowed to the `false` boolean schema.

If `enum` is specified, it contains the exhaustive list of valid values.
If `enum` is specified as an empty array, there are no valid values.

#### Enum Without A Valid Value
If a schema specifies `enum` without a value that passes the rest of the schema;
it is impossible to pass, it will be narrowed to the `false` boolean schema.

### Narrow Typehints
Typehints are narrowed if it has no impact on expressiveness.

#### AllOf, AnyOf and OneOf Are Always Arrays

`allOf`, `anyOf` and `oneOf` can express two things:
1. There are subschemas
2. There are not

To express there are no subschemas, the value is omitted.

As such, the Reader structures `allOf`, `anyOf` and `oneOf` in two ways:
1. A non-empty array
2. An empty array

Though these keywords are not allowed to be empty,
[The Reader allows it](validation-deviations.md#allof-anyof-and-oneof-can-be-empty)
for the sake of simplicity.

This simplifies code that loops through subschemas.

#### String Metadata Is Always String

Optional metadata is expressed in two ways:
1. There is data
2. There is not

As such, the Reader structures metadata in two ways:
1. A string containing non-whitespace characters
2. An empty string `''`

When accessing string metadata only one check is necessary:

```php
if ($metadata !== '') {
// do something...
}
```

### Combined Fields
Data is combined, if and only if, it has no impact on expressiveness.

#### Maximum|Minimum are combined with ExclusiveMaximum|ExclusiveMinimum

[//]: # (TODO explain how they are combined for 3.0 and in 3.1 we take the more restrictive keyword)

A numerical limit can only be expressed in three ways:
- There is no limit
- There is an inclusive limit
- There is an exclusive limit

As such the Reader combines the relevant keywords into:
- `Limit|null $maximum`.
- `Limit|null $minimum`.

Where `Limit` has two properties:
- `float|int $limit`
- `bool $exclusive`

#### Const Overrides Enum in 3.1

[//]: # (TODO Flesh out a bit)

The more restrictive keyword takes precedence.
84 changes: 84 additions & 0 deletions docs/validation-deviations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Validation And How It Deviates From OpenAPI Specification
This page documents where validation deviates from the OpenAPI Specification.

## Stricter Requirements
It is stricter when it comes to providing an unambiguous specification.
This helps to avoid confusion as well as simplify development for other OpenAPI tools.

### OperationId Is Required

Operations MUST have an [operationId](https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.0.3.md#fixed-fields-8).

`operationId` is a unique string used to identify an operation.
By making it required, it serves as a _reliable_ method of identification.

### Query Strings Must Be Unambiguous

Operations MUST NOT use more than one _ambiguous parameter_.

In this context, an _ambiguous parameter_ is defined as being `in:query` with one of the following combinations:
- `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 everything else can be identified; then through a process of elimination, the ambiguous segment belongs to the _ambiguous parameter_

If an operation contains two or more _ambiguous parameters_, then there are multiple ways of interpreting the ambiguous segment.
This ambiguity means the query string cannot be resolved deterministically.
As such, it is not allowed.

## Looser Requirements

These requirements are looser than the OpenAPI Specification.
Where the OpenAPI Specification would be invalid, the reader will add a warning to the `Validated` object.

### MultipleOf Can Be Negative

Normally `multipleOf` MUST be a positive non-zero number.

The Reader allows `multipleOf` to be any non-zero number.
A multiple of a negative number is also a multiple of its absolute value.
It's more confusing, but what is expressed is identical.

Therefore, if a negative value is given:
- You will receive a Warning
- The absolute value will be used

### AllOf, AnyOf and OneOf Can Be Empty

Normally, `allOf`, `anyOf` and `oneOf` MUST be non-empty.

The Reader allows them to be empty.
If any of these keywords are omitted, they are treated as empty arrays.


Therefore, if an empty array is given:
- You will receive a Warning
- An empty array will be used.

If it is omitted:
- An empty array will be used.

[This is done for simplicity](object-simplifications.md#allof-anyof-and-oneof-are-always-arrays).

### Required Can Be Empty

OpenAPI 3.0 states `required` MUST be non-empty.
OpenAPI 3.1 allows `required` to be empty and defaults to empty if omitted.

The Reader always allows `required` to be empty and defaults to empty if omitted.
This allows us to narrow the typehint for `required` to always being an array.

If an empty array is given:
- If your API is using 3.0, you will receive a Warning
- An empty array will be used

### Required Can Contain Duplicates

Normally `required` MUST contain unique values.

The Reader allows `required` to contain duplicates.

If a duplicate item is found:
- You will receive a Warning
- The duplicate will be removed.
58 changes: 0 additions & 58 deletions docs/validation.md

This file was deleted.

25 changes: 19 additions & 6 deletions src/Exception/InvalidOpenAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Membrane\OpenAPIReader\Exception;

use Membrane\OpenAPIReader\ValueObject\Valid\Enum\Type;
use Membrane\OpenAPIReader\ValueObject\Valid\Identifier;
use RuntimeException;

Expand Down Expand Up @@ -312,11 +313,14 @@ public static function invalidType(Identifier $identifier, string $type): self
return new self($message);
}

public static function typeArrayInWrongVersion(Identifier $identifier): self
{
public static function keywordMustBeType(
Identifier $identifier,
string $keyword,
Type $type,
): self {
$message = <<<TEXT
$identifier
Specifying type as an array is only valid for OpenAPI ^3.1
$keyword MUST be $type->value
TEXT;

return new self($message);
Expand Down Expand Up @@ -356,19 +360,19 @@ public static function boolExclusiveMinMaxIn31(
return new self($message);
}

public static function keywordMustBeStrictlyPositiveNumber(
public static function keywordCannotBeZero(
Identifier $identifier,
string $keyword,
): self {
$message = <<<TEXT
$identifier
$keyword MUST be strictly greater than zero
$keyword MUST not be zero
TEXT;

return new self($message);
}

public static function keywordMustBeNegativeInteger(
public static function keywordMustBeNonNegativeInteger(
Identifier $identifier,
string $keyword,
): self {
Expand Down Expand Up @@ -445,4 +449,13 @@ public static function responseCodeMustBeNumericOrDefault(

return new self($message);
}

public static function defaultMustConformToType(
Identifier $identifier,
): self {
return new self(<<<TEXT
$identifier
- default MUST conform to type
TEXT);
}
}
2 changes: 1 addition & 1 deletion src/Factory/V30/FromCebe.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ private static function createParameters(array $parameters): array

private static function createSchema(
Cebe\Reference|Cebe\Schema|null $schema
): ?Schema {
): Schema|null {
assert(!$schema instanceof Cebe\Reference);

if ($schema === null) {
Expand Down
Loading
Loading