Skip to content

Commit dc227ec

Browse files
authored
Merge pull request #591 from Simperfit/feature/swagger-v2
feat: swagger v2
2 parents 2d972c0 + 420deeb commit dc227ec

File tree

21 files changed

+796
-19
lines changed

21 files changed

+796
-19
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ before_install:
2222
- if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then phpenv config-rm xdebug.ini; fi;
2323
- phpunit --self-update
2424
- if [ "$SYMFONY_VERSION" != "" ]; then composer require "symfony/symfony:${SYMFONY_VERSION}" --no-update; fi;
25+
- npm install -g swagger-cli
2526

2627
install:
2728
- composer update --no-interaction --prefer-dist
2829

2930
script:
3031
- vendor/bin/phpunit
3132
- vendor/bin/behat
33+
- tests/Fixtures/app/console api:swagger:export > swagger.json && swagger validate swagger.json && rm swagger.json
34+

behat.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ default:
44
contexts:
55
- 'FeatureContext': { doctrine: '@doctrine' }
66
- 'HydraContext'
7+
- 'SwaggerContext'
78
- 'Behat\MinkExtension\Context\MinkContext'
89
- 'Sanpi\Behatch\Context\RestContext'
910
- 'Sanpi\Behatch\Context\JsonContext'
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Behat\Behat\Context\Context;
13+
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
14+
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
15+
use Sanpi\Behatch\Context\RestContext;
16+
use Symfony\Component\PropertyAccess\PropertyAccess;
17+
18+
final class SwaggerContext implements Context
19+
{
20+
private $propertyAccessor;
21+
22+
public function __construct()
23+
{
24+
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
25+
}
26+
27+
/**
28+
* Gives access to the Behatch context.
29+
*
30+
* @param BeforeScenarioScope $scope
31+
*
32+
* @BeforeScenario
33+
*/
34+
public function gatherContexts(BeforeScenarioScope $scope)
35+
{
36+
/** @var InitializedContextEnvironment $environment */
37+
$environment = $scope->getEnvironment();
38+
$this->restContext = $environment->getContext(RestContext::class);
39+
}
40+
41+
/**
42+
* @Then the Swagger class ":class" exist
43+
*/
44+
public function assertTheSwaggerClassExist($className)
45+
{
46+
$this->getClassInfos($className);
47+
}
48+
49+
/**
50+
* @Then the Swagger class ":class" not exist
51+
*/
52+
public function assertTheSwaggerClassNotExist($className)
53+
{
54+
try {
55+
$this->getClassInfos($className);
56+
57+
throw new \PHPUnit_Framework_ExpectationFailedException(sprintf('The class "%s" exist.', $className));
58+
} catch (\Exception $exception) {
59+
// an exception must be catched
60+
}
61+
}
62+
63+
/**
64+
* @Then the value of the node ":node" of the Swagger class ":class" is ":value"
65+
*/
66+
public function assertNodeValueIs($nodeName, $className, $value)
67+
{
68+
$classInfos = $this->getClassInfos($className);
69+
\PHPUnit_Framework_Assert::assertEquals($this->propertyAccessor->getValue($classInfos, $nodeName), $value);
70+
}
71+
72+
/**
73+
* @Then the value of the node ":node" of the property ":prop" of the Swagger class ":class" is ":value"
74+
*/
75+
public function assertPropertyNodeValueIs($nodeName, $propertyName, $className, $value)
76+
{
77+
$property = $this->getProperty($propertyName, $className);
78+
if (empty($property)) {
79+
throw new \PHPUnit_Framework_ExpectationFailedException(
80+
sprintf('The property "%s" for the class "%s" exist.', $propertyName, $className)
81+
);
82+
}
83+
\PHPUnit_Framework_Assert::assertEquals($this->propertyAccessor->getValue($property, $nodeName), $value);
84+
}
85+
86+
/**
87+
* @Then the value of the node ":node" of the operation ":operation" of the Swagger class ":class" is ":value"
88+
*/
89+
public function assertOperationNodeValueIs($nodeName, $operationMethod, $className, $value)
90+
{
91+
$property = $this->getOperation($operationMethod, $className);
92+
93+
\PHPUnit_Framework_Assert::assertEquals($this->propertyAccessor->getValue($property, $nodeName), $value);
94+
}
95+
96+
/**
97+
* @Then :nb operations are available for Swagger class ":class"
98+
*/
99+
public function assertNbOperationsExist($nb, $className)
100+
{
101+
$operations = $this->getOperations($className);
102+
103+
\PHPUnit_Framework_Assert::assertEquals($nb, count($operations));
104+
}
105+
106+
/**
107+
* @Then :nb properties are available for Swagger class ":class"
108+
*/
109+
public function assertNbPropertiesExist($nb, $className)
110+
{
111+
$properties = $this->getProperties($className);
112+
113+
\PHPUnit_Framework_Assert::assertEquals($nb, count($properties));
114+
}
115+
116+
/**
117+
* @Then ":prop" property doesn't exist for the Swagger class ":class"
118+
*/
119+
public function assertPropertyNotExist($propertyName, $className)
120+
{
121+
$property = $this->getProperty($propertyName, $className);
122+
if (empty($property)) {
123+
throw new \PHPUnit_Framework_ExpectationFailedException(
124+
sprintf('The property "%s" for the class "%s" exist.', $propertyName, $className)
125+
);
126+
}
127+
}
128+
129+
/**
130+
* @Then ":prop" property is required for Swagger class ":class"
131+
*/
132+
public function assertPropertyIsRequired(string $propertyName, string $className)
133+
{
134+
$classInfo = $this->getClassInfos($className);
135+
if (!in_array($propertyName, $classInfo->{'required'})) {
136+
throw new \Exception(sprintf('Property "%s" of class "%s" is not required', $propertyName, $className));
137+
}
138+
}
139+
140+
private function getProperty(string $propertyName, string $className) : stdClass
141+
{
142+
$properties = $this->getProperties($className);
143+
$propertyInfos = null;
144+
foreach ($properties as $classPropertyName => $property) {
145+
if ($classPropertyName === $propertyName) {
146+
return $property;
147+
}
148+
}
149+
150+
return new stdClass();
151+
}
152+
153+
/**
154+
* Gets an operation by its method name.
155+
*
156+
* @param string $className
157+
* @param string $method
158+
*
159+
* @throws Exception
160+
*
161+
* @return array | stdClass
162+
*/
163+
private function getOperation(string $method, string $className) : stdClass
164+
{
165+
foreach ($this->getOperations($className) as $classMethod => $operation) {
166+
if ($classMethod === $method) {
167+
return $operation;
168+
}
169+
}
170+
171+
throw new \Exception(sprintf('Operation "%s" of class "%s" does not exist.', $method, $className));
172+
}
173+
174+
/**
175+
* Gets all operations of a given class.
176+
*/
177+
private function getOperations(string $className) : stdClass
178+
{
179+
$classInfos = $this->getClassInfos($className);
180+
181+
return empty($classInfos) ? $classInfos : new stdClass();
182+
}
183+
184+
/**
185+
* Gets all properties of a given class.
186+
*/
187+
private function getProperties(string $className) : stdClass
188+
{
189+
$classInfos = $this->getClassInfos($className);
190+
191+
return empty($classInfos->{'properties'}) ? $classInfos->{'properties'} : new stdClass();
192+
}
193+
194+
private function getClassInfos(string $className, bool $getOperation = false) : stdClass
195+
{
196+
$json = $this->getLastJsonResponse();
197+
$classInfos = null;
198+
199+
if (isset($json->{'definitions'}) && !$getOperation) {
200+
foreach ($json->{'definitions'} as $classTitle => $classData) {
201+
if ($classTitle === $className) {
202+
$classInfos = $classData;
203+
}
204+
}
205+
}
206+
207+
if (isset($json->{'paths'}) && $getOperation) {
208+
foreach ($json->{'paths'} as $classTitle => $classPath) {
209+
foreach ($classPath as $classOperations) {
210+
foreach ($classOperations as $classOperation) {
211+
if (in_array($className, $classOperation['tags'])) {
212+
$classInfos = $classOperations;
213+
}
214+
}
215+
}
216+
}
217+
}
218+
219+
if (empty($classInfos)) {
220+
throw new \Exception(sprintf('Class %s cannot be found in the vocabulary', $className));
221+
}
222+
223+
return $classInfos;
224+
}
225+
226+
private function getLastJsonResponse() : stdClass
227+
{
228+
$content = $this->restContext->getMink()->getSession()->getDriver()->getContent();
229+
if (null === ($decoded = json_decode($content))) {
230+
throw new \RuntimeException('JSON response seems to be invalid');
231+
}
232+
233+
return $decoded;
234+
}
235+
}

features/swagger/doc.feature

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Feature: Documentation support
2+
In order to build an auto-discoverable API
3+
As a client software developer
4+
I need to know Swagger specifications of objects I send and receive
5+
6+
Scenario: Retrieve the API vocabulary
7+
Given I send a "GET" request to "/swagger"
8+
Then the response status code should be 200
9+
And the response should be in JSON
10+
And the header "Content-Type" should be equal to "application/ld+json"
11+
# Context
12+
And the JSON node "swagger" should be equal to "2.0"
13+
# Root properties
14+
And the JSON node "info.title" should be equal to "My Dummy API"
15+
And the JSON node "info.description" should be equal to "This is a test API."
16+
#And the JSON node "host" should be equal to "exemple.com"
17+
And the JSON node "basePath" should be equal to "/"
18+
# Supported classes
19+
And the Swagger class "CircularReference" exist
20+
And the Swagger class "CustomIdentifierDummy" exist
21+
And the Swagger class "CustomNormalizedDummy" exist
22+
And the Swagger class "CustomWritableIdentifierDummy" exist
23+
And the Swagger class "Dummy" exist
24+
And the Swagger class "RelatedDummy" exist
25+
And the Swagger class "RelationEmbedder" exist
26+
And the Swagger class "ThirdLevel" exist
27+
And the Swagger class "ParentDummy" not exist
28+
And the Swagger class "UnknownDummy" not exist
29+
# Properties
30+
And "id" property doesn't exist for the Swagger class "Dummy"
31+
And "name" property is required for Swagger class "Dummy"

src/Bridge/NelmioApiDoc/Extractor/AnnotationsProvider/ApiPlatformProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use ApiPlatform\Core\Api\FilterCollection;
1515
use ApiPlatform\Core\Bridge\NelmioApiDoc\Parser\ApiPlatformParser;
1616
use ApiPlatform\Core\Bridge\Symfony\Routing\OperationMethodResolverInterface;
17-
use ApiPlatform\Core\Hydra\ApiDocumentationBuilderInterface;
17+
use ApiPlatform\Core\Documentation\ApiDocumentationBuilderInterface;
1818
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1919
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
2020
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Command;
13+
14+
use ApiPlatform\Core\Swagger\ApiDocumentationBuilder;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
19+
/**
20+
* Console command to dump Swagger API documentations.
21+
*
22+
* @author Amrouche Hamza <[email protected]>
23+
*/
24+
final class SwaggerCommand extends Command
25+
{
26+
private $apiDocumentationBuilder;
27+
28+
public function __construct(ApiDocumentationBuilder $apiDocumentationBuilder)
29+
{
30+
parent::__construct();
31+
$this->apiDocumentationBuilder = $apiDocumentationBuilder;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
protected function configure()
38+
{
39+
$this
40+
->setName('api:swagger:export')
41+
->setDescription('Dump the Swagger 2.0 (OpenAPI) documentation');
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
protected function execute(InputInterface $input, OutputInterface $output)
48+
{
49+
$data = $this->apiDocumentationBuilder->getApiDocumentation();
50+
$content = json_encode($data, JSON_PRETTY_PRINT);
51+
$output->writeln($content);
52+
}
53+
}

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ public function load(array $configs, ContainerBuilder $container)
8383
$loader->load('metadata.xml');
8484
$loader->load('data_provider.xml');
8585

86+
if ($config['enable_swagger']) {
87+
$loader->load('swagger.xml');
88+
$container->setParameter('api_platform.enable_swagger', (string) $config['enable_swagger']);
89+
}
90+
8691
$this->enableJsonLd($loader);
8792
$this->registerAnnotationLoaders($container);
8893
$this->registerFileLoaders($container);

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ public function getConfigTreeBuilder()
6767
->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end()
6868
->booleanNode('enable_fos_user')->defaultValue(false)->info('Enable the FOSUserBundle integration.')->end()
6969
->booleanNode('enable_nelmio_api_doc')->defaultTrue()->info('Enable the Nelmio Api doc integration.')->end()
70-
->arrayNode('collection')
70+
->booleanNode('enable_swagger')->defaultValue(true)->info('Enable the Swagger documentation and export.')->end()
71+
72+
->arrayNode('collection')
7173
->addDefaultsIfNotSet()
7274
->children()
7375
->scalarNode('order')->defaultNull()->info('The default order of results.')->end()

src/Bridge/Symfony/Bundle/Resources/config/hydra.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979

8080
<!-- Action -->
8181

82-
<service id="api_platform.hydra.action.documentation" class="ApiPlatform\Core\Hydra\Action\DocumentationAction">
82+
<service id="api_platform.hydra.action.documentation" class="ApiPlatform\Core\Documentation\Action\DocumentationAction">
8383
<argument type="service" id="api_platform.hydra.documentation_builder" />
8484
</service>
8585

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
3+
<routes xmlns="http://symfony.com/schema/routing"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/routing
6+
http://symfony.com/schema/routing/routing-1.0.xsd">
7+
8+
<route id="api_swagger_vocab" path="/swagger">
9+
<default key="_controller">api_platform.swagger.action.documentation</default>
10+
</route>
11+
</routes>

0 commit comments

Comments
 (0)