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
32 changes: 28 additions & 4 deletions src/Exception/InvalidOpenAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
*/
final class InvalidOpenAPI extends RuntimeException
{
public static function missingRequiredField(
Identifier $identifier,
string $requiredField,
): self {
return new InvalidOpenAPI(<<<TEXT
$identifier
- Missing required field "$requiredField"
TEXT);
}

public static function missingInfo(): self
{
$message = <<<TEXT
Expand Down Expand Up @@ -256,11 +266,12 @@ public static function duplicateOperationIds(
return new self($message);
}

public static function mustHaveSchemaXorContent(string $name): self
public static function mustHaveSchemaXorContent(Identifier $identifier): self
{
return new self(
sprintf('Parameter "%s" MUST have either a Schema or Content, but not both.', $name),
);
return new self(<<<TEXT
$identifier
Parameter MUST have either a Schema or Content, but not both.
TEXT);
}

public static function parameterContentCanOnlyHaveOneEntry(Identifier $identifier): self
Expand Down Expand Up @@ -421,4 +432,17 @@ public static function mustHaveStringKeys(

return new self($message);
}

public static function responseCodeMustBeNumericOrDefault(
Identifier $identifier,
string $code,
): self {
$message = <<<TEXT
$identifier
Response code MUST be numeric, or "default".
"$code" is invalid.
TEXT;

return new self($message);
}
}
35 changes: 35 additions & 0 deletions src/Factory/V30/FromCebe.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
namespace Membrane\OpenAPIReader\Factory\V30;

use cebe\openapi\spec as Cebe;
use Membrane\OpenAPIReader\ValueObject\Partial\Header;
use Membrane\OpenAPIReader\ValueObject\Partial\MediaType;
use Membrane\OpenAPIReader\ValueObject\Partial\OpenAPI;
use Membrane\OpenAPIReader\ValueObject\Partial\Operation;
use Membrane\OpenAPIReader\ValueObject\Partial\Parameter;
use Membrane\OpenAPIReader\ValueObject\Partial\PathItem;
use Membrane\OpenAPIReader\ValueObject\Partial\RequestBody;
use Membrane\OpenAPIReader\ValueObject\Partial\Response;
use Membrane\OpenAPIReader\ValueObject\Partial\Schema;
use Membrane\OpenAPIReader\ValueObject\Partial\Server;
use Membrane\OpenAPIReader\ValueObject\Partial\ServerVariable;
Expand Down Expand Up @@ -210,11 +212,19 @@ private static function createOperation(
return null;
}


$responses = [];
foreach ($operation->responses ?? [] as $code => $response) {
$responses[$code] = self::createResponse($response);
}


return new Operation(
operationId: $operation->operationId,
servers: self::createServers($operation->servers),
parameters: self::createParameters($operation->parameters),
requestBody: self::createRequestBody($operation->requestBody),
responses: $responses,
);
}

Expand All @@ -233,4 +243,29 @@ private static function createRequestBody(
$requestBody->required ?? false,
);
}

public static function createHeader(
Cebe\Reference|Cebe\Header $header
): Header {
return new Header(
description: $header->description ?? null,
style: $header->style ?? 'simple',
explode: $header->explode ?? false,
required: $header->required ?? false,
schema: isset($header->schema) ? self::createSchema($header->schema) : null,
content: isset($header->content) ? self::createContent($header->content) : [],
);
}

private static function createResponse(
Cebe\Reference|Cebe\Response $response,
): Response {
assert(! $response instanceof Cebe\Reference);

return new Response(
description: $response->description,
headers: array_map(fn($h) => self::createHeader($h), $response->headers),
content: self::createContent($response->content ?? []),
);
}
}
21 changes: 21 additions & 0 deletions src/ValueObject/Partial/Header.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIReader\ValueObject\Partial;

final class Header
{
/**
* @param array<MediaType> $content
*/
public function __construct(
public string|null $description = null,
public string|null $style = null,
public bool|null $explode = null,
public bool|null $required = null,
public Schema|null $schema = null,
public array $content = [],
) {
}
}
2 changes: 2 additions & 0 deletions src/ValueObject/Partial/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ final class Operation
/**
* @param Server[] $servers
* @param Parameter[] $parameters
* @param Response[] $responses
*/
public function __construct(
public string|null $operationId = null,
public array $servers = [],
public array $parameters = [],
public RequestBody|null $requestBody = null,
public array $responses = [],
) {
}
}
19 changes: 19 additions & 0 deletions src/ValueObject/Partial/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIReader\ValueObject\Partial;

final class Response
{
/**
* @param array<string,Header> $headers,
* @param MediaType[] $content
*/
public function __construct(
public string|null $description = null,
public array $headers = [],
public array $content = [],
) {
}
}
19 changes: 19 additions & 0 deletions src/ValueObject/Valid/Parameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIReader\ValueObject\Valid;

use Membrane\OpenAPIReader\ValueObject\Valid\Enum\Style;

interface Parameter
{
public function getSchema(): Schema;

/**
* @phpstan-assert-if-true string $this->getMediaType()
* @phpstan-assert-if-false null $this->getMediaType()
*/
public function hasMediaType(): bool;
public function getMediaType(): ?string;
}
123 changes: 123 additions & 0 deletions src/ValueObject/Valid/V30/Header.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIReader\ValueObject\Valid\V30;

use Membrane\OpenAPIReader\Exception\InvalidOpenAPI;
use Membrane\OpenAPIReader\ValueObject\Partial;
use Membrane\OpenAPIReader\ValueObject\Valid\Enum\In;
use Membrane\OpenAPIReader\ValueObject\Valid\Enum\Style;
use Membrane\OpenAPIReader\ValueObject\Valid\Enum\Type;
use Membrane\OpenAPIReader\ValueObject\Valid\Identifier;
use Membrane\OpenAPIReader\ValueObject\Valid\Validated;
use Membrane\OpenAPIReader\ValueObject\Valid\Warning;
use Membrane\OpenAPIReader\ValueObject\Valid;

final class Header extends Validated implements Valid\Parameter
{
public readonly Style $style;
public readonly bool $explode;
public readonly bool $required;

/**
* A Parameter MUST define a "schema" or "content" but not both
*/
public readonly ?Schema $schema;

/**
* A Parameter MUST have a "schema" or "content" but not both
* If "content" is defined:
* - It MUST contain only one Media Type.
* - That MediaType MUST define a schema.
* @var array<string,MediaType>
*/
public readonly array $content;

public function __construct(
Identifier $identifier,
Partial\Header $header
) {
parent::__construct($identifier);

$this->style = $this->validateStyle($identifier, $header->style);
$this->required = $header->required ?? false;
$this->explode = $header->explode ?? Style::Simple->defaultExplode();


isset($header->schema) === empty($header->content) ?:
throw InvalidOpenAPI::mustHaveSchemaXorContent($identifier);

if (isset($header->schema)) {
$this->content = [];
$this->schema = new Schema(
$this->appendedIdentifier('schema'),
$header->schema
);
} else {
$this->schema = null;

$this->content = $this->validateContent(
$this->getIdentifier(),
$header->content
);
}
}

public function getSchema(): Schema
{
if (isset($this->schema)) {
return $this->schema;
} else {
assert(array_values($this->content)[0]->schema !== null);
return array_values($this->content)[0]->schema;
}
}

public function hasMediaType(): bool
{
return !empty($this->content);
}

public function getMediaType(): ?string
{
return array_key_first($this->content);
}

private function validateStyle(Identifier $identifier, ?string $style): Style
{
$defaultStyle = Style::default(In::Header);

if ($style !== null && Style::from($style) !== $defaultStyle) {
throw InvalidOpenAPI::parameterIncompatibleStyle($identifier);
}

return $defaultStyle;
}

/**
* @param array<Partial\MediaType> $content
* @return array<string,MediaType>
*/
private function validateContent(Identifier $identifier, array $content): array
{
if (count($content) !== 1) {
throw InvalidOpenAPI::parameterContentCanOnlyHaveOneEntry($identifier);
}

if (!isset($content[0]->contentType)) {
throw InvalidOpenAPI::contentMissingMediaType($identifier);
}

if (!isset($content[0]->schema)) {
throw InvalidOpenAPI::mustHaveSchemaXorContent($identifier);
}

return [
$content[0]->contentType => new MediaType(
$this->appendedIdentifier($content[0]->contentType),
$content[0]
),
];
}
}
35 changes: 35 additions & 0 deletions src/ValueObject/Valid/V30/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Membrane\OpenAPIReader\ValueObject\Valid\Validated;
use Membrane\OpenAPIReader\ValueObject\Valid\Warning;

//TODO valid operation needs to validateResponses
final class Operation extends Validated
{
/**
Expand All @@ -26,13 +27,16 @@ final class Operation extends Validated
* @param string $operationId
* Required by Membrane
* MUST be unique, value is case-sensitive.
*
* @param Response[] $responses
*/
private function __construct(
Identifier $identifier,
public readonly string $operationId,
public readonly array $servers,
public readonly array $parameters,
public readonly RequestBody|null $requestBody,
public readonly array $responses,
) {
parent::__construct($identifier);

Expand All @@ -48,6 +52,7 @@ public function withoutServers(): Operation
[new Server($this->getIdentifier(), new Partial\Server('/'))],
$this->parameters,
$this->requestBody,
$this->responses,
);
}

Expand Down Expand Up @@ -86,12 +91,15 @@ public static function fromPartial(
new RequestBody($identifier, $operation->requestBody) :
null;

$responses = self::validateResponses($identifier, $operation->responses);

return new Operation(
$identifier,
$operationId,
$servers,
$parameters,
$requestBody,
$responses,
);
}

Expand Down Expand Up @@ -214,4 +222,31 @@ private static function mergeParameters(

return array_values($result);
}

/**
* @param Partial\Response[] $responses
* @return array<string, Response>
*/
private static function validateResponses(
Identifier $identifier,
array $responses,
): array {
$result = [];

foreach ($responses as $code => $response) {
if (! is_numeric($code) && $code !== 'default') {
throw InvalidOpenAPI::responseCodeMustBeNumericOrDefault(
$identifier,
(string) $code,
);
}

$result[(string) $code] = new Response(
$identifier->append('response', (string) $code),
$response,
);
}

return $result;
}
}
Loading