Skip to content

Commit

Permalink
feat(openapi): document error outputs using json-schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Jan 27, 2025
1 parent d676103 commit 0d1473f
Show file tree
Hide file tree
Showing 16 changed files with 624 additions and 264 deletions.
26 changes: 24 additions & 2 deletions src/JsonApi/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\State\ApiResource\Error;

/**
* Decorator factory which adds JSON:API properties to the JSON Schema document.
Expand All @@ -31,6 +32,14 @@
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
{
use ResourceMetadataTrait;

/**
* As JSON:API recommends using [includes](https://jsonapi.org/format/#fetching-includes) instead of groups
* this flag allows to force using groups to generate the JSON:API JSONSchema. Defaults to true, use it in
* a serializer context.
*/
public const DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS = 'disable_json_schema_serializer_groups';

private const LINKS_PROPS = [
'type' => 'object',
'properties' => [
Expand Down Expand Up @@ -124,14 +133,27 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
}
// We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources.
// That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, [], $forceCollection);
$serializerContext ??= $this->getSerializerContext($operation ?? $this->findOperation($className, $type, $operation, $serializerContext, $format), $type);
$jsonApiSerializerContext = !($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true) ? $serializerContext : [];
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection);

if (($key = $schema->getRootDefinitionKey()) || ($key = $schema->getItemsDefinitionKey())) {
$definitions = $schema->getDefinitions();
$properties = $definitions[$key]['properties'] ?? [];

if (Error::class === $className && !isset($properties['errors'])) {
$definitions[$key]['properties'] = [
'errors' => [
'type' => 'object',
'properties' => $properties,
],
];

return $schema;
}

// Prevent reapplying
if (isset($properties['id'], $properties['type']) || isset($properties['data'])) {
if (isset($properties['id'], $properties['type']) || isset($properties['data']) || isset($properties['errors'])) {
return $schema;
}

Expand Down
13 changes: 9 additions & 4 deletions src/JsonSchema/ResourceMetadataTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private function findOutputClass(string $className, string $type, Operation $ope
return $forceSubschema ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null);
}

private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext): Operation
private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext, ?string $format = null): Operation
{
if (null === $operation) {
if (null === $this->resourceMetadataFactory) {
Expand All @@ -54,7 +54,7 @@ private function findOperation(string $className, string $type, ?Operation $oper
$operation = new HttpOperation();
}

return $this->findOperationForType($resourceMetadataCollection, $type, $operation);
return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $format);
}

// The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise
Expand All @@ -65,13 +65,13 @@ private function findOperation(string $className, string $type, ?Operation $oper
return $resourceMetadataCollection->getOperation($operation->getName());
}

return $this->findOperationForType($resourceMetadataCollection, $type, $operation);
return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $format);
}

return $operation;
}

private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation): Operation
private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation, ?string $format = null): Operation
{
// Find the operation and use the first one that matches criterias
foreach ($resourceMetadataCollection as $resourceMetadata) {
Expand All @@ -85,6 +85,11 @@ private function findOperationForType(ResourceMetadataCollection $resourceMetada
$operation = $op;
break 2;
}

if ($format && Schema::TYPE_OUTPUT === $type && \array_key_exists($format, $op->getOutputFormats() ?? [])) {
$operation = $op;
break 2;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public function __construct(
*/
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
{
if (!is_a($resourceClass, Model::class, true)) {
return $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty();
}

try {
$refl = new \ReflectionClass($resourceClass);
$model = $refl->newInstanceWithoutConstructor();
Expand Down
54 changes: 27 additions & 27 deletions src/Metadata/ApiProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ public function getProperty(): ?string
return $this->property;
}

public function withProperty(string $property): self
public function withProperty(string $property): static
{
$self = clone $this;
$self->property = $property;
Expand All @@ -244,7 +244,7 @@ public function getDescription(): ?string
return $this->description;
}

public function withDescription(string $description): self
public function withDescription(string $description): static
{
$self = clone $this;
$self->description = $description;
Expand All @@ -257,7 +257,7 @@ public function isReadable(): ?bool
return $this->readable;
}

public function withReadable(bool $readable): self
public function withReadable(bool $readable): static
{
$self = clone $this;
$self->readable = $readable;
Expand All @@ -270,7 +270,7 @@ public function isWritable(): ?bool
return $this->writable;
}

public function withWritable(bool $writable): self
public function withWritable(bool $writable): static
{
$self = clone $this;
$self->writable = $writable;
Expand All @@ -283,7 +283,7 @@ public function isReadableLink(): ?bool
return $this->readableLink;
}

public function withReadableLink(bool $readableLink): self
public function withReadableLink(bool $readableLink): static
{
$self = clone $this;
$self->readableLink = $readableLink;
Expand All @@ -296,7 +296,7 @@ public function isWritableLink(): ?bool
return $this->writableLink;
}

public function withWritableLink(bool $writableLink): self
public function withWritableLink(bool $writableLink): static
{
$self = clone $this;
$self->writableLink = $writableLink;
Expand All @@ -309,7 +309,7 @@ public function isRequired(): ?bool
return $this->required;
}

public function withRequired(bool $required): self
public function withRequired(bool $required): static
{
$self = clone $this;
$self->required = $required;
Expand All @@ -322,7 +322,7 @@ public function isIdentifier(): ?bool
return $this->identifier;
}

public function withIdentifier(bool $identifier): self
public function withIdentifier(bool $identifier): static
{
$self = clone $this;
$self->identifier = $identifier;
Expand All @@ -335,7 +335,7 @@ public function getDefault()
return $this->default;
}

public function withDefault($default): self
public function withDefault($default): static
{
$self = clone $this;
$self->default = $default;
Expand All @@ -348,7 +348,7 @@ public function getExample(): mixed
return $this->example;
}

public function withExample(mixed $example): self
public function withExample(mixed $example): static
{
$self = clone $this;
$self->example = $example;
Expand All @@ -361,7 +361,7 @@ public function getDeprecationReason(): ?string
return $this->deprecationReason;
}

public function withDeprecationReason($deprecationReason): self
public function withDeprecationReason($deprecationReason): static
{
$self = clone $this;
$self->deprecationReason = $deprecationReason;
Expand All @@ -374,7 +374,7 @@ public function isFetchable(): ?bool
return $this->fetchable;
}

public function withFetchable($fetchable): self
public function withFetchable($fetchable): static
{
$self = clone $this;
$self->fetchable = $fetchable;
Expand All @@ -387,7 +387,7 @@ public function getFetchEager(): ?bool
return $this->fetchEager;
}

public function withFetchEager($fetchEager): self
public function withFetchEager($fetchEager): static
{
$self = clone $this;
$self->fetchEager = $fetchEager;
Expand All @@ -400,7 +400,7 @@ public function getJsonldContext(): ?array
return $this->jsonldContext;
}

public function withJsonldContext($jsonldContext): self
public function withJsonldContext($jsonldContext): static
{
$self = clone $this;
$self->jsonldContext = $jsonldContext;
Expand All @@ -413,7 +413,7 @@ public function getOpenapiContext(): ?array
return $this->openapiContext;
}

public function withOpenapiContext($openapiContext): self
public function withOpenapiContext($openapiContext): static
{
$self = clone $this;
$self->openapiContext = $openapiContext;
Expand All @@ -426,7 +426,7 @@ public function getJsonSchemaContext(): ?array
return $this->jsonSchemaContext;
}

public function withJsonSchemaContext($jsonSchemaContext): self
public function withJsonSchemaContext($jsonSchemaContext): static
{
$self = clone $this;
$self->jsonSchemaContext = $jsonSchemaContext;
Expand All @@ -439,7 +439,7 @@ public function getPush(): ?bool
return $this->push;
}

public function withPush($push): self
public function withPush($push): static
{
$self = clone $this;
$self->push = $push;
Expand All @@ -452,7 +452,7 @@ public function getSecurity(): ?string
return $this->security instanceof \Stringable ? (string) $this->security : $this->security;
}

public function withSecurity($security): self
public function withSecurity($security): static
{
$self = clone $this;
$self->security = $security;
Expand All @@ -465,7 +465,7 @@ public function getSecurityPostDenormalize(): ?string
return $this->securityPostDenormalize instanceof \Stringable ? (string) $this->securityPostDenormalize : $this->securityPostDenormalize;
}

public function withSecurityPostDenormalize($securityPostDenormalize): self
public function withSecurityPostDenormalize($securityPostDenormalize): static
{
$self = clone $this;
$self->securityPostDenormalize = $securityPostDenormalize;
Expand All @@ -481,7 +481,7 @@ public function getTypes(): ?array
/**
* @param string[]|string $types
*/
public function withTypes(array|string $types = []): self
public function withTypes(array|string $types = []): static
{
$self = clone $this;
$self->types = (array) $types;
Expand All @@ -500,7 +500,7 @@ public function getBuiltinTypes(): ?array
/**
* @param Type[] $builtinTypes
*/
public function withBuiltinTypes(array $builtinTypes = []): self
public function withBuiltinTypes(array $builtinTypes = []): static
{
$self = clone $this;
$self->builtinTypes = $builtinTypes;
Expand All @@ -513,15 +513,15 @@ public function getSchema(): ?array
return $this->schema;
}

public function withSchema(array $schema = []): self
public function withSchema(array $schema = []): static
{
$self = clone $this;
$self->schema = $schema;

return $self;
}

public function withInitializable(bool $initializable): self
public function withInitializable(?bool $initializable): static
{
$self = clone $this;
$self->initializable = $initializable;
Expand All @@ -539,7 +539,7 @@ public function getExtraProperties(): ?array
return $this->extraProperties;
}

public function withExtraProperties(array $extraProperties = []): self
public function withExtraProperties(array $extraProperties = []): static
{
$self = clone $this;
$self->extraProperties = $extraProperties;
Expand All @@ -560,7 +560,7 @@ public function getIris()
*
* @param string|string[] $iris
*/
public function withIris(string|array $iris): self
public function withIris(string|array $iris): static
{
$metadata = clone $this;
$metadata->iris = (array) $iris;
Expand All @@ -576,7 +576,7 @@ public function getGenId()
return $this->genId;
}

public function withGenId(bool $genId): self
public function withGenId(bool $genId): static
{
$metadata = clone $this;
$metadata->genId = $genId;
Expand All @@ -594,7 +594,7 @@ public function getUriTemplate(): ?string
return $this->uriTemplate;
}

public function withUriTemplate(?string $uriTemplate): self
public function withUriTemplate(?string $uriTemplate): static
{
$metadata = clone $this;
$metadata->uriTemplate = $uriTemplate;
Expand Down
Loading

0 comments on commit 0d1473f

Please sign in to comment.