Skip to content

Commit 0cfcaf9

Browse files
authored
Merge pull request #16 from stixx/nelmio-problem-details
Bundle configuration to automatically include ProblemDetails in the OpenAPI schema
2 parents 20ce59a + 26b664e commit 0cfcaf9

15 files changed

Lines changed: 749 additions & 4 deletions

.coderabbit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
language: "en"
22
reviews:
33
profile: "chill"
4-
request_changes_workflow: false
4+
request_changes_workflow: true
55
high_level_summary: true
66
collapse_walkthrough: false
77
auto_review:

src/DependencyInjection/Configuration.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ public function getConfigTreeBuilder(): TreeBuilder
3939
->end()
4040
->end()
4141
->end()
42+
->arrayNode('openapi')
43+
->addDefaultsIfNotSet()
44+
->children()
45+
->booleanNode('problem_details')
46+
->defaultTrue()
47+
->end()
48+
->end()
49+
->end()
4250
->end();
4351

4452
return $treeBuilder;

src/DependencyInjection/StixxOpenApiCommandExtension.php

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,63 @@
1313

1414
namespace Stixx\OpenApiCommandBundle\DependencyInjection;
1515

16+
use Stixx\OpenApiCommandBundle\Model\ProblemDetails;
17+
use Stixx\OpenApiCommandBundle\Model\ProblemDetailsInvalidRequestBody;
18+
use Stixx\OpenApiCommandBundle\Model\Violation;
1619
use Stixx\OpenApiCommandBundle\Responder\ResponderInterface;
1720
use Stixx\OpenApiCommandBundle\Validator\ValidatorInterface;
1821
use Symfony\Component\Config\FileLocator;
1922
use Symfony\Component\DependencyInjection\ContainerBuilder;
2023
use Symfony\Component\DependencyInjection\Extension\Extension;
24+
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
2125
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
26+
use Symfony\Component\Yaml\Yaml;
2227

23-
final class StixxOpenApiCommandExtension extends Extension
28+
final class StixxOpenApiCommandExtension extends Extension implements PrependExtensionInterface
2429
{
30+
public function prepend(ContainerBuilder $container): void
31+
{
32+
$configs = $container->getExtensionConfig($this->getAlias());
33+
/** @var array{openapi: array{problem_details: bool}} $config */
34+
$config = $this->processConfiguration(new Configuration(), $configs);
35+
36+
if (!$config['openapi']['problem_details']) {
37+
return;
38+
}
39+
40+
$problemDetailsConfigPath = __DIR__.'/../Resources/specifications/nelmio_problem_details.yaml';
41+
if (!file_exists($problemDetailsConfigPath)) {
42+
return;
43+
}
44+
45+
$problemDetailsConfig = Yaml::parseFile($problemDetailsConfigPath);
46+
47+
if (is_array($problemDetailsConfig) && isset($problemDetailsConfig['nelmio_api_doc']) && is_array($problemDetailsConfig['nelmio_api_doc'])) {
48+
/** @var array<string, mixed> $nelmioConfig */
49+
$nelmioConfig = $problemDetailsConfig['nelmio_api_doc'];
50+
$container->prependExtensionConfig('nelmio_api_doc', $nelmioConfig);
51+
}
52+
53+
$container->prependExtensionConfig('nelmio_api_doc', [
54+
'models' => [
55+
'names' => [
56+
[
57+
'alias' => 'ProblemDetails',
58+
'type' => ProblemDetails::class,
59+
],
60+
[
61+
'alias' => 'Violation',
62+
'type' => Violation::class,
63+
],
64+
[
65+
'alias' => 'ProblemDetailsInvalidRequestBody',
66+
'type' => ProblemDetailsInvalidRequestBody::class,
67+
],
68+
],
69+
],
70+
]);
71+
}
72+
2573
public function load(array $configs, ContainerBuilder $container): void
2674
{
2775
$configuration = new Configuration();

src/Model/ProblemDetails.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the StixxOpenApiCommandBundle package.
7+
*
8+
* (c) Stixx
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Stixx\OpenApiCommandBundle\Model;
15+
16+
use OpenApi\Attributes as OA;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\Validator\Constraints as Assert;
19+
20+
#[OA\Schema(
21+
title: 'ProblemDetails',
22+
required: ['type', 'title', 'status'],
23+
externalDocs: new OA\ExternalDocumentation(
24+
description: 'Problem Details for HTTP APIs',
25+
url: 'https://datatracker.ietf.org/doc/html/rfc7807'
26+
)
27+
)]
28+
class ProblemDetails
29+
{
30+
public function __construct(
31+
#[OA\Property(description: 'A URI reference [RFC3986] that identifies the problem type.', format: 'uri-reference', default: 'about:blank')]
32+
#[Assert\NotBlank]
33+
public string $type = 'about:blank',
34+
#[OA\Property(description: 'A short, human-readable summary of the problem type.', type: 'string', default: 'An error occurred')]
35+
public string $title = 'An error occurred',
36+
#[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)]
37+
public int $status = 400,
38+
#[OA\Property(description: 'A human-readable explanation specific to this occurrence of the problem.', type: 'string')]
39+
public ?string $detail = null,
40+
#[OA\Property(description: 'A URI reference that identifies the specific occurrence of the problem.', type: 'string', format: 'uri')]
41+
public ?string $instance = null,
42+
) {
43+
}
44+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the StixxOpenApiCommandBundle package.
7+
*
8+
* (c) Stixx
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Stixx\OpenApiCommandBundle\Model;
15+
16+
use Nelmio\ApiDocBundle\Attribute\Model;
17+
use OpenApi\Attributes as OA;
18+
19+
#[OA\Schema(
20+
title: 'InvalidRequestBody',
21+
properties: [
22+
new OA\Property(property: 'violations', type: 'array', items: new OA\Items(ref: new Model(type: Violation::class)), maxItems: 100),
23+
],
24+
example: [
25+
'type' => 'about:blank',
26+
'title' => 'The request body contains errors.',
27+
'status' => 400,
28+
'detail' => 'Validation failed.',
29+
'violations' => [
30+
[
31+
'propertyPath' => 'foo',
32+
'message' => 'This value should not be blank.',
33+
'code' => 'c1ac8c7d-eab5-458f-a950-fcf121d23059',
34+
'constraint' => 'NotBlank',
35+
],
36+
],
37+
]
38+
)]
39+
final class ProblemDetailsInvalidRequestBody extends ProblemDetails
40+
{
41+
}

src/Model/Violation.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the StixxOpenApiCommandBundle package.
7+
*
8+
* (c) Stixx
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Stixx\OpenApiCommandBundle\Model;
15+
16+
use OpenApi\Attributes as OA;
17+
18+
#[OA\Schema(
19+
required: [
20+
'propertyPath',
21+
'message',
22+
'code',
23+
'constraint',
24+
]
25+
)]
26+
final class Violation
27+
{
28+
public function __construct(
29+
public string $propertyPath,
30+
public string $message,
31+
public string $code,
32+
public string $constraint,
33+
public ?string $error,
34+
) {
35+
}
36+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
nelmio_api_doc:
2+
documentation:
3+
components:
4+
responses:
5+
InvalidRequestProblemDetailsResponse:
6+
description: RFC7807 Problem Details
7+
content:
8+
application/problem+json:
9+
schema:
10+
anyOf:
11+
- $ref: '#/components/schemas/ProblemDetailsInvalidRequestBody'
12+
- $ref: '#/components/schemas/ProblemDetails'
13+
examples:
14+
invalidJsonInRequestBody:
15+
summary: Invalid JSON in Request Body
16+
value:
17+
type: 'about:blank'
18+
title: 'The request body contains errors'
19+
status: 400
20+
detail: 'Validation failed.'
21+
violations:
22+
- propertyPath: 'foo'
23+
message: 'This value should not be blank.'
24+
code: c1ac8c7d-eab5-458f-a950-fcf121d23059
25+
constraint: NotBlank
26+
error: "NOT_EQUAL_ERROR"
27+
invalidRequestBody:
28+
summary: Invalid Request Body
29+
value:
30+
type: 'about:blank'
31+
title: 'The request body contains errors'
32+
status: 400
33+
detail: 'Body does not match schema'
34+
violations:
35+
- propertyPath: 'body'
36+
message: 'Body does not match schema for content-type "application/json" for Request [post /api/example]'
37+
code: 'openapi_request_validation'
38+
constraint: 'openapi_request_validation'
39+
UnauthorizedProblemDetailsResponse:
40+
description: RFC7807 Unauthorized Problem Details
41+
content:
42+
application/problem+json:
43+
schema:
44+
$ref: '#/components/schemas/ProblemDetails'
45+
example:
46+
type: 'about:blank'
47+
title: 'Unauthorized'
48+
status: 401
49+
detail: 'The authentication token is missing or invalid.'
50+
ForbiddenProblemDetailsResponse:
51+
description: RFC7807 Forbidden Problem Details
52+
content:
53+
application/problem+json:
54+
schema:
55+
$ref: '#/components/schemas/ProblemDetails'
56+
example:
57+
type: 'about:blank'
58+
title: 'Forbidden'
59+
status: 403
60+
detail: 'You are not allowed to perform this action.'
61+
ResourceNotFoundProblemDetailsResponse:
62+
description: RFC7807 Resource Not Found Problem Details
63+
content:
64+
application/problem+json:
65+
schema:
66+
$ref: '#/components/schemas/ProblemDetails'
67+
example:
68+
type: 'about:blank'
69+
title: 'Resource Not Found'
70+
status: 404
71+
detail: 'The requested resource was not found.'
72+
ConflictProblemDetailsResponse:
73+
description: RFC7807 Conflict Problem Details
74+
content:
75+
application/problem+json:
76+
schema:
77+
$ref: '#/components/schemas/ProblemDetails'
78+
example:
79+
type: 'about:blank'
80+
title: 'Conflict'
81+
status: 409
82+
detail: 'The resource already exists.'
83+
DefaultProblemDetailsResponse:
84+
description: RFC7807 Default Problem Details
85+
content:
86+
application/problem+json:
87+
schema:
88+
$ref: '#/components/schemas/ProblemDetails'
89+
example:
90+
type: 'about:blank'
91+
title: 'An error occurred'
92+
status: 500

tests/Functional/App/Command/CreateBookCommand.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
description: 'Book created',
3232
content: new OA\JsonContent(ref: new Model(type: BookResource::class))
3333
),
34+
new OA\Response(ref: '#/components/responses/InvalidRequestProblemDetailsResponse', response: 400),
35+
new OA\Response(ref: '#/components/responses/DefaultProblemDetailsResponse', response: 500),
3436
]
3537
)]
3638
final class CreateBookCommand extends BookRequest

tests/Functional/App/Command/DeleteBookCommand.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
#[OA\Delete(path: '/api/books/{id}', summary: 'Delete a book')]
2020
#[OA\Response(response: 204, description: 'Book deleted')]
21+
#[OA\Response(ref: '#/components/responses/ResourceNotFoundProblemDetailsResponse', response: 404)]
22+
#[OA\Response(ref: '#/components/responses/DefaultProblemDetailsResponse', response: 500)]
2123
final class DeleteBookCommand
2224
{
2325
public function __construct(

tests/Functional/App/Command/UpdateBookCommand.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
description: 'Book updated',
3232
content: new OA\JsonContent(ref: new Model(type: BookResource::class))
3333
),
34+
new OA\Response(ref: '#/components/responses/InvalidRequestProblemDetailsResponse', response: 400),
35+
new OA\Response(ref: '#/components/responses/ResourceNotFoundProblemDetailsResponse', response: 404),
36+
new OA\Response(ref: '#/components/responses/DefaultProblemDetailsResponse', response: 500),
3437
]
3538
)]
3639
final class UpdateBookCommand extends BookRequest

0 commit comments

Comments
 (0)