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
2 changes: 1 addition & 1 deletion .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: "en"
reviews:
profile: "chill"
request_changes_workflow: false
request_changes_workflow: true
high_level_summary: true
collapse_walkthrough: false
auto_review:
Expand Down
8 changes: 8 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->end()
->end()
->arrayNode('openapi')
->addDefaultsIfNotSet()
->children()
->booleanNode('problem_details')
->defaultTrue()
->end()
->end()
->end()
->end();

return $treeBuilder;
Expand Down
50 changes: 49 additions & 1 deletion src/DependencyInjection/StixxOpenApiCommandExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,63 @@

namespace Stixx\OpenApiCommandBundle\DependencyInjection;

use Stixx\OpenApiCommandBundle\Model\ProblemDetails;
use Stixx\OpenApiCommandBundle\Model\ProblemDetailsInvalidRequestBody;
use Stixx\OpenApiCommandBundle\Model\Violation;
use Stixx\OpenApiCommandBundle\Responder\ResponderInterface;
use Stixx\OpenApiCommandBundle\Validator\ValidatorInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\Yaml\Yaml;

final class StixxOpenApiCommandExtension extends Extension
final class StixxOpenApiCommandExtension extends Extension implements PrependExtensionInterface
{
public function prepend(ContainerBuilder $container): void
{
$configs = $container->getExtensionConfig($this->getAlias());
/** @var array{openapi: array{problem_details: bool}} $config */
$config = $this->processConfiguration(new Configuration(), $configs);

if (!$config['openapi']['problem_details']) {
return;
}

$problemDetailsConfigPath = __DIR__.'/../Resources/specifications/nelmio_problem_details.yaml';
if (!file_exists($problemDetailsConfigPath)) {
return;
}

$problemDetailsConfig = Yaml::parseFile($problemDetailsConfigPath);

if (is_array($problemDetailsConfig) && isset($problemDetailsConfig['nelmio_api_doc']) && is_array($problemDetailsConfig['nelmio_api_doc'])) {
/** @var array<string, mixed> $nelmioConfig */
$nelmioConfig = $problemDetailsConfig['nelmio_api_doc'];
$container->prependExtensionConfig('nelmio_api_doc', $nelmioConfig);
}

$container->prependExtensionConfig('nelmio_api_doc', [
'models' => [
'names' => [
[
'alias' => 'ProblemDetails',
'type' => ProblemDetails::class,
],
[
'alias' => 'Violation',
'type' => Violation::class,
],
[
'alias' => 'ProblemDetailsInvalidRequestBody',
'type' => ProblemDetailsInvalidRequestBody::class,
],
],
],
]);
}

public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
Expand Down
44 changes: 44 additions & 0 deletions src/Model/ProblemDetails.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

/*
* This file is part of the StixxOpenApiCommandBundle package.
*
* (c) Stixx
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Stixx\OpenApiCommandBundle\Model;

use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Constraints as Assert;

#[OA\Schema(
title: 'ProblemDetails',
required: ['type', 'title', 'status'],
externalDocs: new OA\ExternalDocumentation(
description: 'Problem Details for HTTP APIs',
url: 'https://datatracker.ietf.org/doc/html/rfc7807'
)
)]
class ProblemDetails
{
public function __construct(
#[OA\Property(description: 'A URI reference [RFC3986] that identifies the problem type.', format: 'uri-reference', default: 'about:blank')]
#[Assert\NotBlank]
public string $type = 'about:blank',
#[OA\Property(description: 'A short, human-readable summary of the problem type.', type: 'string', default: 'An error occurred')]
public string $title = 'An error occurred',
#[OA\Property(description: 'The HTTP status code generated by the origin server for this occurrence of the problem.', type: 'integer', default: Response::HTTP_BAD_REQUEST)]
public int $status = 400,
#[OA\Property(description: 'A human-readable explanation specific to this occurrence of the problem.', type: 'string')]
public ?string $detail = null,
#[OA\Property(description: 'A URI reference that identifies the specific occurrence of the problem.', type: 'string', format: 'uri')]
public ?string $instance = null,
) {
}
}
41 changes: 41 additions & 0 deletions src/Model/ProblemDetailsInvalidRequestBody.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/*
* This file is part of the StixxOpenApiCommandBundle package.
*
* (c) Stixx
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Stixx\OpenApiCommandBundle\Model;

use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA;

#[OA\Schema(
title: 'InvalidRequestBody',
properties: [
new OA\Property(property: 'violations', type: 'array', items: new OA\Items(ref: new Model(type: Violation::class)), maxItems: 100),
],
example: [
'type' => 'about:blank',
'title' => 'The request body contains errors.',
'status' => 400,
'detail' => 'Validation failed.',
'violations' => [
[
'propertyPath' => 'foo',
'message' => 'This value should not be blank.',
'code' => 'c1ac8c7d-eab5-458f-a950-fcf121d23059',
'constraint' => 'NotBlank',
],
],
]
)]
final class ProblemDetailsInvalidRequestBody extends ProblemDetails
{
}
36 changes: 36 additions & 0 deletions src/Model/Violation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

/*
* This file is part of the StixxOpenApiCommandBundle package.
*
* (c) Stixx
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Stixx\OpenApiCommandBundle\Model;

use OpenApi\Attributes as OA;

#[OA\Schema(
required: [
'propertyPath',
'message',
'code',
'constraint',
]
)]
final class Violation
{
public function __construct(
public string $propertyPath,
public string $message,
public string $code,
public string $constraint,
public ?string $error,
) {
}
}
92 changes: 92 additions & 0 deletions src/Resources/specifications/nelmio_problem_details.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
nelmio_api_doc:
documentation:
components:
responses:
InvalidRequestProblemDetailsResponse:
description: RFC7807 Problem Details
content:
application/problem+json:
schema:
anyOf:
- $ref: '#/components/schemas/ProblemDetailsInvalidRequestBody'
- $ref: '#/components/schemas/ProblemDetails'
examples:
invalidJsonInRequestBody:
summary: Invalid JSON in Request Body
value:
type: 'about:blank'
title: 'The request body contains errors'
status: 400
detail: 'Validation failed.'
violations:
- propertyPath: 'foo'
message: 'This value should not be blank.'
code: c1ac8c7d-eab5-458f-a950-fcf121d23059
constraint: NotBlank
error: "NOT_EQUAL_ERROR"
invalidRequestBody:
summary: Invalid Request Body
value:
type: 'about:blank'
title: 'The request body contains errors'
status: 400
detail: 'Body does not match schema'
violations:
- propertyPath: 'body'
message: 'Body does not match schema for content-type "application/json" for Request [post /api/example]'
code: 'openapi_request_validation'
constraint: 'openapi_request_validation'
UnauthorizedProblemDetailsResponse:
description: RFC7807 Unauthorized Problem Details
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
example:
type: 'about:blank'
title: 'Unauthorized'
status: 401
detail: 'The authentication token is missing or invalid.'
ForbiddenProblemDetailsResponse:
description: RFC7807 Forbidden Problem Details
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
example:
type: 'about:blank'
title: 'Forbidden'
status: 403
detail: 'You are not allowed to perform this action.'
ResourceNotFoundProblemDetailsResponse:
description: RFC7807 Resource Not Found Problem Details
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
example:
type: 'about:blank'
title: 'Resource Not Found'
status: 404
detail: 'The requested resource was not found.'
ConflictProblemDetailsResponse:
description: RFC7807 Conflict Problem Details
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
example:
type: 'about:blank'
title: 'Conflict'
status: 409
detail: 'The resource already exists.'
DefaultProblemDetailsResponse:
description: RFC7807 Default Problem Details
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetails'
example:
type: 'about:blank'
title: 'An error occurred'
status: 500
2 changes: 2 additions & 0 deletions tests/Functional/App/Command/CreateBookCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
description: 'Book created',
content: new OA\JsonContent(ref: new Model(type: BookResource::class))
),
new OA\Response(ref: '#/components/responses/InvalidRequestProblemDetailsResponse', response: 400),
new OA\Response(ref: '#/components/responses/DefaultProblemDetailsResponse', response: 500),
]
)]
final class CreateBookCommand extends BookRequest
Expand Down
2 changes: 2 additions & 0 deletions tests/Functional/App/Command/DeleteBookCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

#[OA\Delete(path: '/api/books/{id}', summary: 'Delete a book')]
#[OA\Response(response: 204, description: 'Book deleted')]
#[OA\Response(ref: '#/components/responses/ResourceNotFoundProblemDetailsResponse', response: 404)]
#[OA\Response(ref: '#/components/responses/DefaultProblemDetailsResponse', response: 500)]
final class DeleteBookCommand
{
public function __construct(
Expand Down
3 changes: 3 additions & 0 deletions tests/Functional/App/Command/UpdateBookCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
description: 'Book updated',
content: new OA\JsonContent(ref: new Model(type: BookResource::class))
),
new OA\Response(ref: '#/components/responses/InvalidRequestProblemDetailsResponse', response: 400),
new OA\Response(ref: '#/components/responses/ResourceNotFoundProblemDetailsResponse', response: 404),
new OA\Response(ref: '#/components/responses/DefaultProblemDetailsResponse', response: 500),
]
)]
final class UpdateBookCommand extends BookRequest
Expand Down
Loading