diff --git a/.gitignore b/.gitignore index 12723e9..f8b6f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules .phpunit.result.cache docs/.vitepress/dist docs/.vitepress/cache +.idea/* diff --git a/src/Endpoint/CollectionAction.php b/src/Endpoint/CollectionAction.php index 0310976..5deec0f 100644 --- a/src/Endpoint/CollectionAction.php +++ b/src/Endpoint/CollectionAction.php @@ -6,17 +6,21 @@ use Nyholm\Psr7\Response; use Psr\Http\Message\ResponseInterface; use Tobyz\JsonApiServer\Context; +use Tobyz\JsonApiServer\Endpoint\Concerns\BuildsOpenApiPaths; use Tobyz\JsonApiServer\Exception\ForbiddenException; use Tobyz\JsonApiServer\Exception\MethodNotAllowedException; use Tobyz\JsonApiServer\OpenApi\OpenApiPathsProvider; use Tobyz\JsonApiServer\Resource\Collection; use Tobyz\JsonApiServer\Schema\Concerns\HasDescription; +use Tobyz\JsonApiServer\Schema\Concerns\HasSummary; use Tobyz\JsonApiServer\Schema\Concerns\HasVisibility; class CollectionAction implements Endpoint, OpenApiPathsProvider { use HasVisibility; + use HasSummary; use HasDescription; + use BuildsOpenApiPaths; public string $method = 'POST'; @@ -52,9 +56,7 @@ public function handle(Context $context): ?ResponseInterface throw new ForbiddenException(); } - ($this->handler)($context); - - return new Response(204); + return ($this->handler)($context) ?? new Response(204); } public function getOpenApiPaths(Collection $collection): array @@ -62,10 +64,17 @@ public function getOpenApiPaths(Collection $collection): array return [ "/{$collection->name()}/$this->name" => [ 'post' => [ + 'summary' => $this->getSummary(), 'description' => $this->getDescription(), 'tags' => [$collection->name()], 'responses' => [ - '204' => [], + '204' => [ + 'description' => 'No Content', + ], + '400' => $this->buildBadRequestErrorResponse(), + '401' => $this->buildUnauthorizedErrorResponse(), + '403' => $this->buildForbiddenErrorResponse(), + '500' => $this->buildInternalServerErrorResponse(), ], ], ], diff --git a/src/Endpoint/Concerns/BuildsOpenApiPaths.php b/src/Endpoint/Concerns/BuildsOpenApiPaths.php index 65cf5a1..ae28af6 100644 --- a/src/Endpoint/Concerns/BuildsOpenApiPaths.php +++ b/src/Endpoint/Concerns/BuildsOpenApiPaths.php @@ -2,12 +2,27 @@ namespace Tobyz\JsonApiServer\Endpoint\Concerns; +use ReflectionException; +use ReflectionFunction; +use Tobyz\JsonApiServer\Endpoint\Index; use Tobyz\JsonApiServer\JsonApi; +use Tobyz\JsonApiServer\Resource\Collection; +use Tobyz\JsonApiServer\Resource\Resource; +use Tobyz\JsonApiServer\Schema\Field\Field; +use Tobyz\JsonApiServer\Schema\Field\Relationship; +use Tobyz\JsonApiServer\Schema\Filter; trait BuildsOpenApiPaths { - private function buildOpenApiContent(array $resources, bool $multiple = false): array - { + private function buildOpenApiContent( + string $name, + array $resources, + bool $multiple = false, + bool $included = true, + bool $links = false, + bool $countable = false, + bool $paginatable = false, + ): array { $item = count($resources) === 1 ? $resources[0] : ['oneOf' => $resources]; return [ @@ -15,8 +30,271 @@ private function buildOpenApiContent(array $resources, bool $multiple = false): 'schema' => [ 'type' => 'object', 'required' => ['data'], - 'properties' => [ + 'properties' => array_filter([ + 'links' => $links ? $this->buildLinksObject($name) : [], 'data' => $multiple ? ['type' => 'array', 'items' => $item] : $item, + 'included' => $included ? ['type' => 'array'] : [], + 'meta' => $this->buildMetaObject($countable, $paginatable), + ]), + ], + ], + ]; + } + + private function buildLinksObject(string $name): array + { + // @todo: maybe pull in the API or Context to return a server name? + $baseUri = sprintf('https://{server}/%s', $name); + $defaultQuery = ['page[limit]' => 10]; + + $links = [ + 'self' => ['page[offset]' => 2], + 'first' => ['page[offset]' => 1], + 'prev' => ['page[offset]' => 1], + 'next' => ['page[offset]' => 3], + 'last' => ['page[offset]' => 10], + ]; + + foreach ($links as $key => $params) { + $params = $params + $defaultQuery; + + $query = implode( + '&', + array_map( + fn($k, $v) => $k . '=' . urlencode($v), + array_keys($params), + $params, + ) + ); + + $links[$key] = sprintf('%s/%s', $baseUri, $query); + } + + return [ + 'type' => 'object', + 'properties' => array_map(function (string $uri) { + return [ + 'type' => 'string', + 'example' => $uri, + ]; + }, $links), + ]; + } + + private function buildMetaObject(bool $countable, bool $paginatable): array + { + if (!($countable || $paginatable)) { + return []; + } + + return [ + 'type' => 'object', + 'properties' => [ + 'page' => [ + 'type' => 'object', + 'properties' => array_filter([ + 'total' => $countable ? ['type' => 'integer'] : [], + 'limit' => $paginatable ? ['type' => 'integer'] : [], + 'offset' => $paginatable ? ['type' => 'integer'] : [], + ]), + ], + ], + ]; + } + + /** + * @throws ReflectionException + */ + private function buildOpenApiParameters(Collection $collection): array + { + // @todo: fix this + assert($collection instanceof Resource); + + $parameters = [ + $this->buildIncludeParameter($collection), + ...$this->buildFilterParameters($collection), + ...$this->buildPaginatableParameters(), + ]; + + return array_values(array_filter($parameters)); + } + + private function buildIncludeParameter(Resource $resource): array + { + $relationshipNames = array_map( + fn(Relationship $relationship) => $relationship->name, + array_filter( + $resource->fields(), + fn(Field $field) => $field instanceof Relationship && $field->includable, + ), + ); + + if (empty($relationshipNames)) { + return []; + } + + $includes = implode(', ', $relationshipNames); + + return [ + 'name' => 'include', + 'in' => 'query', + 'description' => "Available include parameters: {$includes}.", + 'schema' => [ + 'type' => 'string', + ], + ]; + } + + + private function buildFilterParameters(Resource $resource): array + { + if (!$this instanceof Index) { + return []; + } + + return array_map(function (Filter $filter) { + return [ + 'name' => "filter[{$filter->name}]", + 'in' => 'query', + 'description' => $filter->getDescription(), + 'schema' => [ + 'type' => 'string', + ], + ]; + }, $resource->filters()); + } + + /** + * @throws ReflectionException + */ + private function buildPaginatableParameters(): array + { + if (property_exists($this, 'paginationResolver')) { + $resolver = $this->paginationResolver; + $reflection = new ReflectionFunction($resolver); + + if ($reflection->getNumberOfRequiredParameters() > 0) { + return [ + [ + 'name' => 'page[limit]', + 'in' => 'query', + 'description' => "The limit pagination field.", + 'schema' => [ + 'type' => 'number', + ], + ], + [ + 'name' => 'page[offset]', + 'in' => 'query', + 'description' => "The offset pagination field.", + 'schema' => [ + 'type' => 'number', + ], + ], + ]; + } + } + + return []; + } + + public function buildBadRequestErrorResponse(): array + { + return $this->buildErrorResponse( + 'A bad request.', + 400, + 'Bad Request', + 'Please try again with a valid request.', + ); + } + + public function buildUnauthorizedErrorResponse(): array + { + return $this->buildErrorResponse( + 'An unauthorised error.', + 401, + 'Unauthorized', + 'Please login and try again.', + ); + } + + public function buildForbiddenErrorResponse(): array + { + return $this->buildErrorResponse( + 'A forbidden error.', + 403, + 'Forbidden', + ); + } + + public function buildNotFoundErrorResponse(): array + { + return $this->buildErrorResponse( + 'A bad request.', + 404, + 'Not Found', + 'The requested resource could not be found.', + ); + } + + public function buildInternalServerErrorResponse(): array + { + return $this->buildErrorResponse( + 'A bad request.', + 500, + 'Internal Server Error', + 'Please try again later.', + ); + } + + public function buildErrorResponse(string $description, int $status, string $title, ?string $detail = null): array + { + return [ + 'description' => $description, + 'content' => [ + JsonApi::MEDIA_TYPE => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'errors' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'required' => [ + 'status', + 'title', + ], + 'properties' => array_filter([ + 'status' => [ + 'type' => 'string', + 'example' => (string)$status, + ], + 'title' => [ + 'type' => 'string', + 'example' => $title, + ], + 'detail' => [ + 'type' => 'string', + 'example' => $detail, + ], + 'source' => [ + 'type' => 'object', + 'properties' => [ + 'pointer' => [ + 'type' => 'string', + ], + 'parameter' => [ + 'type' => 'string', + ], + 'header' => [ + 'type' => 'string', + ], + ], + ], + ]), + ], + ], + ], ], ], ], diff --git a/src/Endpoint/Concerns/SavesData.php b/src/Endpoint/Concerns/SavesData.php index 6dbf534..719f74c 100644 --- a/src/Endpoint/Concerns/SavesData.php +++ b/src/Endpoint/Concerns/SavesData.php @@ -176,7 +176,7 @@ private function assertDataValid(Context $context, array $data, bool $validateAl foreach ($context->fields($context->resource) as $field) { $empty = !has_value($data, $field); - if ($empty && (!$field->required || !$validateAll)) { + if ($empty && (!$field->isRequired($context) || !$validateAll)) { continue; } @@ -187,7 +187,7 @@ private function assertDataValid(Context $context, array $data, bool $validateAl ]; }; - if ($empty && $field->required) { + if ($empty && $field->isRequired($context)) { $fail('field is required'); } else { $field->validateValue(get_value($data, $field), $fail, $context->withField($field)); diff --git a/src/Endpoint/Create.php b/src/Endpoint/Create.php index 0ba8a1c..8d6aaea 100644 --- a/src/Endpoint/Create.php +++ b/src/Endpoint/Create.php @@ -14,6 +14,7 @@ use Tobyz\JsonApiServer\Resource\Collection; use Tobyz\JsonApiServer\Resource\Creatable; use Tobyz\JsonApiServer\Schema\Concerns\HasDescription; +use Tobyz\JsonApiServer\Schema\Concerns\HasSummary; use Tobyz\JsonApiServer\Schema\Concerns\HasVisibility; use function Tobyz\JsonApiServer\has_value; @@ -25,6 +26,7 @@ class Create implements Endpoint, OpenApiPathsProvider use HasVisibility; use SavesData; use ShowsResources; + use HasSummary; use HasDescription; use BuildsOpenApiPaths; @@ -109,11 +111,14 @@ public function getOpenApiPaths(Collection $collection): array return [ "/{$collection->name()}" => [ 'post' => [ + 'summary' => $this->getSummary(), 'description' => $this->getDescription(), 'tags' => [$collection->name()], + 'parameters' => $this->buildOpenApiParameters($collection), 'requestBody' => [ 'required' => true, 'content' => $this->buildOpenApiContent( + $collection->name(), array_map( fn($resource) => [ '$ref' => "#/components/schemas/{$resource}Create", @@ -124,13 +129,20 @@ public function getOpenApiPaths(Collection $collection): array ], 'responses' => [ '200' => [ + 'description' => 'Resource created successfully.', 'content' => $this->buildOpenApiContent( + $collection->name(), array_map( fn($resource) => ['$ref' => "#/components/schemas/$resource"], $collection->resources(), ), ), ], + '400' => $this->buildBadRequestErrorResponse(), + '401' => $this->buildUnauthorizedErrorResponse(), + '403' => $this->buildForbiddenErrorResponse(), + '404' => $this->buildNotFoundErrorResponse(), + '500' => $this->buildInternalServerErrorResponse(), ], ], ], diff --git a/src/Endpoint/Delete.php b/src/Endpoint/Delete.php index f8b20c5..a55eb16 100644 --- a/src/Endpoint/Delete.php +++ b/src/Endpoint/Delete.php @@ -6,6 +6,7 @@ use Psr\Http\Message\ResponseInterface; use RuntimeException; use Tobyz\JsonApiServer\Context; +use Tobyz\JsonApiServer\Endpoint\Concerns\BuildsOpenApiPaths; use Tobyz\JsonApiServer\Endpoint\Concerns\FindsResources; use Tobyz\JsonApiServer\Exception\ForbiddenException; use Tobyz\JsonApiServer\Exception\MethodNotAllowedException; @@ -14,6 +15,7 @@ use Tobyz\JsonApiServer\Resource\Deletable; use Tobyz\JsonApiServer\Schema\Concerns\HasDescription; use Tobyz\JsonApiServer\Schema\Concerns\HasMeta; +use Tobyz\JsonApiServer\Schema\Concerns\HasSummary; use Tobyz\JsonApiServer\Schema\Concerns\HasVisibility; use function Tobyz\JsonApiServer\json_api_response; @@ -23,7 +25,9 @@ class Delete implements Endpoint, OpenApiPathsProvider use HasMeta; use HasVisibility; use FindsResources; + use HasSummary; use HasDescription; + use BuildsOpenApiPaths; public static function make(): static { @@ -72,6 +76,7 @@ public function getOpenApiPaths(Collection $collection): array return [ "/{$collection->name()}/{id}" => [ 'delete' => [ + 'summary' => $this->getSummary(), 'description' => $this->getDescription(), 'tags' => [$collection->name()], 'parameters' => [ @@ -83,7 +88,14 @@ public function getOpenApiPaths(Collection $collection): array ], ], 'responses' => [ - '204' => [], + '204' => [ + 'description' => 'No Content', + ], + '400' => $this->buildBadRequestErrorResponse(), + '401' => $this->buildUnauthorizedErrorResponse(), + '403' => $this->buildForbiddenErrorResponse(), + '404' => $this->buildNotFoundErrorResponse(), + '500' => $this->buildInternalServerErrorResponse(), ], ], ], diff --git a/src/Endpoint/Index.php b/src/Endpoint/Index.php index 32523ef..5616895 100644 --- a/src/Endpoint/Index.php +++ b/src/Endpoint/Index.php @@ -4,6 +4,7 @@ use Closure; use Psr\Http\Message\ResponseInterface as Response; +use ReflectionException; use RuntimeException; use Tobyz\JsonApiServer\Context; use Tobyz\JsonApiServer\Endpoint\Concerns\BuildsOpenApiPaths; @@ -19,6 +20,7 @@ use Tobyz\JsonApiServer\Resource\Listable; use Tobyz\JsonApiServer\Schema\Concerns\HasDescription; use Tobyz\JsonApiServer\Schema\Concerns\HasMeta; +use Tobyz\JsonApiServer\Schema\Concerns\HasSummary; use Tobyz\JsonApiServer\Schema\Concerns\HasVisibility; use Tobyz\JsonApiServer\Serializer; @@ -31,6 +33,7 @@ class Index implements Endpoint, OpenApiPathsProvider use HasMeta; use HasVisibility; use IncludesData; + use HasSummary; use HasDescription; use BuildsOpenApiPaths; @@ -173,23 +176,37 @@ private function applyFilters($query, Context $context): void } } + /** + * @throws ReflectionException + */ public function getOpenApiPaths(Collection $collection): array { return [ "/{$collection->name()}" => [ 'get' => [ + 'summary' => $this->getSummary(), 'description' => $this->getDescription(), 'tags' => [$collection->name()], + 'parameters' => $this->buildOpenApiParameters($collection), 'responses' => [ '200' => [ + 'description' => 'Successful list all response.', 'content' => $this->buildOpenApiContent( + $collection->name(), array_map( fn($resource) => ['$ref' => "#/components/schemas/$resource"], $collection->resources(), ), multiple: true, + links: true, + countable: true, + paginatable: true, ), ], + '400' => $this->buildBadRequestErrorResponse(), + '401' => $this->buildUnauthorizedErrorResponse(), + '403' => $this->buildForbiddenErrorResponse(), + '500' => $this->buildInternalServerErrorResponse(), ], ], ], diff --git a/src/Endpoint/ResourceAction.php b/src/Endpoint/ResourceAction.php index df02cff..e5e9468 100644 --- a/src/Endpoint/ResourceAction.php +++ b/src/Endpoint/ResourceAction.php @@ -13,6 +13,7 @@ use Tobyz\JsonApiServer\OpenApi\OpenApiPathsProvider; use Tobyz\JsonApiServer\Resource\Collection; use Tobyz\JsonApiServer\Schema\Concerns\HasDescription; +use Tobyz\JsonApiServer\Schema\Concerns\HasSummary; use Tobyz\JsonApiServer\Schema\Concerns\HasVisibility; use function Tobyz\JsonApiServer\json_api_response; @@ -20,6 +21,7 @@ class ResourceAction implements Endpoint, OpenApiPathsProvider { use HasVisibility; + use HasSummary; use HasDescription; use FindsResources; use ShowsResources; @@ -75,6 +77,7 @@ public function getOpenApiPaths(Collection $collection): array return [ "/{$collection->name()}/{id}/{$this->name}" => [ strtolower($this->method) => [ + 'summary' => $this->getSummary(), 'description' => $this->getDescription(), 'tags' => [$collection->name()], 'parameters' => [ @@ -87,13 +90,20 @@ public function getOpenApiPaths(Collection $collection): array ], 'responses' => [ '200' => [ + 'description' => 'Successful custom action response.', 'content' => $this->buildOpenApiContent( + $collection->name(), array_map( fn($resource) => ['$ref' => "#/components/schemas/$resource"], $collection->resources(), ), ), ], + '400' => $this->buildBadRequestErrorResponse(), + '401' => $this->buildUnauthorizedErrorResponse(), + '403' => $this->buildForbiddenErrorResponse(), + '404' => $this->buildNotFoundErrorResponse(), + '500' => $this->buildInternalServerErrorResponse(), ], ], ], diff --git a/src/Endpoint/Show.php b/src/Endpoint/Show.php index 6a9aca6..d6afb4c 100644 --- a/src/Endpoint/Show.php +++ b/src/Endpoint/Show.php @@ -12,6 +12,7 @@ use Tobyz\JsonApiServer\OpenApi\OpenApiPathsProvider; use Tobyz\JsonApiServer\Resource\Collection; use Tobyz\JsonApiServer\Schema\Concerns\HasDescription; +use Tobyz\JsonApiServer\Schema\Concerns\HasSummary; use Tobyz\JsonApiServer\Schema\Concerns\HasVisibility; use function Tobyz\JsonApiServer\json_api_response; @@ -21,6 +22,7 @@ class Show implements Endpoint, OpenApiPathsProvider use HasVisibility; use FindsResources; use ShowsResources; + use HasSummary; use HasDescription; use BuildsOpenApiPaths; @@ -52,28 +54,41 @@ public function handle(Context $context): ?ResponseInterface public function getOpenApiPaths(Collection $collection): array { + $parameters = array_merge( + [ + [ + 'name' => 'id', + 'in' => 'path', + 'required' => true, + 'schema' => ['type' => 'string'], + ], + ], + $this->buildOpenApiParameters($collection) + ); + return [ "/{$collection->name()}/{id}" => [ 'get' => [ + 'summary' => $this->getSummary(), 'description' => $this->getDescription(), 'tags' => [$collection->name()], - 'parameters' => [ - [ - 'name' => 'id', - 'in' => 'path', - 'required' => true, - 'schema' => ['type' => 'string'], - ], - ], + 'parameters' => $parameters, 'responses' => [ '200' => [ + 'description' => 'Successful show response.', 'content' => $this->buildOpenApiContent( + $collection->name(), array_map( fn($resource) => ['$ref' => "#/components/schemas/$resource"], $collection->resources(), ), ), ], + '400' => $this->buildBadRequestErrorResponse(), + '401' => $this->buildUnauthorizedErrorResponse(), + '403' => $this->buildForbiddenErrorResponse(), + '404' => $this->buildNotFoundErrorResponse(), + '500' => $this->buildInternalServerErrorResponse(), ], ], ], diff --git a/src/Endpoint/Update.php b/src/Endpoint/Update.php index 838fd8f..3680edb 100644 --- a/src/Endpoint/Update.php +++ b/src/Endpoint/Update.php @@ -15,6 +15,7 @@ use Tobyz\JsonApiServer\Resource\Collection; use Tobyz\JsonApiServer\Resource\Updatable; use Tobyz\JsonApiServer\Schema\Concerns\HasDescription; +use Tobyz\JsonApiServer\Schema\Concerns\HasSummary; use Tobyz\JsonApiServer\Schema\Concerns\HasVisibility; use function Tobyz\JsonApiServer\json_api_response; @@ -25,6 +26,7 @@ class Update implements Endpoint, OpenApiPathsProvider use FindsResources; use SavesData; use ShowsResources; + use HasSummary; use HasDescription; use BuildsOpenApiPaths; @@ -82,6 +84,7 @@ public function getOpenApiPaths(Collection $collection): array return [ "/{$collection->name()}/{id}" => [ 'patch' => [ + 'summary' => $this->getSummary(), 'description' => $this->getDescription(), 'tags' => [$collection->name()], 'parameters' => [ @@ -95,6 +98,7 @@ public function getOpenApiPaths(Collection $collection): array 'requestBody' => [ 'required' => true, 'content' => $this->buildOpenApiContent( + $collection->name(), array_map( fn($resource) => [ '$ref' => "#/components/schemas/{$resource}Update", @@ -105,13 +109,20 @@ public function getOpenApiPaths(Collection $collection): array ], 'responses' => [ '200' => [ + 'description' => 'Successful update response.', 'content' => $this->buildOpenApiContent( + $collection->name(), array_map( fn($resource) => ['$ref' => "#/components/schemas/$resource"], $collection->resources(), ), ), ], + '400' => $this->buildBadRequestErrorResponse(), + '401' => $this->buildUnauthorizedErrorResponse(), + '403' => $this->buildForbiddenErrorResponse(), + '404' => $this->buildNotFoundErrorResponse(), + '500' => $this->buildInternalServerErrorResponse(), ], ], ], diff --git a/src/OpenApi/Decorator.php b/src/OpenApi/Decorator.php new file mode 100644 index 0000000..119ed18 --- /dev/null +++ b/src/OpenApi/Decorator.php @@ -0,0 +1,10 @@ +writable) { $updateSchema[$location]['properties'][$field->name] = $fieldSchema; - if ($field->required) { + if ($field->isRequired()) { $updateSchema[$location]['required'][] = $field->name; } } - if ($field->writableOnCreate) { + if ($field->writable || $field->writableOnCreate) { $createSchema[$location]['properties'][$field->name] = $fieldSchema; - if ($field->required) { + if ($field->isRequired()) { $createSchema[$location]['required'][] = $field->name; } } @@ -62,14 +62,16 @@ public function generate(JsonApi $api): array $type = $resource->type(); $schemas[$type] = $this->buildSchema($resource, $schema, [ + 'required' => ['id'], 'properties' => ['id' => ['type' => 'string', 'readOnly' => true]], ]); - $schemas["{$type}Create"] = $this->buildSchema($resource, $createSchema, [ - 'required' => ['type'], - ]); + $schemas["{$type}Create"] = $this->buildSchema($resource, $createSchema); - $schemas["{$type}Update"] = $this->buildSchema($resource, $updateSchema); + $schemas["{$type}Update"] = $this->buildSchema($resource, $updateSchema, [ + 'required' => ['type', 'id'], + 'properties' => ['id' => ['type' => 'string']], + ]); } return array_filter([ @@ -88,13 +90,21 @@ public function generate(JsonApi $api): array private function buildSchema(Resource $resource, array $schema, array $overrides = []): array { + $hasAttributes = !empty($schema['attributes']); + $hasRelationships = !empty($schema['relationships']); + return array_replace_recursive( [ 'type' => 'object', - 'required' => ['type', 'id'], + 'required' => array_values( + array_filter([ + 'type', + $hasAttributes ? 'attributes' : null, + $hasRelationships ? 'relationships' : null, + ]) + ), 'properties' => [ 'type' => ['type' => 'string', 'const' => $resource->type()], - 'id' => ['type' => 'string'], 'attributes' => ['type' => 'object'] + ($schema['attributes'] ?? []), 'relationships' => ['type' => 'object'] + ($schema['relationships'] ?? []), ], diff --git a/src/OpenApi/OpenApiPathsProvider.php b/src/OpenApi/OpenApiPathsProvider.php index b066adf..023ec47 100644 --- a/src/OpenApi/OpenApiPathsProvider.php +++ b/src/OpenApi/OpenApiPathsProvider.php @@ -3,6 +3,7 @@ namespace Tobyz\JsonApiServer\OpenApi; use Tobyz\JsonApiServer\Resource\Collection; +use Tobyz\JsonApiServer\Resource\Resource; interface OpenApiPathsProvider { diff --git a/src/Resource/Paginatable.php b/src/Resource/Paginatable.php index 6da9d08..8021f21 100644 --- a/src/Resource/Paginatable.php +++ b/src/Resource/Paginatable.php @@ -2,12 +2,12 @@ namespace Tobyz\JsonApiServer\Resource; -use Tobyz\JsonApiServer\Pagination\OffsetPagination; +use Tobyz\JsonApiServer\Pagination\Pagination; interface Paginatable { /** * Paginate the given query. */ - public function paginate(object $query, OffsetPagination $pagination): void; + public function paginate(object $query, Pagination $pagination): void; } diff --git a/src/Schema/Concerns/HasSummary.php b/src/Schema/Concerns/HasSummary.php new file mode 100644 index 0000000..480af0a --- /dev/null +++ b/src/Schema/Concerns/HasSummary.php @@ -0,0 +1,23 @@ +summary = $summary; + + return $this; + } + + public function getSummary(): ?string + { + return $this->summary; + } +} diff --git a/src/Schema/Concerns/SetsValue.php b/src/Schema/Concerns/SetsValue.php index c6181e5..c334511 100644 --- a/src/Schema/Concerns/SetsValue.php +++ b/src/Schema/Concerns/SetsValue.php @@ -11,7 +11,7 @@ trait SetsValue { public ?Closure $writable = null; public ?Closure $writableOnCreate = null; - public bool $required = false; + public ?Closure $required = null; public ?Closure $default = null; public ?Closure $deserializer = null; public ?Closure $setter = null; @@ -41,9 +41,9 @@ public function writableOnCreate(?Closure $condition = null): static /** * Mark this field as required. */ - public function required(bool $required = true): static + public function required(?Closure $condition = null): static { - $this->required = $required; + $this->required = $condition ?: fn() => true; return $this; } @@ -121,6 +121,14 @@ public function isWritableOnCreate(Context $context): bool ($this->writableOnCreate && ($this->writableOnCreate)($context->model, $context)); } + /** + * Check if this field is required. + */ + public function isRequired(): bool + { + return $this->required && ($this->required)(); + } + /** * Deserialize a JSON value to an internal representation. */ diff --git a/src/Schema/Field/Relationship.php b/src/Schema/Field/Relationship.php index a5e229e..e6b1529 100644 --- a/src/Schema/Field/Relationship.php +++ b/src/Schema/Field/Relationship.php @@ -134,7 +134,7 @@ public function getSchema(JsonApi $api): array parent::getSchema($api) + [ 'type' => 'object', 'properties' => ['data' => $this->getDataSchema($api)], - 'required' => $this->required ? ['data'] : [], + 'required' => $this->isRequired() ? ['data'] : [], ]; }