Skip to content

Open API generator changes #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 31 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ad15ae4
Added the optional included block
joshmurrayeu Jun 23, 2025
6f27fd6
Add the include path parameter to every endpoint which has relations
joshmurrayeu Jun 23, 2025
ef1747b
Typo within the parameter location
joshmurrayeu Jun 23, 2025
635686e
Added a GeneratorInterface to allow for decorator patten usage
joshmurrayeu Jun 23, 2025
6e7ab1a
Add the pagination parameters to all endpoints
joshmurrayeu Jun 25, 2025
faaa315
Added the .idea to the .gitignore
joshmurrayeu Jun 26, 2025
6d301e8
Add the ability to add a summary to the Open API doc
joshmurrayeu Jun 26, 2025
51fb507
Only add the pagination query params when the endpoint can be paginated
joshmurrayeu Jun 26, 2025
8da719e
Added descriptions to the response blocks to form a valid doc
joshmurrayeu Jun 26, 2025
5eaafc5
Added an array filter to strip out empty parameters
joshmurrayeu Jun 26, 2025
68c7e9c
Remove the content if 204 no content
joshmurrayeu Jun 26, 2025
4e0399d
Only add the pagination parameters if a valid pagination resolver has…
joshmurrayeu Jun 26, 2025
589122b
Updated the Show endpoint Open API docs
joshmurrayeu Jun 26, 2025
a827d63
Added an array values to reset the keys so that an array is created r…
joshmurrayeu Jun 26, 2025
c1bea6c
Added some error response methods
joshmurrayeu Jun 27, 2025
4655da0
Use the new error methods within the endpoints
joshmurrayeu Jun 27, 2025
39d2767
Added in links to the open api generation
joshmurrayeu Jun 27, 2025
75a56d7
Added a return type plus exception instead of Laravel's dd()
joshmurrayeu Jun 27, 2025
ff88b05
Add links = true to allow for pagination links to be included
joshmurrayeu Jun 27, 2025
59f541a
Fixed a bug which prevented the links from being generated properly
joshmurrayeu Jun 27, 2025
5b215f0
Added the ability to pass the collection name to create collection do…
joshmurrayeu Jun 30, 2025
5645a85
Return the result of the handler else return 204
joshmurrayeu Jun 30, 2025
3d24d48
Modified required to be a closure to allow for logic behind the requi…
joshmurrayeu Jul 3, 2025
20271bc
Allow writable fields to show up in the Open API 'create' schemas
joshmurrayeu Jul 3, 2025
7f13b89
Fixed an issue where id was required even in create requests
joshmurrayeu Jul 7, 2025
a007509
Add the filters to the Open API document
joshmurrayeu Jul 7, 2025
3be10f0
Small bug fixing the filter parameters
joshmurrayeu Jul 9, 2025
2bb4a2c
Added countable and paginatable (works for now but should be re-visited)
joshmurrayeu Jul 9, 2025
d22c657
Fixed some required flags and ID to be required and readonly
joshmurrayeu Jul 10, 2025
2bd5d0f
Small bug fix to ensure required is an array and not an object
joshmurrayeu Jul 18, 2025
83910dd
Use the Pagination interface rather than the concrete class
joshmurrayeu Jul 25, 2025
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ node_modules
.phpunit.result.cache
docs/.vitepress/dist
docs/.vitepress/cache
.idea/*
17 changes: 13 additions & 4 deletions src/Endpoint/CollectionAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -52,20 +56,25 @@ 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
{
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(),
],
],
],
Expand Down
284 changes: 281 additions & 3 deletions src/Endpoint/Concerns/BuildsOpenApiPaths.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,299 @@

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 [
JsonApi::MEDIA_TYPE => [
'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',
],
],
],
]),
],
],
],
],
],
],
Expand Down
4 changes: 2 additions & 2 deletions src/Endpoint/Concerns/SavesData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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));
Expand Down
Loading