Skip to content

Commit aff44c9

Browse files
antograssiotdunglas
authored andcommitted
Add support for OpenAPI 3 (#2302)
* Create an abstract documentation normalizer Add a OpenAPI 3.0 compatible normalizer Update the export command Update AbstractDocumentation namespace Increase new normalizer priority to make it the default in swaggerUI Rename namespace from OpenApi to OpenAPI Make OpenAPI 3 the default and alow to fallback to 2 Adding openapi_context attribute and cleaning * Remove useless swagger.yaml * Rename spec-version in spec_version for consistency with api_gateway * Use a new openapi key in filter description * Fix tests
1 parent 9a5b6f0 commit aff44c9

File tree

22 files changed

+3601
-383
lines changed

22 files changed

+3601
-383
lines changed

.travis.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,7 @@ script:
5050
else
5151
vendor/bin/behat --suite=default --format=progress;
5252
fi
53-
- tests/Fixtures/app/console api:swagger:export > swagger.json && npx swagger-cli validate swagger.json && rm swagger.json
54-
- tests/Fixtures/app/console api:swagger:export --yaml > swagger.yaml && npx swagger-cli validate swagger.yaml && rm swagger.yaml
53+
- tests/Fixtures/app/console api:swagger:export --spec-version 2 > swagger.json && npx swagger-cli validate swagger.json && rm swagger.json
54+
- tests/Fixtures/app/console api:swagger:export --spec-version 3 --yaml > swagger.yaml && npx swagger-cli validate swagger.yaml && rm swagger.yaml
55+
- tests/Fixtures/app/console api:openapi:export > swagger.json && npx swagger-cli validate swagger.json && rm swagger.json
56+
- tests/Fixtures/app/console api:openapi:export --yaml > swagger.yaml && npx swagger-cli validate swagger.yaml && rm swagger.yaml

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ API Platform Core is an easy to use and powerful system to create [hypermedia-dr
44
It is a component of the [API Platform framework](https://api-platform.com) and it can be integrated
55
with [the Symfony framework](https://symfony.com) using the bundle distributed with the library.
66

7-
It natively supports popular open formats including [JSON for Linked Data (JSON-LD)](http://json-ld.org), [Hydra Core Vocabulary](http://www.hydra-cg.com), [Swagger (OpenAPI)](http://swagger.io), [HAL](http://stateless.co/hal_specification.html) and [HTTP Problem](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-03).
7+
It natively supports popular open formats including [JSON for Linked Data (JSON-LD)](http://json-ld.org), [Hydra Core Vocabulary](http://www.hydra-cg.com), [OpenAPI v2 (formerly Swagger) and v3](https://www.openapis.org), [HAL](http://stateless.co/hal_specification.html) and [HTTP Problem](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-03).
88

99
Build a working and fully-featured CRUD API in minutes. Leverage the awesome features of the tool to develop complex and
1010
high performance API-first projects. Extend or override everything you want.

features/bootstrap/SwaggerContext.php

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ public function assertTheSwaggerClassExist(string $className)
4949
}
5050
}
5151

52+
/**
53+
* @Then the OpenAPI class :class exists
54+
*/
55+
public function assertTheOpenApiClassExist(string $className)
56+
{
57+
try {
58+
$this->getClassInfo($className, 3);
59+
} catch (\InvalidArgumentException $e) {
60+
throw new ExpectationFailedException(sprintf('The class "%s" doesn\'t exist.', $className), null, $e);
61+
}
62+
}
63+
5264
/**
5365
* @Then the Swagger class :class doesn't exist
5466
*/
@@ -63,8 +75,23 @@ public function assertTheSwaggerClassNotExist(string $className)
6375
throw new ExpectationFailedException(sprintf('The class "%s" exists.', $className));
6476
}
6577

78+
/**
79+
* @Then the OpenAPI class :class doesn't exist
80+
*/
81+
public function assertTheOPenAPIClassNotExist(string $className)
82+
{
83+
try {
84+
$this->getClassInfo($className, 3);
85+
} catch (\InvalidArgumentException $e) {
86+
return;
87+
}
88+
89+
throw new ExpectationFailedException(sprintf('The class "%s" exists.', $className));
90+
}
91+
6692
/**
6793
* @Then the Swagger path :arg1 exists
94+
* @Then the OpenAPI path :arg1 exists
6895
*/
6996
public function assertThePathExist(string $path)
7097
{
@@ -76,7 +103,7 @@ public function assertThePathExist(string $path)
76103
/**
77104
* @Then :prop property exists for the Swagger class :class
78105
*/
79-
public function assertPropertyExist(string $propertyName, string $className)
106+
public function assertPropertyExistForTheSwaggerClass(string $propertyName, string $className)
80107
{
81108
try {
82109
$this->getPropertyInfo($propertyName, $className);
@@ -85,24 +112,46 @@ public function assertPropertyExist(string $propertyName, string $className)
85112
}
86113
}
87114

115+
/**
116+
* @Then :prop property exists for the OpenAPI class :class
117+
*/
118+
public function assertPropertyExistForTheOpenApiClass(string $propertyName, string $className)
119+
{
120+
try {
121+
$this->getPropertyInfo($propertyName, $className, 3);
122+
} catch (\InvalidArgumentException $e) {
123+
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" exists.', $propertyName, $className), null, $e);
124+
}
125+
}
126+
88127
/**
89128
* @Then :prop property is required for Swagger class :class
90129
*/
91-
public function assertPropertyIsRequired(string $propertyName, string $className)
130+
public function assertPropertyIsRequiredForSwagger(string $propertyName, string $className)
92131
{
93132
if (!\in_array($propertyName, $this->getClassInfo($className)->required, true)) {
94133
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className));
95134
}
96135
}
97136

137+
/**
138+
* @Then :prop property is required for OpenAPi class :class
139+
*/
140+
public function assertPropertyIsRequiredForOpenAPi(string $propertyName, string $className)
141+
{
142+
if (!\in_array($propertyName, $this->getClassInfo($className, 3)->required, true)) {
143+
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className));
144+
}
145+
}
146+
98147
/**
99148
* Gets information about a property.
100149
*
101150
* @throws \InvalidArgumentException
102151
*/
103-
private function getPropertyInfo(string $propertyName, string $className): \stdClass
152+
private function getPropertyInfo(string $propertyName, string $className, int $specVersion = 2): \stdClass
104153
{
105-
foreach ($this->getProperties($className) as $classPropertyName => $property) {
154+
foreach ($this->getProperties($className, $specVersion) as $classPropertyName => $property) {
106155
if ($classPropertyName === $propertyName) {
107156
return $property;
108157
}
@@ -114,19 +163,20 @@ private function getPropertyInfo(string $propertyName, string $className): \stdC
114163
/**
115164
* Gets all operations of a given class.
116165
*/
117-
private function getProperties(string $className): \stdClass
166+
private function getProperties(string $className, int $specVersion = 2): \stdClass
118167
{
119-
return $this->getClassInfo($className)->{'properties'} ?? new \stdClass();
168+
return $this->getClassInfo($className, $specVersion)->{'properties'} ?? new \stdClass();
120169
}
121170

122171
/**
123172
* Gets information about a class.
124173
*
125174
* @throws \InvalidArgumentException
126175
*/
127-
private function getClassInfo(string $className): \stdClass
176+
private function getClassInfo(string $className, int $specVersion = 2): \stdClass
128177
{
129-
foreach ($this->getLastJsonResponse()->{'definitions'} as $classTitle => $classData) {
178+
$nodes = 2 === $specVersion ? $this->getLastJsonResponse()->{'definitions'} : $this->getLastJsonResponse()->{'components'}->{'schemas'};
179+
foreach ($nodes as $classTitle => $classData) {
130180
if ($classTitle === $className) {
131181
return $classData;
132182
}

features/openapi/docs.feature

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
Feature: Documentation support
2+
In order to build an auto-discoverable API
3+
As a client software developer
4+
I need to know OpenAPI specifications of objects I send and receive
5+
6+
@createSchema
7+
Scenario: Retrieve the OpenAPI documentation
8+
Given I send a "GET" request to "/docs.json"
9+
Then the response status code should be 200
10+
And the response should be in JSON
11+
And the header "Content-Type" should be equal to "application/json; charset=utf-8"
12+
# Context
13+
And the JSON node "openapi" should be equal to "3.0.2"
14+
# Root properties
15+
And the JSON node "info.title" should be equal to "My Dummy API"
16+
And the JSON node "info.description" should be equal to "This is a test API."
17+
# Supported classes
18+
And the OpenAPI class "AbstractDummy" exists
19+
And the OpenAPI class "CircularReference" exists
20+
And the OpenAPI class "CircularReference-circular" exists
21+
And the OpenAPI class "CompositeItem" exists
22+
And the OpenAPI class "CompositeLabel" exists
23+
And the OpenAPI class "ConcreteDummy" exists
24+
And the OpenAPI class "CustomIdentifierDummy" exists
25+
And the OpenAPI class "CustomNormalizedDummy-input" exists
26+
And the OpenAPI class "CustomNormalizedDummy-output" exists
27+
And the OpenAPI class "CustomWritableIdentifierDummy" exists
28+
And the OpenAPI class "Dummy" exists
29+
And the OpenAPI class "RelatedDummy" exists
30+
And the OpenAPI class "DummyTableInheritance" exists
31+
And the OpenAPI class "DummyTableInheritanceChild" exists
32+
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_get" exists
33+
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_put" exists
34+
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_read" exists
35+
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_write" exists
36+
And the OpenAPI class "RelatedDummy" exists
37+
And the OpenAPI class "NoCollectionDummy" exists
38+
And the OpenAPI class "RelatedToDummyFriend" exists
39+
And the OpenAPI class "RelatedToDummyFriend-fakemanytomany" exists
40+
And the OpenAPI class "DummyFriend" exists
41+
And the OpenAPI class "RelationEmbedder-barcelona" exists
42+
And the OpenAPI class "RelationEmbedder-chicago" exists
43+
And the OpenAPI class "User-user_user-read" exists
44+
And the OpenAPI class "User-user_user-write" exists
45+
And the OpenAPI class "UuidIdentifierDummy" exists
46+
And the OpenAPI class "ThirdLevel" exists
47+
And the OpenAPI class "ParentDummy" doesn't exist
48+
And the OpenAPI class "UnknownDummy" doesn't exist
49+
And the OpenAPI path "/relation_embedders/{id}/custom" exists
50+
And the OpenAPI path "/override/swagger" exists
51+
And the OpenAPI path "/api/custom-call/{id}" exists
52+
And the JSON node "paths./api/custom-call/{id}.get" should exist
53+
And the JSON node "paths./api/custom-call/{id}.put" should exist
54+
# Properties
55+
And "id" property exists for the OpenAPI class "Dummy"
56+
And "name" property is required for OpenAPI class "Dummy"
57+
# Filters
58+
And the JSON node "paths./dummies.get.parameters[0].name" should be equal to "dummyBoolean"
59+
And the JSON node "paths./dummies.get.parameters[0].in" should be equal to "query"
60+
And the JSON node "paths./dummies.get.parameters[0].required" should be false
61+
And the JSON node "paths./dummies.get.parameters[0].schema.type" should be equal to "boolean"
62+
63+
# Subcollection - check filter on subResource
64+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].name" should be equal to "id"
65+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].in" should be equal to "path"
66+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].required" should be true
67+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].schema.type" should be equal to "string"
68+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].name" should be equal to "name"
69+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].in" should be equal to "query"
70+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].required" should be false
71+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].schema.type" should be equal to "string"
72+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].name" should be equal to "description"
73+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].in" should be equal to "query"
74+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].required" should be false
75+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].schema.type" should be equal to "string"
76+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters" should have 3 element
77+
78+
# Subcollection - check schema
79+
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.responses.200.content.application/ld+json.schema.items.$ref" should be equal to "#/components/schemas/RelatedToDummyFriend-fakemanytomany"
80+
81+
# Deprecations
82+
And the JSON node "paths./dummies.get.deprecated" should not exist
83+
And the JSON node "paths./deprecated_resources.get.deprecated" should be true
84+
And the JSON node "paths./deprecated_resources.post.deprecated" should be true
85+
And the JSON node "paths./deprecated_resources/{id}.get.deprecated" should be true
86+
And the JSON node "paths./deprecated_resources/{id}.delete.deprecated" should be true
87+
And the JSON node "paths./deprecated_resources/{id}.put.deprecated" should be true
88+
And the JSON node "paths./deprecated_resources/{id}.patch.deprecated" should be true
89+
90+
Scenario: OpenAPI UI is enabled for docs endpoint
91+
Given I add "Accept" header equal to "text/html"
92+
And I send a "GET" request to "/docs"
93+
Then the response status code should be 200
94+
And I should see text matching "My Dummy API"
95+
And I should see text matching "openapi"
96+
And I should see text matching "3.0.2"
97+
98+
Scenario: OpenAPI UI is enabled for an arbitrary endpoint
99+
Given I add "Accept" header equal to "text/html"
100+
And I send a "GET" request to "/dummies"
101+
Then the response status code should be 200
102+
And I should see text matching "openapi"
103+
And I should see text matching "3.0.2"

features/swagger/docs.feature

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Feature: Documentation support
55

66
@createSchema
77
Scenario: Retrieve the Swagger/OpenAPI documentation
8-
Given I send a "GET" request to "/docs.json"
8+
Given I send a "GET" request to "/docs.json?spec_version=2"
99
Then the response status code should be 200
1010
And the response should be in JSON
1111
And the header "Content-Type" should be equal to "application/json; charset=utf-8"
@@ -89,12 +89,16 @@ Feature: Documentation support
8989

9090
Scenario: Swagger UI is enabled for docs endpoint
9191
Given I add "Accept" header equal to "text/html"
92-
And I send a "GET" request to "/docs"
92+
And I send a "GET" request to "/docs?spec_version=2"
9393
Then the response status code should be 200
9494
And I should see text matching "My Dummy API"
95+
And I should see text matching "swagger"
96+
And I should see text matching "2.0"
9597

9698
Scenario: Swagger UI is enabled for an arbitrary endpoint
9799
Given I add "Accept" header equal to "text/html"
98-
And I send a "GET" request to "/dummies"
100+
And I send a "GET" request to "/dummies?spec_version=2"
99101
Then the response status code should be 200
100102
And I should see text matching "My Dummy API"
103+
And I should see text matching "swagger"
104+
And I should see text matching "2.0"

src/Annotation/ApiProperty.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
* @Attribute("deprecationReason", type="string"),
2727
* @Attribute("fetchable", type="bool"),
2828
* @Attribute("fetchEager", type="bool"),
29+
* @Attribute("openapiContext", type="array"),
2930
* @Attribute("jsonldContext", type="array"),
3031
* @Attribute("swaggerContext", type="array")
3132
* )
@@ -109,6 +110,13 @@ final class ApiProperty
109110
*/
110111
private $swaggerContext;
111112

113+
/**
114+
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
115+
*
116+
* @var array
117+
*/
118+
private $openapiContext;
119+
112120
/**
113121
* @throws InvalidArgumentException
114122
*/

src/Annotation/ApiResource.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
* @Attribute("maximumItemsPerPage", type="int"),
4444
* @Attribute("mercure", type="mixed"),
4545
* @Attribute("normalizationContext", type="array"),
46+
* @Attribute("openapiContext", type="array"),
4647
* @Attribute("order", type="array"),
4748
* @Attribute("outputClass", type="mixed"),
4849
* @Attribute("paginationClientEnabled", type="bool"),
@@ -288,6 +289,13 @@ final class ApiResource
288289
*/
289290
private $outputClass;
290291

292+
/**
293+
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
294+
*
295+
* @var array
296+
*/
297+
private $openapiContext;
298+
291299
/**
292300
* @throws InvalidArgumentException
293301
*/

src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ private function getContext(Request $request, Documentation $documentation): arr
117117

118118
$swaggerData = [
119119
'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']),
120-
'spec' => $this->normalizer->normalize($documentation, 'json', ['base_url' => $request->getBaseUrl()]),
120+
'spec' => $this->normalizer->normalize($documentation, 'json', ['base_url' => $request->getBaseUrl(), 'spec_version' => $request->query->getInt('spec_version', 3)]),
121121
];
122122

123123
$swaggerData['oauth'] = [

0 commit comments

Comments
 (0)