Skip to content

Add support for OpenAPI 3 #2302

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

Merged
merged 5 commits into from
Dec 20, 2018
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
6 changes: 4 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,7 @@ script:
else
vendor/bin/behat --suite=default --format=progress;
fi
- tests/Fixtures/app/console api:swagger:export > swagger.json && npx swagger-cli validate swagger.json && rm swagger.json
- tests/Fixtures/app/console api:swagger:export --yaml > swagger.yaml && npx swagger-cli validate swagger.yaml && rm swagger.yaml
- tests/Fixtures/app/console api:swagger:export --spec-version 2 > swagger.json && npx swagger-cli validate swagger.json && rm swagger.json
- tests/Fixtures/app/console api:swagger:export --spec-version 3 --yaml > swagger.yaml && npx swagger-cli validate swagger.yaml && rm swagger.yaml
- tests/Fixtures/app/console api:openapi:export > swagger.json && npx swagger-cli validate swagger.json && rm swagger.json
- tests/Fixtures/app/console api:openapi:export --yaml > swagger.yaml && npx swagger-cli validate swagger.yaml && rm swagger.yaml
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ API Platform Core is an easy to use and powerful system to create [hypermedia-dr
It is a component of the [API Platform framework](https://api-platform.com) and it can be integrated
with [the Symfony framework](https://symfony.com) using the bundle distributed with the library.

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).
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).

Build a working and fully-featured CRUD API in minutes. Leverage the awesome features of the tool to develop complex and
high performance API-first projects. Extend or override everything you want.
Expand Down
66 changes: 58 additions & 8 deletions features/bootstrap/SwaggerContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ public function assertTheSwaggerClassExist(string $className)
}
}

/**
* @Then the OpenAPI class :class exists
*/
public function assertTheOpenApiClassExist(string $className)
{
try {
$this->getClassInfo($className, 3);
} catch (\InvalidArgumentException $e) {
throw new ExpectationFailedException(sprintf('The class "%s" doesn\'t exist.', $className), null, $e);
}
}

/**
* @Then the Swagger class :class doesn't exist
*/
Expand All @@ -63,8 +75,23 @@ public function assertTheSwaggerClassNotExist(string $className)
throw new ExpectationFailedException(sprintf('The class "%s" exists.', $className));
}

/**
* @Then the OpenAPI class :class doesn't exist
*/
public function assertTheOPenAPIClassNotExist(string $className)
{
try {
$this->getClassInfo($className, 3);
} catch (\InvalidArgumentException $e) {
return;
}

throw new ExpectationFailedException(sprintf('The class "%s" exists.', $className));
}

/**
* @Then the Swagger path :arg1 exists
* @Then the OpenAPI path :arg1 exists
*/
public function assertThePathExist(string $path)
{
Expand All @@ -76,7 +103,7 @@ public function assertThePathExist(string $path)
/**
* @Then :prop property exists for the Swagger class :class
*/
public function assertPropertyExist(string $propertyName, string $className)
public function assertPropertyExistForTheSwaggerClass(string $propertyName, string $className)
{
try {
$this->getPropertyInfo($propertyName, $className);
Expand All @@ -85,24 +112,46 @@ public function assertPropertyExist(string $propertyName, string $className)
}
}

/**
* @Then :prop property exists for the OpenAPI class :class
*/
public function assertPropertyExistForTheOpenApiClass(string $propertyName, string $className)
{
try {
$this->getPropertyInfo($propertyName, $className, 3);
} catch (\InvalidArgumentException $e) {
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" exists.', $propertyName, $className), null, $e);
}
}

/**
* @Then :prop property is required for Swagger class :class
*/
public function assertPropertyIsRequired(string $propertyName, string $className)
public function assertPropertyIsRequiredForSwagger(string $propertyName, string $className)
{
if (!\in_array($propertyName, $this->getClassInfo($className)->required, true)) {
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className));
}
}

/**
* @Then :prop property is required for OpenAPi class :class
*/
public function assertPropertyIsRequiredForOpenAPi(string $propertyName, string $className)
{
if (!\in_array($propertyName, $this->getClassInfo($className, 3)->required, true)) {
throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className));
}
}

/**
* Gets information about a property.
*
* @throws \InvalidArgumentException
*/
private function getPropertyInfo(string $propertyName, string $className): \stdClass
private function getPropertyInfo(string $propertyName, string $className, int $specVersion = 2): \stdClass
{
foreach ($this->getProperties($className) as $classPropertyName => $property) {
foreach ($this->getProperties($className, $specVersion) as $classPropertyName => $property) {
if ($classPropertyName === $propertyName) {
return $property;
}
Expand All @@ -114,19 +163,20 @@ private function getPropertyInfo(string $propertyName, string $className): \stdC
/**
* Gets all operations of a given class.
*/
private function getProperties(string $className): \stdClass
private function getProperties(string $className, int $specVersion = 2): \stdClass
{
return $this->getClassInfo($className)->{'properties'} ?? new \stdClass();
return $this->getClassInfo($className, $specVersion)->{'properties'} ?? new \stdClass();
}

/**
* Gets information about a class.
*
* @throws \InvalidArgumentException
*/
private function getClassInfo(string $className): \stdClass
private function getClassInfo(string $className, int $specVersion = 2): \stdClass
{
foreach ($this->getLastJsonResponse()->{'definitions'} as $classTitle => $classData) {
$nodes = 2 === $specVersion ? $this->getLastJsonResponse()->{'definitions'} : $this->getLastJsonResponse()->{'components'}->{'schemas'};
foreach ($nodes as $classTitle => $classData) {
if ($classTitle === $className) {
return $classData;
}
Expand Down
103 changes: 103 additions & 0 deletions features/openapi/docs.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
Feature: Documentation support
In order to build an auto-discoverable API
As a client software developer
I need to know OpenAPI specifications of objects I send and receive

@createSchema
Scenario: Retrieve the OpenAPI documentation
Given I send a "GET" request to "/docs.json"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json; charset=utf-8"
# Context
And the JSON node "openapi" should be equal to "3.0.2"
# Root properties
And the JSON node "info.title" should be equal to "My Dummy API"
And the JSON node "info.description" should be equal to "This is a test API."
# Supported classes
And the OpenAPI class "AbstractDummy" exists
And the OpenAPI class "CircularReference" exists
And the OpenAPI class "CircularReference-circular" exists
And the OpenAPI class "CompositeItem" exists
And the OpenAPI class "CompositeLabel" exists
And the OpenAPI class "ConcreteDummy" exists
And the OpenAPI class "CustomIdentifierDummy" exists
And the OpenAPI class "CustomNormalizedDummy-input" exists
And the OpenAPI class "CustomNormalizedDummy-output" exists
And the OpenAPI class "CustomWritableIdentifierDummy" exists
And the OpenAPI class "Dummy" exists
And the OpenAPI class "RelatedDummy" exists
And the OpenAPI class "DummyTableInheritance" exists
And the OpenAPI class "DummyTableInheritanceChild" exists
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_get" exists
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_put" exists
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_read" exists
And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_write" exists
And the OpenAPI class "RelatedDummy" exists
And the OpenAPI class "NoCollectionDummy" exists
And the OpenAPI class "RelatedToDummyFriend" exists
And the OpenAPI class "RelatedToDummyFriend-fakemanytomany" exists
And the OpenAPI class "DummyFriend" exists
And the OpenAPI class "RelationEmbedder-barcelona" exists
And the OpenAPI class "RelationEmbedder-chicago" exists
And the OpenAPI class "User-user_user-read" exists
And the OpenAPI class "User-user_user-write" exists
And the OpenAPI class "UuidIdentifierDummy" exists
And the OpenAPI class "ThirdLevel" exists
And the OpenAPI class "ParentDummy" doesn't exist
And the OpenAPI class "UnknownDummy" doesn't exist
And the OpenAPI path "/relation_embedders/{id}/custom" exists
And the OpenAPI path "/override/swagger" exists
And the OpenAPI path "/api/custom-call/{id}" exists
And the JSON node "paths./api/custom-call/{id}.get" should exist
And the JSON node "paths./api/custom-call/{id}.put" should exist
# Properties
And "id" property exists for the OpenAPI class "Dummy"
And "name" property is required for OpenAPI class "Dummy"
# Filters
And the JSON node "paths./dummies.get.parameters[0].name" should be equal to "dummyBoolean"
And the JSON node "paths./dummies.get.parameters[0].in" should be equal to "query"
And the JSON node "paths./dummies.get.parameters[0].required" should be false
And the JSON node "paths./dummies.get.parameters[0].schema.type" should be equal to "boolean"

# Subcollection - check filter on subResource
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].name" should be equal to "id"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].in" should be equal to "path"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].required" should be true
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].schema.type" should be equal to "string"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].name" should be equal to "name"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].in" should be equal to "query"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].required" should be false
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].schema.type" should be equal to "string"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].name" should be equal to "description"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].in" should be equal to "query"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].required" should be false
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].schema.type" should be equal to "string"
And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters" should have 3 element

# Subcollection - check schema
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"

# Deprecations
And the JSON node "paths./dummies.get.deprecated" should not exist
And the JSON node "paths./deprecated_resources.get.deprecated" should be true
And the JSON node "paths./deprecated_resources.post.deprecated" should be true
And the JSON node "paths./deprecated_resources/{id}.get.deprecated" should be true
And the JSON node "paths./deprecated_resources/{id}.delete.deprecated" should be true
And the JSON node "paths./deprecated_resources/{id}.put.deprecated" should be true
And the JSON node "paths./deprecated_resources/{id}.patch.deprecated" should be true

Scenario: OpenAPI UI is enabled for docs endpoint
Given I add "Accept" header equal to "text/html"
And I send a "GET" request to "/docs"
Then the response status code should be 200
And I should see text matching "My Dummy API"
And I should see text matching "openapi"
And I should see text matching "3.0.2"

Scenario: OpenAPI UI is enabled for an arbitrary endpoint
Given I add "Accept" header equal to "text/html"
And I send a "GET" request to "/dummies"
Then the response status code should be 200
And I should see text matching "openapi"
And I should see text matching "3.0.2"
10 changes: 7 additions & 3 deletions features/swagger/docs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Feature: Documentation support

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

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

Scenario: Swagger UI is enabled for an arbitrary endpoint
Given I add "Accept" header equal to "text/html"
And I send a "GET" request to "/dummies"
And I send a "GET" request to "/dummies?spec_version=2"
Then the response status code should be 200
And I should see text matching "My Dummy API"
And I should see text matching "swagger"
And I should see text matching "2.0"
8 changes: 8 additions & 0 deletions src/Annotation/ApiProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* @Attribute("deprecationReason", type="string"),
* @Attribute("fetchable", type="bool"),
* @Attribute("fetchEager", type="bool"),
* @Attribute("openapiContext", type="array"),
* @Attribute("jsonldContext", type="array"),
* @Attribute("swaggerContext", type="array")
* )
Expand Down Expand Up @@ -109,6 +110,13 @@ final class ApiProperty
*/
private $swaggerContext;

/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
* @var array
*/
private $openapiContext;

/**
* @throws InvalidArgumentException
*/
Expand Down
8 changes: 8 additions & 0 deletions src/Annotation/ApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
* @Attribute("maximumItemsPerPage", type="int"),
* @Attribute("mercure", type="mixed"),
* @Attribute("normalizationContext", type="array"),
* @Attribute("openapiContext", type="array"),
* @Attribute("order", type="array"),
* @Attribute("outputClass", type="mixed"),
* @Attribute("paginationClientEnabled", type="bool"),
Expand Down Expand Up @@ -288,6 +289,13 @@ final class ApiResource
*/
private $outputClass;

/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
* @var array
*/
private $openapiContext;

/**
* @throws InvalidArgumentException
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ private function getContext(Request $request, Documentation $documentation): arr

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

$swaggerData['oauth'] = [
Expand Down
Loading