diff --git a/README.md b/README.md index 7faf861..890d0e7 100644 --- a/README.md +++ b/README.md @@ -4,56 +4,42 @@ [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/kleijnweb/swagger-bundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/kleijnweb/swagger-bundle/?branch=master) [![Latest Stable Version](https://poser.pugx.org/kleijnweb/swagger-bundle/v/stable)](https://packagist.org/packages/kleijnweb/swagger-bundle) -Invert your workflow (contract first) using Swagger specs and set up a Symfony REST app with minimal config. +Invert your workflow (contract first) using Swagger ([Open API](https://openapis.org/)) specs and set up a Symfony REST app with minimal config. -Aimed to be lightweight, this bundle does not depend on FOSRestBundle or Twig (except for dev purposes). +Aimed to be lightweight, this bundle does not depend on FOSRestBundle or Twig. -*SwaggerBundle only supports json in- and output, and only YAML Swagger defintions.* - -Go to the [release page](https://github.com/kleijnweb/swagger-bundle/releases) to find details about the latest release. - -__This bundle is currently actively maintained.__ +## Important Notes + * You are looking at the documentation for the upcoming 3.0 release. + * SwaggerBundle only supports json in- and output, and only YAML Swagger definitions + * This bundle is currently actively maintained. + * Go to the [release page](https://github.com/kleijnweb/swagger-bundle/releases) to find details about the latest release. For a pretty complete example, see [swagger-bundle-example](https://github.com/kleijnweb/swagger-bundle-example). +A minimal example is also [available](https://github.com/kleijnweb/symfony-swagger-microservice-edition). -# This Bundle.. +## This bundle will.. -## Will: - - * Coerce parameters to their defined types when possible. - * Validate content and parameters based on your Swagger spec. - * Configure routing based on your Swagger spec. - * Handle standard status codes such as 500, 400 and 404. - * Encode response data as JSON. - * Return `application/vnd.error+json` responses when errors occur. + * Configure routing based on your Swagger documents(s), accounting for things like type, enums and pattern matches. + * Validate body and parameters based on your Swagger documents(s). + * Coerce query and path parameters to their defined types when possible. + * Resolve [JSON Pointer](http://json-spec.readthedocs.org/en/latest/pointer.html)s anywhere in your Swagger documents and partials (not just in the [JSON Schema](http://json-schema.org/) bits). + * Return [vnd.error](https://github.com/blongden/vnd.error) (`application/vnd.error+json`) responses when errors occur. * Utilize vnd.error's `logref` to make errors traceable. - * Resolve JSON-Schema `$ref`s in your Swagger spec to allow reusable partial specs. - -## Can: + * Optionally (De-) Serialize objects using either the Symfony Component Serializer or JMS\Serializer - * Amend your Swagger spec to include the error responses added by SwaggerBundle. - * (De-) Serialize objects using either the Symfony Component Serializer or JMS\Serializer - * Generate DTO-like classes representing resources in your Swagger spec. - -## Won't: +## It won't, and probably never will: * Handle Form posts. - * Generate your API documentation. Use your Swagger document, plenty of options. + * Generate your API documentation. Use your Swagger documents, plenty of options. * Mix well with GUI bundles. The bundle is biased towards lightweight API-only apps. - * Update the resource schemas in your Swagger spec when these classes change (not yet, but __soon__, see [#3](https://github.com/kleijnweb/swagger-bundle/issues/3)). - * Work with JSON Swagger documents (yet, see [#10](https://github.com/kleijnweb/swagger-bundle/issues/10)). - * Do content negotiation. May support XML in the future (low priority, see [#1](https://github.com/kleijnweb/swagger-bundle/issues/1)). - -__TIP:__ Want to build an API-only app using this bundle? Try [kleijnweb/symfony-swagger-microservice-edition](https://github.com/kleijnweb/symfony-swagger-microservice-edition). + * Work with JSON Swagger documents. + * Do content negotiation or support XML. # Usage 1. Create a Swagger file, for example using http://editor.swagger.io/. 2. Install and configure this bundle -3. Create one or more controllers (as services!), doing the actual work, whatever that may be. -4. You are DONE. - -Pretty much. ;) +3. Create one or more controllers (as services), doing the actual work, whatever that may be. ## Install And Configure @@ -63,16 +49,16 @@ Add Swagger-based routing to your app, for example: ```yml test: - resource: "config/yourapp.yml" - type: swagger + resource: "config/yourapp.yml" + type: swagger ``` The path here is relative to the `swagger.document.base_path` configuration option. The above example would require something like this in your config: ```yml swagger: - document: - base_path: "%kernel.root_dir%" + document: + base_path: "%kernel.root_dir%" ``` # Functional Details / Features @@ -111,7 +97,7 @@ public function placeOrder(array $body) } ``` -__NOTE:__ SwaggerBundle applies some type conversion to input and adds the converted types to the Request `attributes`. +__NOTE:__ SwaggerBundle applies some type conversion to query and path parameters and adds the converted values to the Request `attributes`. Using `Request::get()` will give precedence to parameters in `query`. These values will be 'raw', using `attributes` is preferred. Your controllers do not need to implement any interfaces or extend any classes. A controller might look like this (using object deserialization, see section below): @@ -135,7 +121,17 @@ class StoreController It would make more sense to name the parameter `order` instead of `body`, but this is how it is in the pet store example provided by Swagger. -Other parameters can be added to the signature as well, this is standard Symfony behaviour. +All (type-casted) parameters can be added to the signature, since they are attributes when the controller is invoked. This is standard Symfony behaviour. + +## Caching + +Parsing YAML and resolving JSON Pointers can be slow, especially with larger specs with external references. SwaggerBundle can use a Doctrine cache to mitigate this. Use a DI key to reference the service you want to use: + +```yml +swagger: + document: + cache: "some.doctrine.cache.service" +``` ## Route Matching @@ -145,24 +141,48 @@ and `enum` when dealing with string path parameters. ## Exception Handling -Any exceptions are caught, logged by the `@logger` service, and result in `application/vnd.error+json`. Routing failure results in a 404 response without `logref`. +Any exceptions are caught, logged by the `@logger` service, and result in `application/vnd.error+json`. The log-level/severity depends on the exception type and/or code. "Not Found" errors are logged as 'INFO'. ## Input Validation ### Parameter Validation -SwaggerBundle will attempt to convert string values to any scalar value specified in your swagger file, within reason. +SwaggerBundle will attempt to convert path and query string values to the scalar value specified in your swagger file, within reason. For example, it will accept all of "0", "1", "TRUE", "FALSE", "true", and "false" as boolean values, but wont blindly evaluate any string value as `TRUE`. -Parameter validation errors will result in a `vnd.error` response with a status code of 400. - -__NOTE__: SwaggerBundle currently does not support `multi` for `collectionFormat` when handling array parameters. +__NOTE__: SwaggerBundle currently does not support `multi` for `collectionFormat` when handling array parameters (see [#50](https://github.com/kleijnweb/swagger-bundle/issues/50)). ### Content Validation -If the content cannot be decoded using the format specified by the request's Content-Type header, or if validation -of the content using the resource schema failed, SwaggerBundle will return a `vnd.error` response with a 400 status code. +If the content cannot be decoded as JSON, or if validation of the content using the resource schema failed, +SwaggerBundle will return a `vnd.error` response with a 400 status code. + +### Validation Feedback + +Parameter validation errors will result in a `vnd.error` response with a status code of 400. + +The validation errors (produced by [justinrainbow/json-schema](https://github.com/justinrainbow/json-schema)), are included in the response, with [HAL](http://stateless.co/hal_specification.html) links that are essentially JSON Pointers +to the parameter definition in the relevant Swagger Document. + +In order for this to work properly, you may need some additional config. + +When SwaggerBundle generates the JSON Pointer URI, it uses the following conventions: + +1. For the protocol/scheme, it uses to the scheme used to make the request, unless globally configured otherwise, *or* if not in the specs `schemes` (in which case it will use, in order of preference: https, wss, http, ws). +2. For the host name, it will prefer the global config. If not defined it will use the value of `host` in the spec, ultimately falling back to the host name used to make the request. +3. For the relative path, it will use the path relative to `swagger.document.base_path`. If configured, it will prefix the `swagger.document.public.base_url` + +Example: + +```yaml +swagger: + document: + public: + proto: 'http' # Even if the spec claims it support https, this will cause the links to use http, unless the request was made using https (likewise you can use this to force https even if the request was made using http) + base_url: specs # This will prefix '/specs' to all paths + host: some.host.tld # Fetch specs from said host, instead of what's defined in the spec or the current one +``` ### Object (De-) Serialization @@ -218,58 +238,15 @@ public function placeOrder(Request $request) //... } ``` -When a controller action returns `NULL`, SwaggerBundle will return an empty `204` response. - -#### Using Annotations - -In order to use annotations, you should make sure you use an autoload bootstrap - that will initialize doctrine/annotations: - -```php -use Doctrine\Common\Annotations\AnnotationRegistry; -use Composer\Autoload\ClassLoader; - -/** - * @var ClassLoader $loader - */ -$loader = require __DIR__.'/../vendor/autoload.php'; - -AnnotationRegistry::registerLoader(array($loader, 'loadClass')); - -return $loader; -``` - -Good chance you are already using a bootstrap file like this, but if the annotations won't load, this is where to look. - -## Authentication - -SwaggerBundle 2.0+ does not include authentication functionality. The JWT support from 1.0 was moved into [kleijnweb/jwt-bundle](https://github.com/kleijnweb/jwt-bundle)). - -When using `SecurityDefinition` type `oauth2`, it would be possible to translate *scopes* to Symfony roles, - add them to the user, and automatically configure `access_control`. - This is not currently implemented (yet, see [#15](https://github.com/kleijnweb/swagger-bundle/issues/15)). - +When a controller action returns `NULL`, SwaggerBundle will return an empty `204` response, provided that one is defined in the specification. +Otherwise, it will default to the first 2xx type response defined in your spec, or if all else fails, simply 200. + # Developing -__NOTE:__ In order to use development tools, the `require-dev` dependencies are needed, as well as setting the `dev` configuration option: - -```yml -swagger: - dev: true # Or perhaps "%kernel.debug%" -``` - -## Amending Your Swagger Document - -SwaggerBundle adds some standardized behavior, this should be reflected in your Swagger document. Instead of doing this manually, you can use the `swagger:document:amend` command. - -## Generating Resource Classes - -SwaggerBundle can generate classes for you based on your Swagger resource definitions. -You can use the resulting classes as DTO-like objects for your services, or create Doctrine mapping config for them. Obviously this requires you to enable object serialization. -The resulting classes will have JMS\Serializer annotations by default, the use of which is optional, remove them if you're using the standard Symfony serializer. +# Utilities -See `app/console swagger:generate:resources --help` for more details. +See [swagger-bundle-tools](https://github.com/kleijnweb/swagger-bundle-tools). ## Functional Testing Your API @@ -321,8 +298,8 @@ class PetStoreApiTest extends WebTestCase } ``` -When using ApiTestCase, initSchemaManager() will also validate your Swagger spec against the official schema to ensure it is valid. - +When using ApiTestCase, initSchemaManager() will also validate your Swagger spec against the official schema to ensure it is valid. + ## License KleijnWeb\SwaggerBundle is made available under the terms of the [LGPL, version 3.0](https://spdx.org/licenses/LGPL-3.0.html#licenseText). diff --git a/composer.json b/composer.json index 4ad109a..0883b98 100644 --- a/composer.json +++ b/composer.json @@ -29,13 +29,16 @@ "symfony/finder": ">=2.6.0", "symfony/property-access": ">=2.6.0", "doctrine/collections": "^1.3", - "justinrainbow/json-schema": ">=1.4.2 <1.5" + "justinrainbow/json-schema": ">=1.4.2 <1.5", + "ramsey/vnderror": "^3.0.0", + "nocarrier/hal": "^0.9.12" }, "suggest": { "symfony/serializer": "Object de- serialization using Symfony Serializer Component", "jms/serializer": "Object de- serialization using JMS\\Serializer", "doctrine/annotations": "Object de- serialization annotation support", - "kleijnweb/jwt-bundle": "JWT authentication support" + "kleijnweb/jwt-bundle": "JWT authentication support", + "doctrine/cache": "Caching parsed and resolved Swagger documents" }, "require-dev": { "phpunit/phpunit": ">=4.1.0", @@ -52,9 +55,16 @@ "jms/serializer": "1.0", "phpoption/phpoption": ">=1.1.0", "fr3d/swagger-assertions": "^0.2.0", - "satooshi/php-coveralls": "<1.0" + "satooshi/php-coveralls": "<1.0", + "doctrine/cache": "^1.5.0" }, "config": { "bin-dir": "bin" + }, + "extra": { + "branch-alias": { + "dev-3.0": "3.0.x-dev", + "dev-master": "2.2.x-dev" + } } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 17b8dd6..7226f7f 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -26,10 +26,6 @@ public function getConfigTreeBuilder() $rootNode ->children() - ->scalarNode('dev') - ->defaultFalse() - ->end() - ->arrayNode('serializer') ->addDefaultsIfNotSet() ->children() @@ -45,6 +41,7 @@ public function getConfigTreeBuilder() ->arrayNode('document') ->addDefaultsIfNotSet() ->children() + ->scalarNode('cache')->isRequired()->defaultFalse()->end() ->scalarNode('base_path')->defaultValue('')->end() ->end() ->end() diff --git a/src/DependencyInjection/KleijnWebSwaggerExtension.php b/src/DependencyInjection/KleijnWebSwaggerExtension.php index f6fba83..5c875d4 100644 --- a/src/DependencyInjection/KleijnWebSwaggerExtension.php +++ b/src/DependencyInjection/KleijnWebSwaggerExtension.php @@ -8,8 +8,6 @@ namespace KleijnWeb\SwaggerBundle\DependencyInjection; -use KleijnWeb\SwaggerBundle\Request\ContentDecoder; -use KleijnWeb\SwaggerBundle\Serializer\SerializationTypeResolver; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Reference; @@ -30,10 +28,6 @@ public function load(array $configs, ContainerBuilder $container) $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yml'); - if ($config['dev']) { - $loader->load('services_dev.yml'); - } - $container->setParameter('swagger.document.base_path', $config['document']['base_path']); $container->setParameter('swagger.serializer.namespace', $config['serializer']['namespace']); @@ -44,6 +38,11 @@ public function load(array $configs, ContainerBuilder $container) $resolverDefinition = $container->getDefinition('swagger.request.processor.content_decoder'); $resolverDefinition->addArgument(new Reference('swagger.serializer.type_resolver')); } + + if (isset($config['document']['cache'])) { + $resolverDefinition = $container->getDefinition('swagger.document.repository'); + $resolverDefinition->addArgument(new Reference($config['document']['cache'])); + } } /** diff --git a/src/Dev/Command/AmendSwaggerDocumentCommand.php b/src/Dev/Command/AmendSwaggerDocumentCommand.php deleted file mode 100644 index fbd606b..0000000 --- a/src/Dev/Command/AmendSwaggerDocumentCommand.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ -class AmendSwaggerDocumentCommand extends Command -{ - const NAME = 'swagger:document:amend'; - - /** - * @var DocumentRepository - */ - private $documentRepository; - - /** - * @var Fixer - */ - private $fixer; - - /** - * @param DocumentRepository $documentRepository - * @param Fixer $fixer - */ - public function __construct(DocumentRepository $documentRepository, Fixer $fixer) - { - parent::__construct(self::NAME); - - $this - ->setDescription('Make your Swagger definition reflect your apps in- and output') - ->setHelp( - "Will update your definition with predefined SwaggerBundle responses," - . " as well as update it to reflect any changes in your DTOs, should they exist.\n\n" - . "This is a development tool and will only work with require-dev dependencies included" - ) - ->addArgument('file', InputArgument::REQUIRED, 'File path to the Swagger document') - ->addOption( - 'out', - 'o', - InputOption::VALUE_REQUIRED, - 'Write the resulting document to this location (will overwrite existing by default' - ); - - $this->documentRepository = $documentRepository; - $this->fixer = $fixer; - } - - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return void - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $document = $this->documentRepository->get($input->getArgument('file')); - $this->fixer->fix($document); - $document->write($input->getOption('out')); - } -} diff --git a/src/Dev/Command/GenerateResourceClassesCommand.php b/src/Dev/Command/GenerateResourceClassesCommand.php deleted file mode 100644 index aaf43c3..0000000 --- a/src/Dev/Command/GenerateResourceClassesCommand.php +++ /dev/null @@ -1,80 +0,0 @@ - - */ -class GenerateResourceClassesCommand extends ContainerAwareCommand -{ - const NAME = 'swagger:generate:resources'; - - /** - * @var ResourceGenerator - */ - private $generator; - - /** - * @var DocumentRepository - */ - private $documentRepository; - - /** - * @param DocumentRepository $documentRepository - * @param ResourceGenerator $generator - */ - public function __construct(DocumentRepository $documentRepository, ResourceGenerator $generator) - { - parent::__construct(self::NAME); - - $this - ->setDescription('Generate DTO-like classes using the resource schema definitions in a swagger document') - ->setHelp('This is a development tool and will only work with require-dev dependencies included') - ->addArgument('file', InputArgument::REQUIRED, 'File path to the Swagger document') - ->addArgument('bundle', InputArgument::REQUIRED, 'Name of the bundle you want the classes in') - ->addOption( - 'namespace', - null, - InputOption::VALUE_REQUIRED, - 'Namespace of the classes to generate (relative to the bundle namespace)', - 'Model\Resources' - ); - - $this->documentRepository = $documentRepository; - $this->generator = $generator; - } - - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return void - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - /** @var KernelInterface $kernel */ - $kernel = $this->getContainer()->get('kernel'); - $bundle = $kernel->getBundle($input->getArgument('bundle')); - $document = $this->documentRepository->get($input->getArgument('file')); - $this->generator->setSkeletonDirs(__DIR__ . '/../Resources/skeleton'); - $this->generator->generate($bundle, $document, $input->getOption('namespace')); - } -} diff --git a/src/Dev/DocumentFixer/Fixer.php b/src/Dev/DocumentFixer/Fixer.php deleted file mode 100644 index ee72a15..0000000 --- a/src/Dev/DocumentFixer/Fixer.php +++ /dev/null @@ -1,50 +0,0 @@ -process($document); - - if ($this->next) { - $this->next->fix($document); - } - } - - /** - * @param Fixer $next - * - * @return $this - */ - public function chain(Fixer $next) - { - $this->next = $next; - - return $this; - } - - /** - * @param SwaggerDocument $document - * - * @return void - */ - abstract public function process(SwaggerDocument $document); -} diff --git a/src/Dev/DocumentFixer/Fixers/SwaggerBundleResponseFixer.php b/src/Dev/DocumentFixer/Fixers/SwaggerBundleResponseFixer.php deleted file mode 100644 index 139e0a8..0000000 --- a/src/Dev/DocumentFixer/Fixers/SwaggerBundleResponseFixer.php +++ /dev/null @@ -1,70 +0,0 @@ -getDefinition(); - - if (!isset($definition->responses)) { - $definition->responses = []; - } - if (!isset($definition->responses['ServerError'])) { - $definition->responses['ServerError'] = [ - 'description' => 'Server Error', - 'schema' => ['$ref' => '#/definitions/VndError'] - ]; - } - if (!isset($definition->responses['InputError'])) { - $definition->responses['InputError'] = [ - 'description' => 'Input Error', - 'schema' => ['$ref' => '#/definitions/VndError'] - ]; - } - if (!isset($definition->definitions)) { - $definition->definitions = []; - } - if (!isset($definition->definitions['VndError'])) { - $definition->definitions['VndError'] = [ - 'type' => 'object', - 'required' => ['message', 'logref'], - 'properties' => [ - 'message' => ['type' => 'string'], - 'logref' => ['type' => 'string'] - ] - ]; - } - foreach ($definition->paths as &$operations) { - foreach ($operations as &$operation) { - if (!isset($operation['responses']['500'])) { - $operation['responses']['500'] = [ - 'description' => 'Generic server error', - 'schema' => ['$ref' => '#/responses/ServerError'] - ]; - } - if (!isset($operation['responses']['400'])) { - $operation['responses']['400'] = [ - 'description' => 'Client input error', - 'schema' => ['$ref' => '#/responses/InputError'] - ]; - } - } - } - } -} diff --git a/src/Dev/Generator/ResourceGenerator.php b/src/Dev/Generator/ResourceGenerator.php deleted file mode 100644 index d6583fe..0000000 --- a/src/Dev/Generator/ResourceGenerator.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -class ResourceGenerator extends Generator -{ - /** - * @param BundleInterface $bundle - * @param SwaggerDocument $document - * @param string $relativeNamespace - */ - public function generate(BundleInterface $bundle, SwaggerDocument $document, $relativeNamespace = 'Model\Resources') - { - $dir = $bundle->getPath(); - - $parameters = [ - 'namespace' => $bundle->getNamespace(), - 'bundle' => $bundle->getName(), - 'resource_namespace' => $relativeNamespace - ]; - - foreach ($document->getResourceSchemas() as $typeName => $spec) { - $resourceFile = "$dir/" . str_replace('\\', '/', $relativeNamespace) . "/$typeName.php"; - $this->renderFile( - 'resource.php.twig', - $resourceFile, - array_merge($parameters, $spec, ['resource' => $typeName, 'resource_class' => $typeName]) - ); - } - } -} diff --git a/src/Dev/Resources/skeleton/resource.php.twig b/src/Dev/Resources/skeleton/resource.php.twig deleted file mode 100644 index 96763f1..0000000 --- a/src/Dev/Resources/skeleton/resource.php.twig +++ /dev/null @@ -1,148 +0,0 @@ -") - */ - private ${{ name }}; - {% elseif property.type == 'number' %} - - /** - * @var float - * @Type("double") - */ - private ${{ name }}; - {% elseif property.type == 'string' and property.format is defined and property.format == 'date-time' %} - - /** - * @var \DateTime - * @Type("DateTime<'Y-m-d'>") - */ - private ${{ name }}; - {% elseif property.type == 'array' and property.items is defined and property.items.type == 'object' %} - {% set typeName = property.items.id|split('/')|last %} - {% set fqTypeName = namespace ~ (resource_namespace ? '\\' ~ resource_namespace : '') ~ '\\' ~ typeName %} - - /** - * @var {{ typeName }}[] - * @Type("array<{{ fqTypeName }}>") - */ - private ${{ name }}; - {% else %} - - /** - * @var {{ property.type }} - * @Type("{{ property.type }}") - */ - private ${{ name }}; - {% endif %} - {% endfor %} - {% for name, property in properties %} - {%- if property.type == 'object' %} - {%- set typeName = property.id|split('/')|last %} - - /** - * @param {{ typeName }} - * - * @return $this - */ - {%- elseif property.type == 'string' and property.format is defined and property.format == 'date' %} - - /** - * @param \DateTime - * - * @return $this - */ - {%- elseif property.type == 'number' %} - - /** - * @param float - * - * @return $this - */ - {%- elseif property.type == 'string' and property.format is defined and property.format == 'date-time' %} - - /** - * @param \DateTime - * - * @return $this - */ - {%- else %} - - /** - * @param {{ property.type }} - * - * @return $this - */ - {%- endif %} - - public function set{{ name|capitalize }}(${{ name }}) - { - $this->{{ name }} = ${{ name }}; - - return $this; - } - {% endfor %} - {% for name, property in properties %} - {% if property.type == 'object' %} - {%- set typeName = property.id|split('/')|last -%} - - /** - * @return {{ typeName }} - */ - {% elseif property.type == 'string' and property.format is defined and property.format == 'date' %} - - /** - * @return \DateTime - */ - {% elseif property.type == 'number' %} - - /** - * @return float - */ - {% elseif property.type == 'string' and property.format is defined and property.format == 'date-time' %} - - /** - * @return \DateTime - */ - {% else %} - - /** - * @return {{ property.type }} - */ - {%- endif %} - - public function get{{ name|capitalize }}() - { - return $this->{{ name }}; - }{{- "\n" -}} - {%- endfor -%} -{%- endblock class_body -%}{{- '}' -}} -{{- "\n" -}} \ No newline at end of file diff --git a/src/Dev/Test/ApiResponseErrorException.php b/src/Dev/Test/ApiResponseErrorException.php deleted file mode 100644 index 1c9c815..0000000 --- a/src/Dev/Test/ApiResponseErrorException.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ -class ApiResponseErrorException extends \Exception -{ - /** - * @param object $json - * @param int $httpStatusCode - */ - public function __construct($json, $httpStatusCode) - { - $this->message = "Returned $httpStatusCode"; - if ($json) { - $this->message = $json->message; - if (isset($json->logref)) { - $this->message = "$json->message [logref $json->logref]"; - } - - } - - $this->code = $httpStatusCode; - } -} diff --git a/src/Document/DocumentRepository.php b/src/Document/DocumentRepository.php index f37d06f..c3fb5be 100644 --- a/src/Document/DocumentRepository.php +++ b/src/Document/DocumentRepository.php @@ -8,27 +8,39 @@ namespace KleijnWeb\SwaggerBundle\Document; -use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Cache\Cache; +use KleijnWeb\SwaggerBundle\Document\Exception\ResourceNotReadableException; /** * @author John Kleijn */ -class DocumentRepository extends ArrayCollection +class DocumentRepository { /** * @var string */ private $basePath; + /** + * @var array + */ + private $documents = []; + + /** + * @var Cache + */ + private $cache; + /** * Initializes a new Repository. * * @param string $basePath + * @param Cache $cache */ - public function __construct($basePath = null) + public function __construct($basePath = null, Cache $cache = null) { $this->basePath = $basePath; - parent::__construct([]); + $this->cache = $cache; } /** @@ -44,11 +56,35 @@ public function get($documentPath) if (!$documentPath) { throw new \InvalidArgumentException("No document path provided"); } - $document = parent::get($documentPath); + if (!isset($this->documents[$documentPath])) { + $this->documents[$documentPath] = $this->load($documentPath); + } + + return $this->documents[$documentPath]; + } + + /** + * @param string $documentPath + * + * @return SwaggerDocument + * @throws ResourceNotReadableException + */ + private function load($documentPath) + { + if ($this->cache && $document = $this->cache->fetch($documentPath)) { + return $document; + } + + if (!is_readable($documentPath)) { + throw new ResourceNotReadableException("Document '$documentPath' is not readable"); + } + + $parser = new YamlParser(); + $resolver = new RefResolver($parser->parse((string)file_get_contents($documentPath)), $documentPath); + $document = new SwaggerDocument($documentPath, $resolver->resolve()); - if (!$document) { - $document = new SwaggerDocument($documentPath); - $this->set($documentPath, $document); + if ($this->cache) { + $this->cache->save($documentPath, $document); } return $document; diff --git a/src/Document/Exception/InvalidReferenceException.php b/src/Document/Exception/InvalidReferenceException.php new file mode 100644 index 0000000..b28ce39 --- /dev/null +++ b/src/Document/Exception/InvalidReferenceException.php @@ -0,0 +1,16 @@ + + */ +class InvalidReferenceException extends \Exception +{ +} diff --git a/src/Document/Exception/ResourceNotReadableException.php b/src/Document/Exception/ResourceNotReadableException.php new file mode 100644 index 0000000..ff0f166 --- /dev/null +++ b/src/Document/Exception/ResourceNotReadableException.php @@ -0,0 +1,16 @@ + + */ +class ResourceNotReadableException extends InvalidReferenceException +{ +} diff --git a/src/Document/OperationObject.php b/src/Document/OperationObject.php new file mode 100644 index 0000000..5b64638 --- /dev/null +++ b/src/Document/OperationObject.php @@ -0,0 +1,206 @@ + + */ +class OperationObject +{ + /** + * @var SwaggerDocument + */ + private $document; + + /** + * @var object + */ + private $definition; + + /** + * @var string + */ + private $path; + + /** + * @var string + */ + private $method; + + /** + * @param SwaggerDocument $document + * @param string $path + * @param string $method + */ + public function __construct(SwaggerDocument $document, $path, $method) + { + $paths = $document->getPathDefinitions(); + + if (!property_exists($paths, $path)) { + throw new \InvalidArgumentException("Path '$path' not in Swagger document"); + } + $method = strtolower($method); + if (!property_exists($paths->$path, $method)) { + throw new \InvalidArgumentException("Method '$method' not supported for path '$path'"); + } + + $this->document = $document; + $this->path = $path; + $this->method = $method; + $this->definition = $paths->$path->$method; + $this->definition->{'x-request-schema'} = $this->assembleRequestSchema(); + } + + /** + * @param object $definition + * @param string $path + * @param string $method + * + * @return static + */ + public static function createFromOperationDefinition($definition, $path = '/', $method = 'GET') + { + $method = strtolower($method); + $documentDefinition = (object)[ + 'paths' => (object)[ + $path => (object)[ + $method => $definition + ] + ] + ]; + $document = new SwaggerDocument('', $documentDefinition); + + return new static($document, $path, $method); + } + + /** + * @return object + */ + public function getRequestSchema() + { + return $this->definition->{'x-request-schema'}; + } + + /** + * @return bool + */ + public function hasParameters() + { + return property_exists($this->definition, 'parameters'); + } + + /** + * @return object + */ + public function getParameters() + { + return $this->definition->parameters; + } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * @return object + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * @param string $parameterName + * + * @return string + */ + public function createParameterPointer($parameterName) + { + foreach ($this->definition->parameters as $i => $paramDefinition) { + if ($paramDefinition->name === $parameterName) { + return '/' . implode('/', [ + 'paths', + str_replace(['~', '/'], ['~0', '~1'], $this->getPath()), + $this->getMethod(), + 'parameters', + $i + ]); + } + } + throw new \InvalidArgumentException("Parameter '$parameterName' not in document"); + } + + /** + * @param string $parameterName + * + * @return string + */ + public function createParameterSchemaPointer($parameterName) + { + foreach ($this->definition->{'x-request-schema'}->properties as $propertyName => $schema) { + if ($propertyName === $parameterName) { + return '/' . implode('/', [ + 'paths', + str_replace(['~', '/'], ['~0', '~1'], $this->getPath()), + $this->getMethod(), + 'x-request-schema', + 'properties', + $propertyName + ]); + } + } + throw new \InvalidArgumentException("Parameter '$parameterName' not in document"); + } + + /** + * @return object + */ + private function assembleRequestSchema() + { + if (!isset($this->definition->parameters)) { + return new \stdClass; + } + $schema = new \stdClass; + $schema->type = 'object'; + $schema->required = []; + $schema->properties = new \stdClass; + + foreach ($this->definition->parameters as $paramDefinition) { + if (isset($paramDefinition->required) && $paramDefinition->required) { + $schema->required[] = $paramDefinition->name; + } + if ($paramDefinition->in === 'body') { + $schema->properties->{$paramDefinition->name} = property_exists($paramDefinition, 'schema') + ? $paramDefinition->schema + : (object)['type' => 'object']; + continue; + } + + $type = property_exists($paramDefinition, 'type') ? $paramDefinition->type : 'string'; + $propertyDefinition = $schema->properties->{$paramDefinition->name} = (object)['type' => $type]; + if (property_exists($paramDefinition, 'items')) { + $propertyDefinition->items = $paramDefinition->items; + } + } + + return $schema; + } +} diff --git a/src/Document/ParameterRefBuilder.php b/src/Document/ParameterRefBuilder.php new file mode 100644 index 0000000..56a0784 --- /dev/null +++ b/src/Document/ParameterRefBuilder.php @@ -0,0 +1,123 @@ + + */ +class ParameterRefBuilder +{ + /** + * @var array + */ + private static $schemes = ['https', 'wss', 'http', 'ws']; + + /** + * @var string + */ + private $scheme; + + /** + * @var string + */ + private $host; + + /** + * @var string + */ + private $basePath; + + /** + * Construct the wrapper + * + * @param string $basePath + * @param string|null $scheme + * @param string|null $host + */ + public function __construct($basePath = '/', $scheme = null, $host = null) + { + $this->scheme = $scheme; + $this->host = $host; + $this->basePath = $basePath; + } + + /** + * @param Request $request + * @param string $parameterName + * + * @return string + */ + public function buildSpecificationLink(Request $request, $parameterName) + { + return "{$this->buildDocumentLink($request)}#{$this->createParameterPointer($request, $parameterName)}"; + } + + /** + * @param Request $request + * + * @return string + */ + public function buildDocumentLink(Request $request) + { + /** @var SwaggerDocument $document */ + $document = $request->attributes->get('_swagger_document'); + /** @var string $filePath */ + $filePath = $request->attributes->get('_definition'); + + $definition = $document->getDefinition(); + $basePath = $this->basePath; + $host = $this->host ?: property_exists($definition, 'host') ? $definition->host : $request->getHost(); + $scheme = $this->scheme; + if (!$scheme) { + $scheme = $request->getScheme(); + if (property_exists($definition, 'schemes')) { + if (!in_array($scheme, self::$schemes)) { + foreach (self::$schemes as $knownScheme) { + if (in_array($knownScheme, $definition->schemes)) { + $this->scheme = $knownScheme; + break; + } + } + } + } + } + + return "$scheme://$host{$basePath}{$filePath}"; + } + + /** + * @param Request $request + * @param string $parameterName + * + * @return string + */ + public function createParameterPointer(Request $request, $parameterName) + { + /** @var OperationObject $operation */ + $operation = $request->attributes->get('_swagger_operation'); + + return $operation->createParameterPointer($parameterName); + } + + /** + * @param Request $request + * @param string $parameterName + * + * @return string + */ + public function createParameterSchemaPointer(Request $request, $parameterName) + { + /** @var OperationObject $operation */ + $operation = $request->attributes->get('_swagger_operation'); + + return $operation->createParameterSchemaPointer($parameterName); + } +} diff --git a/src/Document/RefResolver.php b/src/Document/RefResolver.php new file mode 100644 index 0000000..5daf125 --- /dev/null +++ b/src/Document/RefResolver.php @@ -0,0 +1,260 @@ + + */ +class RefResolver +{ + /** + * @var object + */ + private $definition; + + /** + * @var string + */ + private $uri; + + /** + * @var string + */ + private $directory; + + /** + * @var YamlParser + */ + private $yamlParser; + + /** + * @param object $definition + * @param string $uri + * @param YamlParser $yamlParser + */ + public function __construct($definition, $uri, YamlParser $yamlParser = null) + { + $this->definition = $definition; + $uriSegs = $this->parseUri($uri); + if (!$uriSegs['proto']) { + $uri = realpath($uri); + } + $this->uri = $uri; + $this->directory = dirname($this->uri); + $this->yamlParser = $yamlParser ?: new YamlParser(); + } + + /** + * @return object + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Resolve all references + * + * @return object + */ + public function resolve() + { + $this->resolveRecursively($this->definition); + + return $this->definition; + } + + /** + * Revert to original state + * + * @return object + */ + public function unresolve() + { + $this->unresolveRecursively($this->definition); + + return $this->definition; + } + + /** + * @param object|array $current + * @param object $document + * @param string $uri + * + * @throws InvalidReferenceException + * @throws ResourceNotReadableException + */ + private function resolveRecursively(&$current, $document = null, $uri = null) + { + $document = $document ?: $this->definition; + $uri = $uri ?: $this->uri; + + if (is_array($current)) { + foreach ($current as &$value) { + if ($value !== null && !is_scalar($value)) { + $this->resolveRecursively($value, $document, $uri); + } + } + } elseif (is_object($current)) { + if (property_exists($current, '$ref')) { + $uri = $current->{'$ref'}; + if ('#' === $uri[0]) { + $current = $this->lookup($uri, $document); + } else { + $uriSegs = $this->parseUri($uri); + $normalizedUri = $this->normalizeUri($uriSegs); + $externalDocument = $this->loadExternal($normalizedUri); + $current = $this->lookup($uriSegs['segment'], $externalDocument, $normalizedUri); + $this->resolveRecursively($current, $externalDocument, $normalizedUri); + } + if (is_object($current)) { + $current->id = $uri; + $current->{'x-ref-id'} = $uri; + } + + return; + } + foreach ($current as $propertyName => &$propertyValue) { + $this->resolveRecursively($propertyValue, $document, $uri); + } + } + } + + /** + * @param object|array $current + * @param object|array $parent + * + * @return void + */ + private function unresolveRecursively(&$current, &$parent = null) + { + foreach ($current as $key => &$value) { + if ($value !== null && !is_scalar($value)) { + $this->unresolveRecursively($value, $current); + } + if ($key === 'x-ref-id') { + $parent = (object)['$ref' => $value]; + } + } + } + + /** + * @param string $path + * @param object $document + * @param string $uri + * + * @return mixed + * @throws InvalidReferenceException + */ + private function lookup($path, $document, $uri = null) + { + $target = $this->lookupRecursively( + explode('/', trim($path, '/#')), + $document + ); + if (!$target) { + throw new InvalidReferenceException( + "Target '$path' does not exist'" . ($uri ? " at '$uri''" : '') + ); + } + + return $target; + } + + /** + * @param array $segments + * @param object $context + * + * @return mixed + */ + private function lookupRecursively(array $segments, $context) + { + $segment = str_replace(['~0', '~1'], ['~', '/'], array_shift($segments)); + if (property_exists($context, $segment)) { + if (!count($segments)) { + return $context->$segment; + } + + return $this->lookupRecursively($segments, $context->$segment); + } + + return null; + } + + /** + * @param string $uri + * + * @return object + * @throws ResourceNotReadableException + */ + private function loadExternal($uri) + { + $exception = new ResourceNotReadableException("Failed reading '$uri'"); + + set_error_handler(function () use ($exception) { + throw $exception; + }); + $response = file_get_contents($uri); + restore_error_handler(); + + if (false === $response) { + throw $exception; + } + if (preg_match('/\b(yml|yaml)\b/', $uri)) { + return $this->yamlParser->parse($response); + } + + return json_decode($response); + } + + + /** + * @param array $uriSegs + * + * @return string + */ + private function normalizeUri(array $uriSegs) + { + return + $uriSegs['proto'] . $uriSegs['host'] + . rtrim($uriSegs['root'], '/') . '/' + . (!$uriSegs['root'] ? ltrim("$this->directory/", '/') : '') + . $uriSegs['path']; + } + + /** + * @param string $uri + * + * @return array + */ + private function parseUri($uri) + { + $defaults = [ + 'root' => '', + 'proto' => '', + 'host' => '', + 'path' => '', + 'segment' => '' + ]; + $pattern = '@' + . '(?P[a-z]+\://)?' + . '(?P[0-9a-z\.\@\:]+\.[a-z]+)?' + . '(?P/)?' + . '(?P[^#]*)' + . '(?P#.*)?' + . '@'; + + preg_match($pattern, $uri, $matches); + + return array_merge($defaults, array_intersect_key($matches, $defaults)); + } +} diff --git a/src/Document/SwaggerDocument.php b/src/Document/SwaggerDocument.php index cfb589d..ded2e45 100644 --- a/src/Document/SwaggerDocument.php +++ b/src/Document/SwaggerDocument.php @@ -8,10 +8,6 @@ namespace KleijnWeb\SwaggerBundle\Document; -use JsonSchema\RefResolver; -use JsonSchema\Uri\UriRetriever; -use Symfony\Component\Yaml\Yaml; - /** * @author John Kleijn */ @@ -20,33 +16,30 @@ class SwaggerDocument /** * @var string */ - private $pathFileName; + private $uri; /** - * @var \ArrayObject + * @var object */ private $definition; /** - * @param $pathFileName + * @var OperationObject[] */ - public function __construct($pathFileName) - { - if (!is_file($pathFileName)) { - throw new \InvalidArgumentException( - "Document file '$pathFileName' does not exist'" - ); - } - - $data = Yaml::parse(file_get_contents($pathFileName)); - $data = self::resolveSelfReferences($data, $data); + private $operations; - $this->pathFileName = $pathFileName; - $this->definition = new \ArrayObject($data, \ArrayObject::ARRAY_AS_PROPS | \ArrayObject::STD_PROP_LIST); + /** + * @param string $pathFileName + * @param object $definition + */ + public function __construct($pathFileName, $definition) + { + $this->uri = $pathFileName; + $this->definition = $definition; } /** - * @return array + * @return object */ public function getDefinition() { @@ -54,141 +47,40 @@ public function getDefinition() } /** - * @return array + * @return object */ public function getPathDefinitions() { return $this->definition->paths; } - /** - * @return array - */ - public function getResourceSchemas() - { - return $this->definition->definitions; - } - - /** - * @return string - */ - public function getBasePath() - { - return $this->definition->basePath; - } - /** * @param string $path * @param string $method * - * @return array - */ - public function getOperationDefinition($path, $method) - { - $paths = $this->getPathDefinitions(); - if (!isset($paths[$path])) { - throw new \InvalidArgumentException("Path '$path' not in Swagger document"); - } - $method = strtolower($method); - if (!isset($paths[$path][$method])) { - throw new \InvalidArgumentException("Method '$method' not supported for path '$path'"); - } - - return $paths[$path][$method]; - } - - /** - * @return array + * @return OperationObject */ - public function getArrayCopy() + public function getOperationObject($path, $method) { - return $this->definition->getArrayCopy(); - } + $key = "$path::$method"; - /** - * @param null $targetPath - * - * @return void - */ - public function write($targetPath = null) - { - $data = $this->getArrayCopy(); - $data = self::unresolveSelfReferences($data, $data); - $yaml = Yaml::dump($data, 10, 2); - $yaml = str_replace(': { }', ': []', $yaml); - file_put_contents($targetPath ?: $this->pathFileName, $yaml); - } - - /** - * Cloning will break things - */ - private function __clone() - { - } - - /** - * @param array $segments - * @param array $context - * - * @return mixed - */ - private static function lookupUsingSegments(array $segments, array $context) - { - $segment = array_shift($segments); - if (isset($context[$segment])) { - if (!count($segments)) { - return $context[$segment]; - } - - return self::lookupUsingSegments($segments, $context[$segment]); + if (isset($this->operations[$key])) { + return $this->operations[$key]; } - return null; + return $this->operations[$key] = new OperationObject($this, $path, $method); } /** - * @param array $doc - * @param array $data + * @deprecated * - * @return array - */ - private function resolveSelfReferences(array $doc, array &$data) - { - foreach ($data as $key => &$value) { - if (is_array($value)) { - $value = self::resolveSelfReferences($doc, $value); - } - if ($key === '$ref' && '#' === $value[0]) { - $data = self::lookupUsingSegments( - explode('/', trim(substr($value, 1), '/')), - $doc - ); - $data['id'] = $value; - // Use something a little less generic for more reliable qnd restoring of original - $data['x-swagger-id'] = $value; - } - } - - return $data; - } - - /** - * @param array $doc - * @param array $data + * @param string $path + * @param string $method * - * @return array + * @return object */ - private function unresolveSelfReferences(array $doc, array &$data) + public function getOperationDefinition($path, $method) { - foreach ($data as $key => &$value) { - if (is_array($value)) { - $value = self::unresolveSelfReferences($doc, $value); - } - if ($key === 'x-swagger-id') { - $data = ['$ref' => $value]; - } - } - - return $data; + return $this->getOperationObject($path, $method)->getDefinition(); } } diff --git a/src/Document/YamlCapableUriRetriever.php b/src/Document/YamlCapableUriRetriever.php deleted file mode 100644 index 383e96c..0000000 --- a/src/Document/YamlCapableUriRetriever.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -class YamlCapableUriRetriever extends AbstractRetriever -{ - /** - * TODO This is of course terribly inefficient - * - * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() - * - * @param string $uri - * - * @return string - */ - public function retrieve($uri) - { - set_error_handler(function () use ($uri) { - throw new ResourceNotFoundException('Schema not found at ' . $uri); - }); - $response = file_get_contents($uri); - restore_error_handler(); - - if (false === $response) { - throw new ResourceNotFoundException('Schema not found at ' . $uri); - } - if ($response == '' - && substr($uri, 0, 7) == 'file://' && substr($uri, -1) == '/' - ) { - throw new ResourceNotFoundException('Schema not found at ' . $uri); - } - $this->contentType = null; - if (preg_match('/\b(yml|yaml)\b/', $uri)) { - $data = Yaml::parse($response); - - return json_encode($data); - } - if (!empty($http_response_header)) { - foreach ($http_response_header as $header) { - if (0 < preg_match("/Content-Type:(\V*)/ims", $header, $match)) { - $actualContentType = trim($match[1]); - if (strpos($actualContentType, 'yaml')) { - return json_encode(Yaml::parse($response)); - } - } - } - } - - return $response; - } -} diff --git a/src/Document/YamlParser.php b/src/Document/YamlParser.php new file mode 100644 index 0000000..60ed1db --- /dev/null +++ b/src/Document/YamlParser.php @@ -0,0 +1,72 @@ + + */ +class YamlParser +{ + /** + * @var Parser + */ + private $parser; + + /** + * Construct the wrapper + */ + public function __construct() + { + $this->parser = new Parser(); + } + + /** + * @param string $string + * + * @return mixed + */ + public function parse($string) + { + // Hashmap support is broken, so disable it and attempt fix afterwards + $data = $this->parser->parse($string, true, false, false); + + return $this->fixHashMaps($data); + } + + /** + * @see https://github.com/symfony/symfony/pull/17711 + * + * @param mixed $data + * + * @return mixed + */ + private function fixHashMaps(&$data) + { + if (is_array($data)) { + $shouldBeObject = false; + $object = new \stdClass(); + $index = 0; + foreach ($data as $key => &$value) { + $object->$key = $this->fixHashMaps($value); + if ($index++ !== $key) { + $shouldBeObject = true; + } + } + if ($shouldBeObject) { + $data = $object; + } + } + + return $data; + } +} diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php deleted file mode 100644 index e5afb01..0000000 --- a/src/EventListener/ExceptionListener.php +++ /dev/null @@ -1,102 +0,0 @@ - - */ -class ExceptionListener -{ - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @param LoggerInterface $logger - */ - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } - - /** - * @param LoggerInterface $logger - * - * @return $this - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - - return $this; - } - - /** - * @param GetResponseForExceptionEvent $event - */ - public function onKernelException(GetResponseForExceptionEvent $event) - { - $logRef = uniqid(); - $exception = $event->getException(); - - if ($exception instanceof NotFoundHttpException) { - $event->setResponse(new VndErrorResponse("Not found", Response::HTTP_NOT_FOUND)); - - return; - } - - if ($exception instanceof AuthenticationException) { - $event->setResponse(new VndErrorResponse("Unauthorized", Response::HTTP_UNAUTHORIZED)); - - return; - } - - $code = $exception->getCode(); - - if (strlen($code) !== 3) { - $this->fallback($message, $code, $logRef, $exception); - } else { - switch (substr($code, 0, 1)) { - case '4': - $message = 'Input Error'; - $this->logger->notice("Input error [logref $logRef]: " . $exception->__toString()); - break; - case '5': - $message = 'Server Error'; - $this->logger->error("Runtime error [logref $logRef]: " . $exception->__toString()); - break; - default: - $this->fallback($message, $code, $logRef, $exception); - } - } - - $event->setResponse(new VndErrorResponse($message, $code, $logRef)); - } - - /** - * @param string $message - * @param string $code - * @param string $logRef - * @param \Exception $exception - */ - private function fallback(&$message, &$code, $logRef, \Exception $exception) - { - $code = 500; - $message = 'Server Error'; - $this->logger->critical("Runtime error [logref $logRef]: " . $exception->__toString()); - } -} diff --git a/src/EventListener/RequestListener.php b/src/EventListener/RequestListener.php index 3b70023..3455512 100644 --- a/src/EventListener/RequestListener.php +++ b/src/EventListener/RequestListener.php @@ -20,7 +20,7 @@ class RequestListener /** * @var DocumentRepository */ - private $schemaRepository; + private $documentRepository; /** * @var RequestProcessor @@ -33,7 +33,7 @@ class RequestListener */ public function __construct(DocumentRepository $schemaRepository, RequestProcessor $processor) { - $this->schemaRepository = $schemaRepository; + $this->documentRepository = $schemaRepository; $this->processor = $processor; } @@ -52,17 +52,12 @@ public function onKernelRequest(GetResponseEvent $event) if (!$request->get('_swagger_path')) { throw new \LogicException("Request does not contain reference to Swagger path"); } - $swaggerDocument = $this->schemaRepository->get($request->get('_definition')); + $swaggerDocument = $this->documentRepository->get($request->get('_definition')); + $request->attributes->set('_swagger_document', $swaggerDocument); - $operationDefinition = $swaggerDocument - ->getOperationDefinition( - $request->get('_swagger_path'), - $request->getMethod() - ); + $operation = $swaggerDocument->getOperationObject($request->get('_swagger_path'), $request->getMethod()); + $request->attributes->set('_swagger_operation', $operation); - $this->processor->process( - $request, - $operationDefinition - ); + $this->processor->process($request, $operation); } } diff --git a/src/EventListener/VndErrorExceptionListener.php b/src/EventListener/VndErrorExceptionListener.php new file mode 100644 index 0000000..90d315c --- /dev/null +++ b/src/EventListener/VndErrorExceptionListener.php @@ -0,0 +1,129 @@ + + */ +class VndErrorExceptionListener +{ + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var VndValidationErrorFactory + */ + private $validationErrorFactory; + + /** + * @param VndValidationErrorFactory $errorFactory + * @param LoggerInterface $logger + */ + public function __construct(VndValidationErrorFactory $errorFactory, LoggerInterface $logger) + { + $this->logger = $logger; + $this->validationErrorFactory = $errorFactory; + } + + /** + * @param LoggerInterface $logger + * + * @return $this + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + + return $this; + } + + /** + * @param GetResponseForExceptionEvent $event + * + * @throws \Exception + */ + public function onKernelException(GetResponseForExceptionEvent $event) + { + $logRef = uniqid(); + + try { + $exception = $event->getException(); + $request = $event->getRequest(); + $code = $exception->getCode(); + + if ($exception instanceof InvalidParametersException) { + $severity = LogLevel::NOTICE; + $statusCode = Response::HTTP_BAD_REQUEST; + $vndError = $this->validationErrorFactory->create($request, $exception, $logRef); + } else { + if ($exception instanceof NotFoundHttpException) { + $statusCode = Response::HTTP_NOT_FOUND; + $severity = LogLevel::INFO; + } else { + if ($exception instanceof AuthenticationException) { + $statusCode = Response::HTTP_UNAUTHORIZED; + $severity = LogLevel::WARNING; + } else { + $is3Digits = strlen($code) === 3; + $class = (int)substr($code, 0, 1); + if (!$is3Digits) { + $statusCode = Response::HTTP_INTERNAL_SERVER_ERROR; + $severity = LogLevel::CRITICAL; + } else { + switch ($class) { + case 4: + $severity = LogLevel::NOTICE; + $statusCode = Response::HTTP_BAD_REQUEST; + break; + case 5: + $statusCode = Response::HTTP_INTERNAL_SERVER_ERROR; + $severity = LogLevel::ERROR; + break; + default: + $statusCode = Response::HTTP_INTERNAL_SERVER_ERROR; + $severity = LogLevel::CRITICAL; + } + } + } + } + $message = Response::$statusTexts[$statusCode]; + $vndError = new VndError($message, $logRef); + $vndError->addLink('help', $request->get('_definition'), ['title' => 'Error Information']); + $vndError->addLink('about', $request->getUri(), ['title' => 'Error Information']); + } + + $reference = $logRef ? " [logref $logRef]" : ''; + $event->setResponse(new VndErrorResponse($vndError, $statusCode)); + $this->logger->log($severity, "{$vndError->getMessage()}{$reference}: $exception"); + } catch (\PHPUnit_Framework_Exception $e) { + throw $e; + } catch (\PHPUnit_Framework_Error $e) { + throw $e; + } catch (\Exception $e) { + // A simpler response where less can go wrong + $message = "Error Handling Failure"; + $vndError = new VndError($message, $logRef); + $this->logger->log(LogLevel::CRITICAL, "$message [logref $logRef]: $e"); + $event->setResponse(new VndErrorResponse($vndError, Response::HTTP_INTERNAL_SERVER_ERROR)); + } + } +} diff --git a/src/Exception/InvalidParametersException.php b/src/Exception/InvalidParametersException.php index a72f765..8d1b715 100644 --- a/src/Exception/InvalidParametersException.php +++ b/src/Exception/InvalidParametersException.php @@ -9,12 +9,33 @@ namespace KleijnWeb\SwaggerBundle\Exception; /** - * TODO Provide helpful info for vnd.error responses - * - * @see https://github.com/kleijnweb/swagger-bundle/issues/27 - * * @author John Kleijn */ class InvalidParametersException extends \Exception { + /** + * @var array + */ + private $validationErrors; + + /** + * @param string $message + * @param int $code + * @param array $validationErrors + * @param \Exception $previous + */ + public function __construct($message, array $validationErrors, $code = 400, $previous = null) + { + parent::__construct($message, $code, $previous); + + $this->validationErrors = $validationErrors; + } + + /** + * @return array + */ + public function getValidationErrors() + { + return $this->validationErrors; + } } diff --git a/src/Request/ContentDecoder.php b/src/Request/ContentDecoder.php index 7b93b8b..8876f6a 100644 --- a/src/Request/ContentDecoder.php +++ b/src/Request/ContentDecoder.php @@ -8,6 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Request; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\Exception\MalformedContentException; use KleijnWeb\SwaggerBundle\Exception\UnsupportedContentTypeException; use KleijnWeb\SwaggerBundle\Serializer\SerializationTypeResolver; @@ -53,17 +54,17 @@ public function setTypeResolver(SerializationTypeResolver $typeResolver = null) } /** - * @param Request $request - * @param array $operationDefinition + * @param Request $request + * @param OperationObject $operationObject * * @return mixed|null * @throws MalformedContentException * @throws UnsupportedContentTypeException */ - public function decodeContent(Request $request, array $operationDefinition) + public function decodeContent(Request $request, OperationObject $operationObject) { if ($content = $request->getContent()) { - $type = $this->typeResolver ? $this->typeResolver->resolve($operationDefinition) : null; + $type = $this->typeResolver ? $this->typeResolver->resolve($operationObject) : null; try { return $this->serializer->deserialize($content, $type, $request->getContentType()); diff --git a/src/Request/ParameterCoercer.php b/src/Request/ParameterCoercer.php index 0dc44a4..cb62dec 100644 --- a/src/Request/ParameterCoercer.php +++ b/src/Request/ParameterCoercer.php @@ -19,26 +19,26 @@ class ParameterCoercer /** * @SuppressWarnings(PHPMD.CyclomaticComplexity) * - * @param array $paramDefinition - * @param mixed $value + * @param object $paramDefinition + * @param mixed $value * * @return mixed * @throws MalformedContentException * @throws UnsupportedException */ - public static function coerceParameter(array $paramDefinition, $value) + public static function coerceParameter($paramDefinition, $value) { - switch ($paramDefinition['type']) { + switch ($paramDefinition->type) { case 'string': - if (!isset($paramDefinition['format'])) { + if (!isset($paramDefinition->format)) { return $value; } - switch ($paramDefinition['format']) { + switch ($paramDefinition->format) { case 'date': $value = \DateTime::createFromFormat('Y-m-d\TH:i:s\Z', "{$value}T00:00:00Z"); if ($value === false) { throw new MalformedContentException( - "Unable to decode param {$paramDefinition['name']}", + "Unable to decode param {$paramDefinition->name}", 400 ); } @@ -48,7 +48,7 @@ public static function coerceParameter(array $paramDefinition, $value) $value = \DateTime::createFromFormat(\DateTime::W3C, $value); if ($value === false) { throw new MalformedContentException( - "Unable to decode param {$paramDefinition['name']}", + "Unable to decode param {$paramDefinition->name}", 400 ); } @@ -57,6 +57,7 @@ public static function coerceParameter(array $paramDefinition, $value) default: return $value; } + break; case 'boolean': switch ($value) { case 'TRUE': @@ -68,12 +69,12 @@ public static function coerceParameter(array $paramDefinition, $value) case '0': return false; default: - throw new MalformedContentException("Unable to decode param {$paramDefinition['name']}", 400); + throw new MalformedContentException("Unable to decode param {$paramDefinition->name}", 400); } break; case 'number': if (!is_numeric($value)) { - throw new MalformedContentException("Unable to decode param {$paramDefinition['name']}", 400); + throw new MalformedContentException("Unable to decode param {$paramDefinition->name}", 400); } return (float)$value; @@ -81,8 +82,8 @@ public static function coerceParameter(array $paramDefinition, $value) if (is_array($value)) { return $value; } - $format = isset($paramDefinition['collectionFormat']) - ? $paramDefinition['collectionFormat'] + $format = isset($paramDefinition->collectionFormat) + ? $paramDefinition->collectionFormat : 'csv'; switch ($format) { @@ -99,15 +100,16 @@ public static function coerceParameter(array $paramDefinition, $value) "Array 'collectionFormat' '$format' is not currently supported" ); } + break; case 'integer': if (!ctype_digit($value)) { - throw new MalformedContentException("Unable to decode param {$paramDefinition['name']}", 400); + throw new MalformedContentException("Unable to decode param {$paramDefinition->name}", 400); } return (integer)$value; case 'null': if ($value !== '') { - throw new MalformedContentException("Unable to decode param {$paramDefinition['name']}", 400); + throw new MalformedContentException("Unable to decode param {$paramDefinition->name}", 400); } return null; diff --git a/src/Request/RequestCoercer.php b/src/Request/RequestCoercer.php index daecd57..b3d1aa0 100644 --- a/src/Request/RequestCoercer.php +++ b/src/Request/RequestCoercer.php @@ -8,6 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Request; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\Exception\UnsupportedException; use Symfony\Component\HttpFoundation\Request; use KleijnWeb\SwaggerBundle\Exception\MalformedContentException; @@ -25,47 +26,38 @@ class RequestCoercer /** * @param ContentDecoder $contentDecoder */ - public function __construct($contentDecoder) + public function __construct(ContentDecoder $contentDecoder) { $this->contentDecoder = $contentDecoder; } /** - * @param Request $request - * @param array $operationDefinition + * @param Request $request + * @param OperationObject $operationObject * * @throws MalformedContentException * @throws UnsupportedException */ - public function coerceRequest(Request $request, array $operationDefinition) + public function coerceRequest(Request $request, OperationObject $operationObject) { - $content = $this->contentDecoder->decodeContent($request, $operationDefinition); + $content = $this->contentDecoder->decodeContent($request, $operationObject); - if (!isset($operationDefinition['parameters'])) { - return; - } $paramBagMapping = [ 'query' => 'query', 'path' => 'attributes', 'header' => 'headers' ]; - foreach ($operationDefinition['parameters'] as $paramDefinition) { - $paramName = $paramDefinition['name']; + foreach ($operationObject->getDefinition()->parameters as $paramDefinition) { + $paramName = $paramDefinition->name; - if ($paramDefinition['in'] === 'body') { + if ($paramDefinition->in === 'body') { if ($content !== null) { $request->attributes->set($paramName, $content); } continue; } - - if (!isset($paramBagMapping[$paramDefinition['in']])) { - throw new UnsupportedException( - "Unsupported parameter 'in' value in definition '{$paramDefinition['in']}'" - ); - } - $paramBagName = $paramBagMapping[$paramDefinition['in']]; + $paramBagName = $paramBagMapping[$paramDefinition->in]; if (!$request->$paramBagName->has($paramName)) { continue; } diff --git a/src/Request/RequestProcessor.php b/src/Request/RequestProcessor.php index 4c6d34a..0649df3 100644 --- a/src/Request/RequestProcessor.php +++ b/src/Request/RequestProcessor.php @@ -8,6 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Request; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use Symfony\Component\HttpFoundation\Request; use KleijnWeb\SwaggerBundle\Exception\InvalidParametersException; use KleijnWeb\SwaggerBundle\Exception\MalformedContentException; @@ -39,17 +40,17 @@ public function __construct(RequestValidator $validator, RequestCoercer $coercer } /** - * @param Request $request - * @param array $operationDefinition + * @param Request $request + * @param OperationObject $operationObject * * @throws InvalidParametersException * @throws MalformedContentException * @throws UnsupportedContentTypeException */ - public function process(Request $request, array $operationDefinition) + public function process(Request $request, OperationObject $operationObject) { - $this->coercer->coerceRequest($request, $operationDefinition); - $this->validator->setOperationDefinition($operationDefinition); + $this->coercer->coerceRequest($request, $operationObject); + $this->validator->setOperationObject($operationObject); $this->validator->validateRequest($request); } } diff --git a/src/Request/RequestValidator.php b/src/Request/RequestValidator.php index 149d697..0eae55a 100644 --- a/src/Request/RequestValidator.php +++ b/src/Request/RequestValidator.php @@ -8,6 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Request; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\Exception\UnsupportedException; use Symfony\Component\HttpFoundation\Request; use JsonSchema\Validator; @@ -19,26 +20,28 @@ class RequestValidator { /** - * @var array + * @var OperationObject */ - private $operationDefinition = []; + private $operationObject; /** - * @param array $operationDefinition + * @param OperationObject $operationObject */ - public function __construct($operationDefinition = []) + public function __construct(OperationObject $operationObject = null) { - $this->operationDefinition = $operationDefinition; + if ($operationObject) { + $this->setOperationObject($operationObject); + } } /** - * @param array $operationDefinition + * @param OperationObject $operationObject * * @return $this */ - public function setOperationDefinition($operationDefinition) + public function setOperationObject(OperationObject $operationObject) { - $this->operationDefinition = $operationDefinition; + $this->operationObject = $operationObject; return $this; } @@ -55,52 +58,18 @@ public function validateRequest(Request $request) $validator->check( $this->assembleParameterDataForValidation($request), - $this->assembleRequestSchema() + $this->operationObject->getRequestSchema() ); if (!$validator->isValid()) { - /** - * TODO Better utilize $validator->getErrors() so we can assemble a more helpful vnd.error response - * @see https://github.com/kleijnweb/swagger-bundle/issues/27 - */ throw new InvalidParametersException( "Parameters incompatible with operation schema: " . implode(', ', $validator->getErrors()[0]), - 400 + $validator->getErrors() ); } } - /** - * @return object - */ - private function assembleRequestSchema() - { - if (!isset($this->operationDefinition['parameters'])) { - return new \stdClass; - } - $schema = new \stdClass; - $schema->type = 'object'; - $schema->required = []; - $schema->properties = new \stdClass; - - foreach ($this->operationDefinition['parameters'] as $paramDefinition) { - if (isset($paramDefinition['required']) && $paramDefinition['required']) { - $schema->required[] = $paramDefinition['name']; - } - - if ($paramDefinition['in'] === 'body') { - $schema->properties->{$paramDefinition['name']} = $this->arrayToObject($paramDefinition['schema']); - continue; - } - $propertySchema = ['type' => $paramDefinition['type']]; - - $schema->properties->{$paramDefinition['name']} = $this->arrayToObject($propertySchema); - } - - return $schema; - } - /** * @param Request $request * @@ -109,10 +78,6 @@ private function assembleRequestSchema() */ private function assembleParameterDataForValidation(Request $request) { - if (!isset($this->operationDefinition['parameters'])) { - return new \stdClass; - } - /** * TODO Hack * @see https://github.com/kleijnweb/swagger-bundle/issues/24 @@ -126,24 +91,13 @@ private function assembleParameterDataForValidation(Request $request) $parameters = new \stdClass; - $paramBagMapping = [ - 'query' => 'query', - 'path' => 'attributes', - 'body' => 'attributes', - 'header' => 'headers' - ]; - foreach ($this->operationDefinition['parameters'] as $paramDefinition) { - $paramName = $paramDefinition['name']; + foreach ($this->operationObject->getDefinition()->parameters as $paramDefinition) { + $paramName = $paramDefinition->name; - if (!isset($paramBagMapping[$paramDefinition['in']])) { - throw new UnsupportedException( - "Unsupported parameter 'in' value in definition '{$paramDefinition['in']}'" - ); - } if (!$request->attributes->has($paramName)) { continue; } - if ($paramDefinition['in'] === 'body' && $content !== null) { + if ($paramDefinition->in === 'body' && $content !== null) { $parameters->$paramName = $content; continue; } @@ -153,34 +107,15 @@ private function assembleParameterDataForValidation(Request $request) * If value already coerced into \DateTime object, use any non-empty value for validation */ if ($parameters->$paramName instanceof \DateTime) { - if (isset($paramDefinition['format'])) { - if ($paramDefinition['format'] === 'date') { - $parameters->$paramName = '1970-01-01'; - } - if ($paramDefinition['format'] === 'date-time') { - $parameters->$paramName = '1970-01-01T00:00:00Z'; - } + if ($paramDefinition->format === 'date') { + $parameters->$paramName = '1970-01-01'; + } + if ($paramDefinition->format === 'date-time') { + $parameters->$paramName = '1970-01-01T00:00:00Z'; } } } return $parameters; } - - /** - * @see https://github.com/kleijnweb/swagger-bundle/issues/29 - * - * @param array $data - * - * @return object - */ - private static function arrayToObject(array $data) - { - $object = new \stdClass; - foreach ($data as $key => $value) { - $object->$key = is_array($value) ? self::arrayToObject($value) : $value; - } - - return $object; - } } diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml index e522200..5a02494 100644 --- a/src/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -1,66 +1,73 @@ services: - swagger.route_loader: - class: KleijnWeb\SwaggerBundle\Routing\SwaggerRouteLoader - arguments: ['@swagger.document.repository'] - tags: - - { name: routing.loader } - - swagger.serializer: - class: KleijnWeb\SwaggerBundle\Serializer\SerializerAdapter - arguments: ['@swagger.serializer.target'] - - swagger.serializer.array: - class: KleijnWeb\SwaggerBundle\Serializer\ArraySerializer - - swagger.serializer.jms: - class: JMS\Serializer\Serializer\Serializer - factory: [KleijnWeb\SwaggerBundle\Serializer\JmsSerializerFactory, factory] - - swagger.serializer.symfony: - class: JMS\Serializer\Serializer\SymfonySerializerFactory - factory: [KleijnWeb\SwaggerBundle\Serializer\SymfonySerializerFactory, factory] - - swagger.serializer.type_resolver: - class: KleijnWeb\SwaggerBundle\Serializer\SerializationTypeResolver - arguments: [%swagger.serializer.namespace%] - - swagger.document.repository: - class: KleijnWeb\SwaggerBundle\Document\DocumentRepository - arguments: [%swagger.document.base_path%] - - swagger.response.factory: - class: KleijnWeb\SwaggerBundle\Response\ResponseFactory - arguments: ['@swagger.document.repository', '@swagger.serializer'] - - swagger.request.processor: - class: KleijnWeb\SwaggerBundle\Request\RequestProcessor - arguments: ['@swagger.request.processor.validator', '@swagger.request.processor.coercer'] - - swagger.request.processor.validator: - class: KleijnWeb\SwaggerBundle\Request\RequestValidator - - swagger.request.processor.coercer: - class: KleijnWeb\SwaggerBundle\Request\RequestCoercer - arguments: ['@swagger.request.processor.content_decoder'] - - swagger.request.processor.content_decoder: - class: KleijnWeb\SwaggerBundle\Request\ContentDecoder - arguments: ['@swagger.serializer', '@swagger.serializer.type_resolver'] - - kernel.listener.swagger.view: - class: KleijnWeb\SwaggerBundle\EventListener\ViewListener - arguments: ['@swagger.response.factory'] - tags: - - { name: kernel.event_listener, event: kernel.view, method: onKernelView } - - kernel.listener.swagger.exception: - class: KleijnWeb\SwaggerBundle\EventListener\ExceptionListener - arguments: ['@logger'] - tags: - - { name: kernel.event_listener, event: kernel.exception, method: onKernelException } - - kernel.listener.swagger.request: - class: KleijnWeb\SwaggerBundle\EventListener\RequestListener - arguments: ['@swagger.document.repository', '@swagger.request.processor'] - tags: - - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } + swagger.route_loader: + class: KleijnWeb\SwaggerBundle\Routing\SwaggerRouteLoader + arguments: ['@swagger.document.repository'] + tags: + - { name: routing.loader } + + swagger.serializer: + class: KleijnWeb\SwaggerBundle\Serializer\SerializerAdapter + arguments: ['@swagger.serializer.target'] + + swagger.serializer.array: + class: KleijnWeb\SwaggerBundle\Serializer\ArraySerializer + + swagger.serializer.jms: + class: JMS\Serializer\Serializer\Serializer + factory: [KleijnWeb\SwaggerBundle\Serializer\JmsSerializerFactory, factory] + + swagger.serializer.symfony: + class: JMS\Serializer\Serializer\SymfonySerializerFactory + factory: [KleijnWeb\SwaggerBundle\Serializer\SymfonySerializerFactory, factory] + + swagger.serializer.type_resolver: + class: KleijnWeb\SwaggerBundle\Serializer\SerializationTypeResolver + arguments: [%swagger.serializer.namespace%] + + swagger.document.repository: + class: KleijnWeb\SwaggerBundle\Document\DocumentRepository + arguments: [%swagger.document.base_path%] + + swagger.response.factory: + class: KleijnWeb\SwaggerBundle\Response\ResponseFactory + arguments: ['@swagger.document.repository', '@swagger.serializer'] + + swagger.response.vnd_validation_error_factory: + class: KleijnWeb\SwaggerBundle\Response\VndValidationErrorFactory + arguments: ['@swagger.document.parameter_ref_builder'] + + swagger.document.parameter_ref_builder: + class: KleijnWeb\SwaggerBundle\Document\ParameterRefBuilder + + swagger.request.processor: + class: KleijnWeb\SwaggerBundle\Request\RequestProcessor + arguments: ['@swagger.request.processor.validator', '@swagger.request.processor.coercer'] + + swagger.request.processor.validator: + class: KleijnWeb\SwaggerBundle\Request\RequestValidator + + swagger.request.processor.coercer: + class: KleijnWeb\SwaggerBundle\Request\RequestCoercer + arguments: ['@swagger.request.processor.content_decoder'] + + swagger.request.processor.content_decoder: + class: KleijnWeb\SwaggerBundle\Request\ContentDecoder + arguments: ['@swagger.serializer', '@swagger.serializer.type_resolver'] + + kernel.listener.swagger.view: + class: KleijnWeb\SwaggerBundle\EventListener\ViewListener + arguments: ['@swagger.response.factory'] + tags: + - { name: kernel.event_listener, event: kernel.view, method: onKernelView } + + kernel.listener.swagger.vnd_error_exception: + class: KleijnWeb\SwaggerBundle\EventListener\VndErrorExceptionListener + arguments: ['@swagger.response.vnd_validation_error_factory', '@logger'] + tags: + - { name: kernel.event_listener, event: kernel.exception, method: onKernelException } + + kernel.listener.swagger.request: + class: KleijnWeb\SwaggerBundle\EventListener\RequestListener + arguments: ['@swagger.document.repository', '@swagger.request.processor'] + tags: + - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } diff --git a/src/Resources/config/services_dev.yml b/src/Resources/config/services_dev.yml deleted file mode 100644 index ac2f3fb..0000000 --- a/src/Resources/config/services_dev.yml +++ /dev/null @@ -1,18 +0,0 @@ -services: - swagger.dev.resource_generator: - class: KleijnWeb\SwaggerBundle\Dev\Generator\ResourceGenerator - - swagger.dev.document_fixer.swagger_bundle_response: - class: KleijnWeb\SwaggerBundle\Dev\DocumentFixer\Fixers\SwaggerBundleResponseFixer - - swagger.dev.command.amend_swagger: - class: KleijnWeb\SwaggerBundle\Dev\Command\AmendSwaggerDocumentCommand - arguments: ['@swagger.document.repository', '@swagger.dev.document_fixer.swagger_bundle_response'] - tags: - - { name: console.command } - - swagger.dev.command.generate_resources: - class: KleijnWeb\SwaggerBundle\Dev\Command\GenerateResourceClassesCommand - arguments: ['@swagger.document.repository', '@swagger.dev.resource_generator'] - tags: - - { name: console.command } \ No newline at end of file diff --git a/src/Response/ResponseFactory.php b/src/Response/ResponseFactory.php index 45142a3..f4c09a6 100644 --- a/src/Response/ResponseFactory.php +++ b/src/Response/ResponseFactory.php @@ -48,13 +48,41 @@ public function __construct(DocumentRepository $documentRepository, SerializerAd */ public function createResponse(Request $request, $data) { - $headers = ['Content-Type' => 'application/json']; + if (!$request->get('_definition')) { + throw new \LogicException("Request does not contain reference to definition"); + } + if (!$request->get('_swagger_path')) { + throw new \LogicException("Request does not contain reference to Swagger path"); + } + + if ($data !== null) { + $data = $this->serializer->serialize($data, 'json'); + } + + $swaggerDocument = $this->documentRepository->get($request->get('_definition')); + + $operationDefinition = $swaggerDocument + ->getOperationDefinition( + $request->get('_swagger_path'), + $request->getMethod() + ); + + $responseCode = 200; + $understands204 = false; + foreach (array_keys((array)$operationDefinition->responses) as $statusCode) { + if ($statusCode == 204) { + $understands204 = true; + } + if (2 == substr($statusCode, 0, 1)) { + $responseCode = $statusCode; + break; + } + } - if ($data === null) { - return new Response('', 204, $headers); + if ($data === null && $understands204) { + $responseCode = 204; } - $data = $this->serializer->serialize($data, 'json'); - return new Response($data, 200, $headers); + return new Response($data, $responseCode, ['Content-Type' => 'application/json']); } } diff --git a/src/Response/VndErrorResponse.php b/src/Response/VndErrorResponse.php index 839cbc3..52dc6bc 100644 --- a/src/Response/VndErrorResponse.php +++ b/src/Response/VndErrorResponse.php @@ -8,6 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Response; +use Ramsey\VndError\VndError; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -19,19 +20,13 @@ class VndErrorResponse extends JsonResponse const DEFAULT_STATUS = Response::HTTP_INTERNAL_SERVER_ERROR; /** - * @param string $message - * @param int $status - * @param null $logref - * @param array $headers + * @param VndError $vndError + * @param int $status + * @param array $headers */ - public function __construct($message, $status = self::DEFAULT_STATUS, $logref = null, $headers = []) + public function __construct(VndError $vndError, $status = self::DEFAULT_STATUS, array $headers = []) { - $data = ['message' => $message]; - if (null !== $logref) { - $data['logref'] = $logref; - } $headers = array_merge(['Content-Type' => 'application/vnd.error+json'], $headers); - parent::__construct($data, $status, $headers); - + parent::__construct($vndError->asJson(false, false), $status, $headers); } } diff --git a/src/Response/VndValidationErrorFactory.php b/src/Response/VndValidationErrorFactory.php new file mode 100644 index 0000000..e98b715 --- /dev/null +++ b/src/Response/VndValidationErrorFactory.php @@ -0,0 +1,71 @@ + + */ +class VndValidationErrorFactory +{ + const DEFAULT_MESSAGE = 'Input Validation Failure'; + + /** + * @var ParameterRefBuilder + */ + private $refBuilder; + + /** + * @param ParameterRefBuilder $refBuilder + */ + public function __construct(ParameterRefBuilder $refBuilder) + { + $this->refBuilder = $refBuilder; + } + + /** + * @param Request $request + * @param InvalidParametersException $exception + * @param string|null $logRef + * + * @return VndError + */ + public function create(Request $request, InvalidParametersException $exception, $logRef = null) + { + $error = new VndError(self::DEFAULT_MESSAGE, $logRef); + $error->addLink('about', $this->refBuilder->buildDocumentLink($request), ['title' => 'Api Specification']); + $error->setUri($request->getUri()); + + foreach ($exception->getValidationErrors() as $errorSpec) { + // For older versions, try to extract the property name from the message + if (!$errorSpec['property']) { + $errorSpec['property'] = preg_replace('/the property (.*) is required/', '\\1', $errorSpec['message']); + } + $normalizedPropertyName = preg_replace('/\[\d+\]/', '', $errorSpec['property']); + $data = [ + 'message' => $errorSpec['message'], + 'path' => $this->refBuilder->createParameterSchemaPointer($request, $normalizedPropertyName) + ]; + $parameterDefinitionUri = $this->refBuilder->buildSpecificationLink($request, $normalizedPropertyName); + + $validationError = new Hal($parameterDefinitionUri, $data); + $error->addResource( + 'errors', + $validationError + ); + } + + return $error; + } +} diff --git a/src/Routing/SwaggerRouteLoader.php b/src/Routing/SwaggerRouteLoader.php index 425b995..85d8206 100644 --- a/src/Routing/SwaggerRouteLoader.php +++ b/src/Routing/SwaggerRouteLoader.php @@ -12,7 +12,6 @@ use Symfony\Component\Config\Loader\Loader; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\Yaml\Yaml; /** * @author John Kleijn @@ -64,10 +63,10 @@ public function load($resource, $type = null) foreach ($methods as $methodName => $operationSpec) { $operationName = $methodName; $controllerKey = "swagger.controller.$resourceName:$operationName"; - if (isset($operationSpec['operationId'])) { - $operationName = $operationSpec['operationId']; - if (false !== strpos($operationSpec['operationId'], ':')) { - $controllerKey = $operationSpec['operationId']; + if (isset($operationSpec->operationId)) { + $operationName = $operationSpec->operationId; + if (false !== strpos($operationSpec->operationId, ':')) { + $controllerKey = $operationSpec->operationId; } else { $controllerKey = "swagger.controller.$resourceName:$operationName"; } @@ -82,22 +81,22 @@ public function load($resource, $type = null) $requirements = []; $operationDefinition = $document->getOperationDefinition($path, $methodName); - - if (isset($operationDefinition['parameters'])) { - foreach ($operationDefinition['parameters'] as $paramDefinition) { - if ($paramDefinition['in'] === 'path' && isset($paramDefinition['type'])) { - switch ($paramDefinition['type']) { + + if (isset($operationDefinition->parameters)) { + foreach ($operationDefinition->parameters as $paramDefinition) { + if ($paramDefinition->in === 'path' && isset($paramDefinition->type)) { + switch ($paramDefinition->type) { case 'integer': - $requirements[$paramDefinition['name']] = '\d+'; + $requirements[$paramDefinition->name] = '\d+'; break; case 'string': - if (isset($paramDefinition['pattern'])) { - $requirements[$paramDefinition['name']] = $paramDefinition['pattern']; + if (isset($paramDefinition->pattern)) { + $requirements[$paramDefinition->name] = $paramDefinition->pattern; break; } - if (isset($paramDefinition['enum'])) { - $requirements[$paramDefinition['name']] = '(' . - implode('|', $paramDefinition['enum']) + if (isset($paramDefinition->enum)) { + $requirements[$paramDefinition->name] = '(' . + implode('|', $paramDefinition->enum) . ')'; break; } diff --git a/src/Serializer/ArraySerializer.php b/src/Serializer/ArraySerializer.php index 38a070e..05747fc 100644 --- a/src/Serializer/ArraySerializer.php +++ b/src/Serializer/ArraySerializer.php @@ -16,11 +16,11 @@ class ArraySerializer { /** - * @param array $data + * @param mixed $data * * @return string */ - public function serialize(array $data) + public function serialize($data) { return json_encode($data); } diff --git a/src/Serializer/SerializationTypeResolver.php b/src/Serializer/SerializationTypeResolver.php index b644644..2d93a24 100644 --- a/src/Serializer/SerializationTypeResolver.php +++ b/src/Serializer/SerializationTypeResolver.php @@ -7,6 +7,8 @@ */ namespace KleijnWeb\SwaggerBundle\Serializer; +use KleijnWeb\SwaggerBundle\Document\OperationObject; + class SerializationTypeResolver { /** @@ -23,19 +25,21 @@ public function __construct($resourceNamespace = null) } /** - * @param array $definitionFragment + * @param OperationObject $operationObject * * @return null|string */ - public function resolve(array $definitionFragment) + public function resolve(OperationObject $operationObject) { - if (isset($definitionFragment['parameters'])) { - foreach ($definitionFragment['parameters'] as $parameterDefinition) { - if ($parameterDefinition['in'] == 'body' && isset($parameterDefinition['schema'])) { - return $this->resolveUsingSchema($parameterDefinition['schema']); + if ($operationObject->hasParameters()) { + foreach ($operationObject->getParameters() as $parameterDefinition) { + if ($parameterDefinition->in == 'body' && isset($parameterDefinition->schema)) { + return $this->resolveUsingSchema($parameterDefinition->schema); } } } + + return null; } /** @@ -49,13 +53,13 @@ public function qualify($typeName) } /** - * @param array $schema + * @param object $schema * * @return string */ - public function resolveUsingSchema(array $schema) + public function resolveUsingSchema($schema) { - $reference = isset($schema['$ref']) ? $schema['$ref'] : (isset($schema['id']) ? $schema['id'] : null); + $reference = isset($schema->{'$ref'}) ? $schema->{'$ref'} : (isset($schema->id) ? $schema->id : null); if (!$reference) { return null; diff --git a/src/Dev/Test/ApiRequest.php b/src/Test/ApiRequest.php similarity index 97% rename from src/Dev/Test/ApiRequest.php rename to src/Test/ApiRequest.php index a118b56..d217bdd 100644 --- a/src/Dev/Test/ApiRequest.php +++ b/src/Test/ApiRequest.php @@ -6,7 +6,7 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Dev\Test; +namespace KleijnWeb\SwaggerBundle\Test; use Symfony\Component\BrowserKit\Request; diff --git a/src/Test/ApiResponseErrorException.php b/src/Test/ApiResponseErrorException.php new file mode 100644 index 0000000..b14a1e5 --- /dev/null +++ b/src/Test/ApiResponseErrorException.php @@ -0,0 +1,62 @@ + + */ +class ApiResponseErrorException extends \Exception +{ + /** + * @var object + */ + private $data; + + /** + * @var string + */ + private $json; + + /** + * @param string $json + * @param object $data + * @param int $httpStatusCode + */ + public function __construct($json, $data, $httpStatusCode) + { + $this->message = "Returned $httpStatusCode"; + if ($data) { + $this->message = $data->message; + if (isset($data->logref)) { + $this->message = "$data->message [logref $data->logref]"; + } + + } + + $this->code = $httpStatusCode; + $this->data = $data; + $this->json = $json; + } + + /** + * @return string + */ + public function getJson() + { + return $this->json; + } + + /** + * @return object + */ + public function getData() + { + return $this->data; + } +} diff --git a/src/Dev/Test/ApiTestCase.php b/src/Test/ApiTestCase.php similarity index 85% rename from src/Dev/Test/ApiTestCase.php rename to src/Test/ApiTestCase.php index ac954b4..0494b08 100644 --- a/src/Dev/Test/ApiTestCase.php +++ b/src/Test/ApiTestCase.php @@ -6,16 +6,16 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Dev\Test; +namespace KleijnWeb\SwaggerBundle\Test; use FR3D\SwaggerAssertions\PhpUnit\AssertsTrait; use FR3D\SwaggerAssertions\SchemaManager; use JsonSchema\Validator; +use KleijnWeb\SwaggerBundle\Document\DocumentRepository; use KleijnWeb\SwaggerBundle\Document\SwaggerDocument; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamDirectory; use org\bovigo\vfs\vfsStreamWrapper; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Yaml\Yaml; @@ -56,7 +56,7 @@ public static function initSchemaManager($swaggerPath) $validator = new Validator(); $validator->check( json_decode(json_encode(Yaml::parse(file_get_contents($swaggerPath)))), - json_decode(file_get_contents(__DIR__ . '/../../../assets/swagger-schema.json')) + json_decode(file_get_contents(__DIR__ . '/../../assets/swagger-schema.json')) ); if (!$validator->isValid()) { @@ -74,7 +74,8 @@ public static function initSchemaManager($swaggerPath) ); self::$schemaManager = new SchemaManager(vfsStream::url('root') . '/swagger.json'); - self::$document = new SwaggerDocument($swaggerPath); + $repository = new DocumentRepository(dirname($swaggerPath)); + self::$document = $repository->get(basename($swaggerPath)); } /** @@ -201,11 +202,11 @@ private function getJsonForLastRequest($fullPath, $method) { $method = strtolower($method); $response = $this->client->getResponse(); - $responseContent = $response->getContent(); - $data = json_decode($responseContent); + $json = $response->getContent(); + $data = json_decode($json); if ($response->getStatusCode() !== 204) { - static $ERRORS = [ + static $errors = [ JSON_ERROR_NONE => 'No error', JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)', @@ -214,11 +215,11 @@ private function getJsonForLastRequest($fullPath, $method) JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded' ]; $error = json_last_error(); - $jsonErrorMessage = isset($ERRORS[$error]) ? $ERRORS[$error] : 'Unknown error'; + $jsonErrorMessage = isset($errors[$error]) ? $errors[$error] : 'Unknown error'; $this->assertSame( JSON_ERROR_NONE, json_last_error(), - "Not valid JSON: " . $jsonErrorMessage . "(" . var_export($responseContent, true) . ")" + "Not valid JSON: " . $jsonErrorMessage . "(" . var_export($json, true) . ")" ); } @@ -227,7 +228,7 @@ private function getJsonForLastRequest($fullPath, $method) $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data); } // This throws an exception so that tests can catch it when it is expected - throw new ApiResponseErrorException($data, $response->getStatusCode()); + throw new ApiResponseErrorException($json, $data, $response->getStatusCode()); } $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data); @@ -246,6 +247,10 @@ private function validateResponse($code, $response, $method, $fullPath, $data) { $request = $this->client->getRequest(); if (!self::$schemaManager->hasPath(['paths', $request->get('_swagger_path'), $method, 'responses', $code])) { + $statusClass = (int)substr((string)$code, 0, 1); + if (in_array($statusClass, [4, 5])) { + return; + } throw new \UnexpectedValueException( "There is no $code response definition for {$request->get('_swagger_path')}:$method. " ); @@ -255,7 +260,14 @@ private function validateResponse($code, $response, $method, $fullPath, $data) foreach ($response->headers->all() as $key => $values) { $headers[str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)))] = $values[0]; } - $this->assertResponseHeadersMatch($headers, self::$schemaManager, $fullPath, $method, $code); - $this->assertResponseBodyMatch($data, self::$schemaManager, $fullPath, $method, $code); + try { + $this->assertResponseHeadersMatch($headers, self::$schemaManager, $fullPath, $method, $code); + $this->assertResponseBodyMatch($data, self::$schemaManager, $fullPath, $method, $code); + } catch (\UnexpectedValueException $e) { + $statusClass = (int)(string)$code[0]; + if (in_array($statusClass, [4, 5])) { + return; + } + } } } diff --git a/src/Dev/Test/ApiTestClient.php b/src/Test/ApiTestClient.php similarity index 95% rename from src/Dev/Test/ApiTestClient.php rename to src/Test/ApiTestClient.php index b68b408..57ce153 100644 --- a/src/Dev/Test/ApiTestClient.php +++ b/src/Test/ApiTestClient.php @@ -6,7 +6,7 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Dev\Test; +namespace KleijnWeb\SwaggerBundle\Test; use Symfony\Bundle\FrameworkBundle\Client; use Symfony\Component\BrowserKit\Request; diff --git a/src/Tests/Dev/Command/AmendSwaggerDocumentCommandTest.php b/src/Tests/Dev/Command/AmendSwaggerDocumentCommandTest.php deleted file mode 100644 index 8067163..0000000 --- a/src/Tests/Dev/Command/AmendSwaggerDocumentCommandTest.php +++ /dev/null @@ -1,68 +0,0 @@ - - */ -class AmendSwaggerDocumentCommandTest extends \PHPUnit_Framework_TestCase -{ - /** - * @var CommandTester - */ - private $commandTester; - - /** - * Set up the command tester - */ - protected function setUp() - { - $application = new Application(); - $application->add(new AmendSwaggerDocumentCommand(new DocumentRepository(), new SwaggerBundleResponseFixer())); - - $command = $application->find(AmendSwaggerDocumentCommand::NAME); - $this->commandTester = new CommandTester($command); - } - - /** - * @test - */ - public function willAddResponsesToDocument() - { - $minimalDocumentPath = __DIR__ . '/../DocumentFixer/assets/minimal.yml'; - vfsStreamWrapper::register(); - vfsStreamWrapper::setRoot(new vfsStreamDirectory('willAddResponsesToDocument')); - - $amendedPath = vfsStream::url('willAddResponsesToDocument/modified.yml'); - $this->commandTester->execute( - [ - 'command' => AmendSwaggerDocumentCommand::NAME, - 'file' => $minimalDocumentPath, - '--out' => $amendedPath - ] - ); - - $modifiedContent = file_get_contents($amendedPath); - $this->assertContains('responses', $modifiedContent); - - $amendedData = Yaml::parse($modifiedContent); - - $this->assertArrayHasKey('responses', $amendedData); - } -} diff --git a/src/Tests/Dev/Command/GenerateResourceClassesCommandTest.php b/src/Tests/Dev/Command/GenerateResourceClassesCommandTest.php deleted file mode 100644 index c2c708e..0000000 --- a/src/Tests/Dev/Command/GenerateResourceClassesCommandTest.php +++ /dev/null @@ -1,74 +0,0 @@ - - */ -class GenerateResourceClassesCommandTest extends KernelTestCase -{ - /** - * @var CommandTester - */ - private $commandTester; - - /** - * Set up the command tester - */ - protected function setUp() - { - self::bootKernel(); - $application = new Application(self::$kernel); - - $application->add( - new GenerateResourceClassesCommand(new DocumentRepository(), new ResourceGenerator()) - ); - - $command = $application->find(GenerateResourceClassesCommand::NAME); - $this->commandTester = new CommandTester($command); - } - - /** - * @test - */ - public function canExecute() - { - $petStoreDocumentPath = __DIR__ . '/../../Functional/PetStore/app/swagger/petstore.yml'; - vfsStreamWrapper::register(); - vfsStreamWrapper::setRoot(new vfsStreamDirectory('willAddResponsesToDocument')); - - $namespace = 'GenerateResourceClassesCommandTest'; - $this->commandTester->execute( - [ - 'command' => GenerateResourceClassesCommand::NAME, - 'file' => $petStoreDocumentPath, - 'bundle' => 'PetStoreBundle', - '--namespace' => $namespace - ] - ); - $bundle = self::$kernel->getBundle('PetStoreBundle'); - $filePathName = $bundle->getPath() . '/GenerateResourceClassesCommandTest/Pet.php'; - - $this->assertTrue( - file_exists($filePathName), - sprintf('%s has not been generated', $filePathName) - ); - $content = file_get_contents($filePathName); - $this->assertContains("namespace {$bundle->getNamespace()}\\GenerateResourceClassesCommandTest;", $content); - } -} diff --git a/src/Tests/Dev/DocumentFixer/SwaggerBundleResponseFixerTest.php b/src/Tests/Dev/DocumentFixer/SwaggerBundleResponseFixerTest.php deleted file mode 100644 index f0654b6..0000000 --- a/src/Tests/Dev/DocumentFixer/SwaggerBundleResponseFixerTest.php +++ /dev/null @@ -1,101 +0,0 @@ - - */ -class SwaggerBundleResponseFixerTest extends \PHPUnit_Framework_TestCase -{ - /** - * @test - */ - public function willAddVndErrorSchema() - { - $fixer = new SwaggerBundleResponseFixer(); - $document = new SwaggerDocument(__DIR__ . '/assets/minimal.yml'); - $fixer->fix($document); - - $definition = $document->getDefinition(); - $this->assertArrayHasKey('definitions', $definition); - $this->assertArrayHasKey('VndError', $definition['definitions']); - $this->assertArrayHasKey('type', $definition['definitions']['VndError']); - $this->assertArrayHasKey('required', $definition['definitions']['VndError']); - $this->assertArrayHasKey('properties', $definition['definitions']['VndError']); - } - - /** - * @test - */ - public function willAddServerErrorResponse() - { - $fixer = new SwaggerBundleResponseFixer(); - $document = new SwaggerDocument(__DIR__ . '/assets/minimal.yml'); - $fixer->fix($document); - - $definition = $document->getDefinition(); - $this->assertArrayHasKey('responses', $definition); - $this->assertArrayHasKey('ServerError', $definition['responses']); - $this->assertArrayHasKey('description', $definition['responses']['ServerError']); - $this->assertArrayHasKey('schema', $definition['responses']['ServerError']); - $this->assertArrayHasKey('$ref', $definition['responses']['ServerError']['schema']); - $this->assertSame($definition['responses']['ServerError']['schema']['$ref'], '#/definitions/VndError'); - } - - /** - * @test - */ - public function willAddServerErrorResponseToOperations() - { - $fixer = new SwaggerBundleResponseFixer(); - $document = new SwaggerDocument(__DIR__ . '/assets/minimal.yml'); - $fixer->fix($document); - - $operationDefinition = $document->getOperationDefinition('/', 'get'); - $responses = $operationDefinition['responses']; - $this->assertArrayHasKey('500', $responses); - $this->assertSame($responses['500']['schema']['$ref'], '#/responses/ServerError'); - } - - /** - * @test - */ - public function willAddInputErrorResponse() - { - $fixer = new SwaggerBundleResponseFixer(); - $document = new SwaggerDocument(__DIR__ . '/assets/minimal.yml'); - $fixer->fix($document); - - $definition = $document->getDefinition(); - $this->assertArrayHasKey('responses', $definition); - $this->assertArrayHasKey('InputError', $definition['responses']); - $this->assertArrayHasKey('description', $definition['responses']['InputError']); - $this->assertArrayHasKey('schema', $definition['responses']['InputError']); - $this->assertArrayHasKey('$ref', $definition['responses']['InputError']['schema']); - $this->assertSame($definition['responses']['InputError']['schema']['$ref'], '#/definitions/VndError'); - } - - /** - * @test - */ - public function willAddInputErrorResponseToOperations() - { - $fixer = new SwaggerBundleResponseFixer(); - $document = new SwaggerDocument(__DIR__ . '/assets/minimal.yml'); - $fixer->fix($document); - - $operationDefinition = $document->getOperationDefinition('/', 'get'); - $responses = $operationDefinition['responses']; - $this->assertArrayHasKey('400', $responses); - $this->assertSame($responses['400']['schema']['$ref'], '#/responses/InputError'); - } -} diff --git a/src/Tests/Dev/DocumentFixer/assets/minimal.yml b/src/Tests/Dev/DocumentFixer/assets/minimal.yml deleted file mode 100644 index e7f0202..0000000 --- a/src/Tests/Dev/DocumentFixer/assets/minimal.yml +++ /dev/null @@ -1,10 +0,0 @@ -swagger: '2.0' -info: - version: 0.0.0 - title: Simple API -paths: - /: - get: - responses: - '200': - description: OK diff --git a/src/Tests/Dev/Generator/ResourceGeneratorJmsSerializerCompatibilityTest.php b/src/Tests/Dev/Generator/ResourceGeneratorJmsSerializerCompatibilityTest.php deleted file mode 100644 index 9db0368..0000000 --- a/src/Tests/Dev/Generator/ResourceGeneratorJmsSerializerCompatibilityTest.php +++ /dev/null @@ -1,106 +0,0 @@ - - */ -class ResourceGeneratorJmsSerializerCompatibilityTest extends \PHPUnit_Framework_TestCase -{ - protected function setUp() - { - $bundle = new PetStoreBundle(); - $document = SwaggerDocumentTest::getPetStoreDocument(); - $generator = new ResourceGenerator(); - $generator->setSkeletonDirs('src/Dev/Resources/skeleton'); - $generator->generate($bundle, $document, 'Model\Jms'); - - require_once $bundle->getPath() . '/Model/Jms/Pet.php'; - require_once $bundle->getPath() . '/Model/Jms/Tag.php'; - require_once $bundle->getPath() . '/Model/Jms/Category.php'; - } - - /** - * @test - */ - public function canSerializeAPet() - { - $pet = new \KleijnWeb\SwaggerBundle\Tests\Functional\PetStore\Model\Jms\Pet(); - $pet - ->setId(1234567) - ->setName('doggie') - ->setPhotourls(['/a/b/c', '/d/e/f']) - ->setTags([ - (new \KleijnWeb\SwaggerBundle\Tests\Functional\PetStore\Model\Jms\Tag())->setName('purebreeds'), - (new \KleijnWeb\SwaggerBundle\Tests\Functional\PetStore\Model\Jms\Tag())->setName('puppies') - ]) - ->setCategory( - (new \KleijnWeb\SwaggerBundle\Tests\Functional\PetStore\Model\Jms\Category())->setName('Dogs') - ); - - $serializer = SerializerBuilder::create()->build(); - $actual = json_decode($serializer->serialize($pet, 'json'), true); - $expected = [ - 'id' => 1234567, - 'category' => ['name' => 'Dogs'], - 'name' => 'doggie', - 'photo_urls' => ['/a/b/c', '/d/e/f'], - 'tags' => [ - ['name' => 'purebreeds'], - ['name' => 'puppies'], - ] - - ]; - $this->assertSame($expected, $actual); - } - - /** - * @test - */ - public function canDeserializeAPet() - { - $data = [ - 'id' => 1234567, - 'category' => ['name' => 'Dogs'], - 'name' => 'doggie', - 'photo_urls' => ['/a/b/c', '/d/e/f'], - 'tags' => [ - ['name' => 'purebreeds'], - ['name' => 'puppies'], - ] - ]; - - $serializer = SerializerBuilder::create()->build(); - $actual = $serializer->deserialize( - json_encode($data), - 'KleijnWeb\SwaggerBundle\Tests\Functional\PetStore\Model\Jms\Pet', - 'json' - ); - - $expected = new \KleijnWeb\SwaggerBundle\Tests\Functional\PetStore\Model\Jms\Pet(); - $expected - ->setId(1234567) - ->setName('doggie') - ->setPhotourls(['/a/b/c', '/d/e/f']) - ->setTags([ - (new \KleijnWeb\SwaggerBundle\Tests\Functional\PetStore\Model\Jms\Tag())->setName('purebreeds'), - (new \KleijnWeb\SwaggerBundle\Tests\Functional\PetStore\Model\Jms\Tag())->setName('puppies') - ]) - ->setCategory( - (new \KleijnWeb\SwaggerBundle\Tests\Functional\PetStore\Model\Jms\Category())->setName('Dogs') - ); - - $this->assertEquals($expected, $actual); - } -} diff --git a/src/Tests/Dev/Generator/ResourceGeneratorTest.php b/src/Tests/Dev/Generator/ResourceGeneratorTest.php deleted file mode 100644 index 0e5467a..0000000 --- a/src/Tests/Dev/Generator/ResourceGeneratorTest.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ -class ResourceGeneratorTest extends \PHPUnit_Framework_TestCase -{ - /** - * @test - */ - public function canRenderResourcesFromPetStore() - { - $bundle = new PetStoreBundle(); - $document = SwaggerDocumentTest::getPetStoreDocument(); - $generator = new ResourceGenerator(); - $generator->setSkeletonDirs('src/Dev/Resources/skeleton'); - $generator->generate($bundle, $document, 'Foo\Bar'); - $files = [ - 'User.php', - 'Category.php', - 'Pet.php', - 'Order.php', - ]; - - foreach ($files as $file) { - $filePathName = $bundle->getPath() . '/Foo/Bar/' . $file; - $this->assertTrue( - file_exists($filePathName), - sprintf('%s has not been generated', $filePathName) - ); - $content = file_get_contents($filePathName); - $this->assertContains("namespace {$bundle->getNamespace()}\\Foo\\Bar;", $content); - } - } -} diff --git a/src/Tests/Document/DocumentRepositoryTest.php b/src/Tests/Document/DocumentRepositoryTest.php index b19598c..779465a 100644 --- a/src/Tests/Document/DocumentRepositoryTest.php +++ b/src/Tests/Document/DocumentRepositoryTest.php @@ -6,7 +6,7 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Tests\Dev\Document; +namespace KleijnWeb\SwaggerBundle\Tests\Document; use KleijnWeb\SwaggerBundle\Document\DocumentRepository; @@ -15,14 +15,32 @@ */ class DocumentRepositoryTest extends \PHPUnit_Framework_TestCase { + /** + * @var DocumentRepository + */ + private $repository; + + protected function setUp() + { + $this->repository = new DocumentRepository(); + } + /** * @test * @expectedException \InvalidArgumentException */ public function willFailWhenKeyIsEmpty() { - $repository = new DocumentRepository(); - $repository->get(''); + $this->repository->get(''); + } + + /** + * @test + * @expectedException \KleijnWeb\SwaggerBundle\Document\Exception\ResourceNotReadableException + */ + public function willFailWhenPathDoesNotExist() + { + $this->repository->get('/this/is/total/bogus'); } /** @@ -30,18 +48,40 @@ public function willFailWhenKeyIsEmpty() */ public function gettingDocumentThatDoestExistWillConstructIt() { - $repository = new DocumentRepository(); - $document = $repository->get('src/Tests/Functional/PetStore/app/swagger/petstore.yml'); + $document = $this->repository->get('src/Tests/Functional/PetStore/app/swagger/petstore.yml'); $this->assertInstanceOf('KleijnWeb\SwaggerBundle\Document\SwaggerDocument', $document); } + /** + * @test + */ + public function definitionIsObject() + { + $document = $this->repository->get('src/Tests/Functional/PetStore/app/swagger/petstore.yml'); + $this->assertInternalType('object', $document->getDefinition()); + } + + /** + * @test + */ + public function willCache() + { + $path = 'src/Tests/Functional/PetStore/app/swagger/petstore.yml'; + $cache = $this->getMockBuilder('Doctrine\Common\Cache\ArrayCache')->disableOriginalConstructor()->getMock(); + $repository = new DocumentRepository(null, $cache); + $cache->expects($this->exactly(1))->method('fetch')->with($path); + $cache->expects($this->exactly(1))->method('save')->with($path, $this->isType('object')); + $document = $repository->get($path); + $this->assertInternalType('object', $document->getDefinition()); + } + /** * @test */ public function canUsePathPrefix() { - $repository = new DocumentRepository('src/Tests/Functional/PetStore'); - $document = $repository->get('app/swagger/petstore.yml'); + $this->repository = new DocumentRepository('src/Tests/Functional/PetStore'); + $document = $this->repository->get('app/swagger/petstore.yml'); $this->assertInstanceOf('KleijnWeb\SwaggerBundle\Document\SwaggerDocument', $document); } } diff --git a/src/Tests/Document/ParameterRefBuilderTest.php b/src/Tests/Document/ParameterRefBuilderTest.php new file mode 100644 index 0000000..9067be9 --- /dev/null +++ b/src/Tests/Document/ParameterRefBuilderTest.php @@ -0,0 +1,54 @@ + + */ +class ParameterRefBuilderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @test + */ + public function willDefaultToRequestUri() + { + $builder = $this->construct(); + $repository = new DocumentRepository('src/Tests/Functional/PetStore/app'); + $document = $repository->get('swagger/petstore.yml'); + $request = Request::create( + '/pet/100', + 'POST' + ); + $request->attributes->set('_definition', 'swagger/petstore.yml'); + $request->attributes->set('_swagger_path', '/pet/{petId}'); + $request->attributes->set('_swagger_document', $document); + $request->attributes->set('_swagger_operation', $document->getOperationObject('/pet/{petId}', 'POST')); + + $actual = $builder->buildSpecificationLink($request, 'name'); + + $this->assertStringStartsWith('http://petstore.swagger.io/swagger/petstore.yml', $actual); + } + + /** + * @param string|null $scheme + * @param string|null $host + * + * @return ParameterRefBuilder + */ + private function construct($scheme = null, $host = null) + { + $builder = new ParameterRefBuilder('/', $scheme, $host); + + return $builder; + } +} diff --git a/src/Tests/Document/RefResolverTest.php b/src/Tests/Document/RefResolverTest.php new file mode 100644 index 0000000..9fda349 --- /dev/null +++ b/src/Tests/Document/RefResolverTest.php @@ -0,0 +1,102 @@ + + */ +class RefResolverTest extends \PHPUnit_Framework_TestCase +{ + /** + * @test + */ + public function canResolveResourceSchemaReferences() + { + $resolver = $this->construct('petstore.yml'); + $resolver->resolve(); + $schemas = $resolver->getDefinition()->definitions; + $propertySchema = $schemas->Pet->properties->category; + $this->assertObjectNotHasAttribute('$ref', $propertySchema); + $this->assertObjectHasAttribute('id', $propertySchema); + $this->assertSame('object', $propertySchema->type); + } + + /** + * @test + */ + public function canResolveParameterSchemaReferences() + { + $resolver = $this->construct('instagram.yml'); + $pathDefinitions = $resolver->getDefinition()->paths; + $pathDefinition = $pathDefinitions->{'/users/{user-id}'}; + $this->assertInternalType('array', $pathDefinition->parameters); + $pathDefinition = $pathDefinitions->{'/users/{user-id}'}; + $resolver->resolve(); + $this->assertInternalType('array', $pathDefinition->parameters); + $argumentPseudoSchema = $pathDefinition->parameters[0]; + $this->assertObjectNotHasAttribute('$ref', $argumentPseudoSchema); + $this->assertObjectHasAttribute('in', $argumentPseudoSchema); + $this->assertSame('user-id', $argumentPseudoSchema->name); + } + + /** + * @test + */ + public function canResolveReferencesWithSlashed() + { + $resolver = $this->construct('partials/slashes.yml'); + $this->assertSame('thevalue', $resolver->resolve()->Foo->bar); + } + + /** + * @test + * + */ + public function canResolveExternalReferences() + { + $resolver = $this->construct('composite.yml'); + $document = $resolver->resolve(); + $this->assertObjectHasAttribute('schema', $document->responses->Created); + $response = $document->paths->{'/pet'}->post->responses->{'500'}; + $this->assertObjectHasAttribute('description', $response); + } + + /** + * @test + */ + public function canUnResolve() + { + $resolver = $this->construct('composite.yml'); + $expected = clone $resolver->getDefinition(); + $resolver->resolve(); + $document = $resolver->unresolve(); + $this->assertObjectNotHasAttribute('schema', $document->responses->Created); + $this->assertEquals($expected, $document); + } + + /** + * @param string $path + * + * @return RefResolver + */ + private function construct($path) + { + $filePath = "src/Tests/Functional/PetStore/app/swagger/$path"; + $contents = file_get_contents($filePath); + $parser = new YamlParser(); + /** @var object $object */ + $object = $parser->parse($contents); + $resolver = new RefResolver($object, $filePath); + + return $resolver; + } +} diff --git a/src/Tests/Document/SwaggerDocumentTest.php b/src/Tests/Document/SwaggerDocumentTest.php index c998f1b..2eec2c3 100644 --- a/src/Tests/Document/SwaggerDocumentTest.php +++ b/src/Tests/Document/SwaggerDocumentTest.php @@ -6,48 +6,29 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Tests\Dev\Document; +namespace KleijnWeb\SwaggerBundle\Tests\Document; +use KleijnWeb\SwaggerBundle\Document\DocumentRepository; use KleijnWeb\SwaggerBundle\Document\SwaggerDocument; -use org\bovigo\vfs\vfsStream; -use org\bovigo\vfs\vfsStreamDirectory; -use org\bovigo\vfs\vfsStreamWrapper; /** * @author John Kleijn */ class SwaggerDocumentTest extends \PHPUnit_Framework_TestCase { - /** - * @test - * @expectedException \InvalidArgumentException - */ - public function willFailWhenPathDoesNotExist() - { - new SwaggerDocument('/this/is/total/bogus'); - } - - /** - * @test - */ - public function willLoadDefinitionIntoArrayObject() - { - $this->assertInstanceOf('ArrayObject', self::getPetStoreDocument()->getDefinition()); - } - /** * @test */ public function canGetPathDefinitions() { $actual = self::getPetStoreDocument()->getPathDefinitions(); - $this->assertInternalType('array', $actual); + $this->assertInternalType('object', $actual); - // Check a few keys - $this->assertArrayHasKey('/pet', $actual); - $this->assertArrayHasKey('/pet/findByStatus', $actual); - $this->assertArrayHasKey('/store/inventory', $actual); - $this->assertArrayHasKey('/user', $actual); + // Check a few attributes + $this->assertObjectHasAttribute('/pet', $actual); + $this->assertObjectHasAttribute('/pet/findByStatus', $actual); + $this->assertObjectHasAttribute('/store/inventory', $actual); + $this->assertObjectHasAttribute('/user', $actual); } /** @@ -56,12 +37,12 @@ public function canGetPathDefinitions() public function getOperationDefinition() { $actual = self::getPetStoreDocument()->getOperationDefinition('/store/inventory', 'get'); - $this->assertInternalType('array', $actual); + $this->assertInternalType('object', $actual); - // Check a few keys - $this->assertArrayHasKey('parameters', $actual); - $this->assertArrayHasKey('responses', $actual); - $this->assertArrayHasKey('security', $actual); + // Check a few attributes + $this->assertObjectHasAttribute('parameters', $actual); + $this->assertObjectHasAttribute('responses', $actual); + $this->assertObjectHasAttribute('security', $actual); } /** @@ -91,109 +72,13 @@ public function getOperationDefinitionWillFailOnUnknownPath() self::getPetStoreDocument()->getOperationDefinition('/this/is/total/bogus', 'post'); } - /** - * @test - */ - public function canWriteValidYamlToFileSystem() - { - $originalHash = md5_file('src/Tests/Functional/PetStore/app/swagger/petstore.yml'); - - $document = self::getPetStoreDocument(); - $document->write(); - - $newHash = md5_file('src/Tests/Functional/PetStore/app/swagger/petstore.yml'); - - $this->assertSame($originalHash, $newHash); - } - - /** - * @test - */ - public function gettingArrayCopyWillLeaveEmptyArraysAsEmptyArrays() - { - $document = self::getPetStoreDocument(); - $data = $document->getArrayCopy(); - - $emptyParameters = $data['paths']['/store/inventory']['get']['parameters']; - $emptyAuthSpec = $data['paths']['/store/inventory']['get']['security'][0]['api_key']; - - $this->assertSame([], $emptyParameters); - $this->assertSame([], $emptyAuthSpec); - } - - /** - * @test - */ - public function canWriteModifiedYamlToFileSystem() - { - $originalHash = md5_file('src/Tests/Functional/PetStore/app/swagger/petstore.yml'); - vfsStreamWrapper::register(); - vfsStreamWrapper::setRoot(new vfsStreamDirectory('canWriteModifiedYamlToFileSystem')); - - $modifiedPath = vfsStream::url('canWriteModifiedYamlToFileSystem/modified.yml'); - - $document = self::getPetStoreDocument(); - $definition = $document->getDefinition(); - $definition->version = '0.0.2'; - $document->write($modifiedPath); - - $newHash = md5_file($modifiedPath); - - $this->assertNotSame($originalHash, $newHash); - } - - /** - * @test - */ - public function canModifiedYamlWrittenToFileSystemHandlesEmptyArraysCorrectly() - { - vfsStreamWrapper::register(); - vfsStreamWrapper::setRoot( - new vfsStreamDirectory('canModifiedYamlWrittenToFileSystemHandlesEmptyArraysCorrectly') - ); - - $modifiedPath = vfsStream::url('canModifiedYamlWrittenToFileSystemHandlesEmptyArraysCorrectly/modified.yml'); - - $document = self::getPetStoreDocument(); - $definition = $document->getDefinition(); - $definition->version = '0.0.2'; - $document->write($modifiedPath); - - $content = file_get_contents($modifiedPath); - $this->assertNotRegExp('/\: \{ \}/', $content); - } - - /** - * @test - */ - public function canResolveResourceSchemaReferences() - { - $document = self::getPetStoreDocument(); - $schemas = $document->getResourceSchemas(); - $propertySchema = $schemas['Pet']['properties']['category']; - $this->assertArrayNotHasKey('$ref', $propertySchema); - $this->assertArrayHasKey('id', $propertySchema); - $this->assertSame('object', $propertySchema['type']); - } - - /** - * @test - */ - public function canResolveParameterSchemaReferences() - { - $document = new SwaggerDocument('src/Tests/Functional/PetStore/app/swagger/instagram.yml'); - $pathDefinitions = $document->getPathDefinitions(); - $argumentPseudoSchema = $pathDefinitions['/users/{user-id}']['parameters'][0]; - $this->assertArrayNotHasKey('$ref', $argumentPseudoSchema); - $this->assertArrayHasKey('in', $argumentPseudoSchema); - $this->assertSame('user-id', $argumentPseudoSchema['name']); - } - /** * @return SwaggerDocument */ public static function getPetStoreDocument() { - return new SwaggerDocument('src/Tests/Functional/PetStore/app/swagger/petstore.yml'); + $repository = new DocumentRepository('src/Tests/Functional/PetStore'); + + return $repository->get('app/swagger/petstore.yml'); } } diff --git a/src/Tests/Document/YamlCapableUriRetrieverTest.php b/src/Tests/Document/YamlCapableUriRetrieverTest.php deleted file mode 100644 index 93bc6ae..0000000 --- a/src/Tests/Document/YamlCapableUriRetrieverTest.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ -class YamlCapableUriRetrieverTest extends \PHPUnit_Framework_TestCase -{ - /** - * @test - */ - public function canRetrieveYamlFileAsJsonString() - { - $retriever = new YamlCapableUriRetriever(); - $result = $retriever->retrieve('file://' . realpath('src/Tests/Functional/PetStore/app/swagger/petstore.yml')); - $this->assertInternalType('string', $result); - $array = json_decode($result, true); - $this->assertNotNull($array); - $this->assertInternalType('array', $array); - } -} diff --git a/src/Tests/Document/YamlParserTest.php b/src/Tests/Document/YamlParserTest.php new file mode 100644 index 0000000..b3ae867 --- /dev/null +++ b/src/Tests/Document/YamlParserTest.php @@ -0,0 +1,67 @@ + + */ +class YamlParserTest extends \PHPUnit_Framework_TestCase +{ + /** + * Check Symfony\Yaml bug + * + * @see https://github.com/symfony/symfony/issues/17709 + * + * @test + */ + public function canParseNumericMap() + { + $yaml = <<parse($yaml); + $this->assertInternalType('object', $actual); + $this->assertInternalType('object', $actual->map); + $this->assertTrue(property_exists($actual->map, '1')); + $this->assertTrue(property_exists($actual->map, '2')); + $this->assertSame('one', $actual->map->{'1'}); + $this->assertSame('two', $actual->map->{'2'}); + } + + /** + * Check Symfony\Yaml bug + * + * @see https://github.com/symfony/symfony/pull/17711 + * + * @test + */ + public function willParseArrayAsArrayAndObjectAsObject() + { + $yaml = <<parse($yaml); + $this->assertInternalType('object', $actual); + + $this->assertInternalType('array', $actual->array); + $this->assertInternalType('object', $actual->array[0]); + $this->assertInternalType('object', $actual->array[1]); + $this->assertSame('one', $actual->array[0]->key); + $this->assertSame('two', $actual->array[1]->key); + } +} diff --git a/src/Tests/EventListener/RequestListenerTest.php b/src/Tests/EventListener/RequestListenerTest.php index 2a90ffa..2779fbe 100644 --- a/src/Tests/EventListener/RequestListenerTest.php +++ b/src/Tests/EventListener/RequestListenerTest.php @@ -6,8 +6,9 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Tests\Dev\EventListener; +namespace KleijnWeb\SwaggerBundle\Tests\EventListener; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\EventListener\RequestListener; use Symfony\Component\HttpFoundation\Request; @@ -95,8 +96,8 @@ public function willTellTransformerToCoerceRequest() $this->documentMock ->expects($this->once()) - ->method('getOperationDefinition') - ->willReturn([]); + ->method('getOperationObject') + ->willReturn(OperationObject::createFromOperationDefinition((object)[])); $this->repositoryMock ->expects($this->once()) @@ -129,7 +130,7 @@ public function willNotTellTransformerToCoerceRequestWhenNotMasterRequest() $this->documentMock ->expects($this->never()) - ->method('getOperationDefinition'); + ->method('getOperationObject'); $this->transformerMock ->expects($this->never()) @@ -195,9 +196,9 @@ public function canGetOperationDefinitionUsingSwaggerPath() $this->documentMock ->expects($this->once()) - ->method('getOperationDefinition') + ->method('getOperationObject') ->with(self::SWAGGER_PATH) - ->willReturn([]); + ->willReturn(OperationObject::createFromOperationDefinition((object)[])); $this->repositoryMock ->expects($this->once()) diff --git a/src/Tests/EventListener/ViewListenerTest.php b/src/Tests/EventListener/ViewListenerTest.php index 0ed9606..b23ae1c 100644 --- a/src/Tests/EventListener/ViewListenerTest.php +++ b/src/Tests/EventListener/ViewListenerTest.php @@ -6,7 +6,7 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Tests\Dev\EventListener; +namespace KleijnWeb\SwaggerBundle\Tests\EventListener; use KleijnWeb\SwaggerBundle\EventListener\ViewListener; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Tests/EventListener/ExceptionListenerTest.php b/src/Tests/EventListener/VndErrorExceptionListenerTest.php similarity index 63% rename from src/Tests/EventListener/ExceptionListenerTest.php rename to src/Tests/EventListener/VndErrorExceptionListenerTest.php index 4d7fdda..7ab7c3a 100644 --- a/src/Tests/EventListener/ExceptionListenerTest.php +++ b/src/Tests/EventListener/VndErrorExceptionListenerTest.php @@ -6,17 +6,22 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Tests\Dev\EventListener; +namespace KleijnWeb\SwaggerBundle\Tests\EventListener; -use KleijnWeb\SwaggerBundle\EventListener\ExceptionListener; +use KleijnWeb\SwaggerBundle\EventListener\VndErrorExceptionListener; +use KleijnWeb\SwaggerBundle\Exception\InvalidParametersException; +use KleijnWeb\SwaggerBundle\Response\VndValidationErrorFactory; use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Ramsey\VndError\VndError; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * @author John Kleijn */ -class ExceptionListenerTest extends \PHPUnit_Framework_TestCase +class VndErrorExceptionListenerTest extends \PHPUnit_Framework_TestCase { /** * @var GetResponseForExceptionEvent @@ -34,47 +39,76 @@ class ExceptionListenerTest extends \PHPUnit_Framework_TestCase private $exception; /** - * @var ExceptionListener + * @var Request + */ + private $request; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var VndErrorExceptionListener */ private $exceptionListener; + /** + * @var VndValidationErrorFactory + */ + private $validationErrorFactory; + /** * Set up mocking */ protected function setUp() { + $this->event = $this + ->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent') + ->disableOriginalConstructor() + ->setMethods(['getException', 'getRequest']) + ->getMock(); + $this->exception = new \Exception("Mary had a little lamb"); $reflection = new \ReflectionClass($this->exception); $codeProperty = $reflection->getProperty('code'); $this->codeProperty = $codeProperty; $this->codeProperty->setAccessible(true); + $attributes = [ + '_definition' => '/foo/bar' + ]; + $this->request = new Request($query = [], $request = [], $attributes); - $this->event = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent') - ->disableOriginalConstructor() - ->setMethods(['getException']) - ->getMock(); - - $this->event->expects($this->any()) + $this->event + ->expects($this->any()) ->method('getException') ->willReturn($this->exception); - /** @var LoggerInterface $logger */ - $logger = $this->getMockForAbstractClass('Psr\Log\LoggerInterface'); - $this->exceptionListener = new ExceptionListener($logger); + $this->event + ->expects($this->any()) + ->method('getRequest') + ->willReturn($this->request); + + $this->validationErrorFactory = $this + ->getMockBuilder('KleijnWeb\SwaggerBundle\Response\VndValidationErrorFactory') + ->disableOriginalConstructor() + ->getMock(); + + $this->logger = $this->getMockForAbstractClass('Psr\Log\LoggerInterface'); + $this->exceptionListener = new VndErrorExceptionListener($this->validationErrorFactory, $this->logger); } /** * @test */ - public function willLogExceptionsWith4xxCodesAsInputErrorNotices() + public function willLogExceptionsWith4xxCodesAsBadRequestNotices() { for ($i = 0; $i < 99; $i++) { $logger = $this->getMockForAbstractClass('Psr\Log\LoggerInterface'); $logger ->expects($this->once()) - ->method('notice') - ->with($this->stringStartsWith('Input error')); + ->method('log') + ->with(LogLevel::NOTICE, $this->stringStartsWith('Bad Request')); /** @var LoggerInterface $logger */ $this->exceptionListener->setLogger($logger); @@ -92,8 +126,8 @@ public function willLogExceptionsWith5xxCodesAsRuntimeErrors() $logger = $this->getMockForAbstractClass('Psr\Log\LoggerInterface'); $logger ->expects($this->once()) - ->method('error') - ->with($this->stringStartsWith('Runtime error')); + ->method('log') + ->with(LogLevel::ERROR, $this->stringStartsWith('Internal Server Error')); /** @var LoggerInterface $logger */ $this->exceptionListener->setLogger($logger); @@ -112,8 +146,8 @@ public function willLogExceptionsWithUnexpectedCodesAsCriticalErrors() $logger = $this->getMockForAbstractClass('Psr\Log\LoggerInterface'); $logger ->expects($this->once()) - ->method('critical') - ->with($this->stringStartsWith('Runtime error')); + ->method('log') + ->with(LogLevel::CRITICAL, $this->stringStartsWith('Internal Server Error')); /** @var LoggerInterface $logger */ $this->exceptionListener->setLogger($logger); @@ -153,11 +187,12 @@ public function willSetResponseWithValidJsonContent() */ public function willSetResponseWithSimpleMessage() { - foreach ([400 => 'Input Error', 500 => 'Server Error'] as $code => $message) { + foreach ([400 => 'Bad Request', 500 => 'Internal Server Error'] as $code => $message) { $this->codeProperty->setValue($this->exception, $code); $this->exceptionListener->onKernelException($this->event); $response = $this->event->getResponse(); - $this->assertEquals($message, json_decode($response->getContent())->message); + $this->assertNotNull($body = json_decode($response->getContent())); + $this->assertEquals($message, $body->message); } } @@ -184,8 +219,8 @@ public function logrefInResponseAndLogMatch() $logger = $this->getMockForAbstractClass('Psr\Log\LoggerInterface'); $logger ->expects($this->once()) - ->method($this->anything()) - ->with($this->callback(function ($message) use (&$logref) { + ->method('log') + ->with($this->anything(), $this->callback(function ($message) use (&$logref) { $matches = []; if (preg_match('/logref ([a-z0-9]*)/', $message, $matches)) { $logref = $matches[1]; @@ -213,17 +248,51 @@ public function willReturn404Responses() $event = $this ->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent') ->disableOriginalConstructor() - ->setMethods(['getException']) + ->setMethods(['getException', 'getRequest']) ->getMock(); $event->expects($this->any()) ->method('getException') ->willReturn(new NotFoundHttpException()); - $this->exceptionListener->onKernelException($event); + $event->expects($this->any()) + ->method('getRequest') + ->willReturn($this->request); + $this->exceptionListener->onKernelException($event); $response = $event->getResponse(); - $this->assertSame(404, $response->getStatusCode()); } + + /** + * @test + */ + public function willCreateValidationErrorResponse() + { + $event = $this + ->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent') + ->disableOriginalConstructor() + ->setMethods(['getException', 'getRequest']) + ->getMock(); + + $exception = new InvalidParametersException('Oh noes', []); + + $event->expects($this->any()) + ->method('getException') + ->willReturn($exception); + + $event->expects($this->any()) + ->method('getRequest') + ->willReturn($this->request); + + $this->validationErrorFactory + ->expects($this->any()) + ->method('create') + ->with($this->request, $exception) + ->willReturn(new VndError('Try again')); + + $this->exceptionListener->onKernelException($event); + $response = $event->getResponse(); + $this->assertSame(400, $response->getStatusCode()); + } } diff --git a/src/Tests/Functional/ApiTestCaseTest.php b/src/Tests/Functional/ApiTestCaseTest.php new file mode 100644 index 0000000..07b79b9 --- /dev/null +++ b/src/Tests/Functional/ApiTestCaseTest.php @@ -0,0 +1,41 @@ + + */ +class ApiTestCaseTest extends WebTestCase +{ + use ApiTestCase; + + /** + * Use config_basic.yml + * + * @var bool + */ + protected $env = 'basic'; + + public static function setUpBeforeClass() + { + static::initSchemaManager(__DIR__ . '/PetStore/app/swagger/petstore.yml'); + } + + /** + * @test + * @expectedException \KleijnWeb\SwaggerBundle\Test\ApiResponseErrorException + */ + public function notFoundApiCallThrowsException() + { + $this->get('/foo'); + } +} diff --git a/src/Tests/Functional/BasicPetStoreApiTest.php b/src/Tests/Functional/BasicPetStoreApiTest.php index 8a924f4..8b8bc4d 100644 --- a/src/Tests/Functional/BasicPetStoreApiTest.php +++ b/src/Tests/Functional/BasicPetStoreApiTest.php @@ -8,7 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Tests\Functional; -use KleijnWeb\SwaggerBundle\Dev\Test\ApiTestCase; +use KleijnWeb\SwaggerBundle\Test\ApiTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** @@ -46,7 +46,7 @@ public function canFindPetsByStatus() public function canAddPet() { $content = [ - 'name' => 'Joe', + 'name' => 'Joe', 'photoUrls' => ['foobar'] ]; diff --git a/src/Tests/Functional/CommandIntegrationTest.php b/src/Tests/Functional/CommandIntegrationTest.php deleted file mode 100644 index 572bf5b..0000000 --- a/src/Tests/Functional/CommandIntegrationTest.php +++ /dev/null @@ -1,109 +0,0 @@ - - */ -class CommandIntegrationTest extends KernelTestCase -{ - /** - * @test - */ - public function canRunAmendSwaggerDocumentCommand() - { - $commandName = AmendSwaggerDocumentCommand::NAME; - $diKey = 'swagger.dev.command.amend_swagger'; - - $mockFixer = $this - ->getMockBuilder('KleijnWeb\SwaggerBundle\Dev\DocumentFixer\Fixer') - ->disableOriginalConstructor() - ->getMock(); - - $diMocks = [ - 'swagger.document.repository' => $this->getRepositoryStub(), - 'swagger.dev.document_fixer.swagger_bundle_response' => $mockFixer - ]; - - $this->runWithMocks($diMocks, $diKey, $commandName, ['file' => '/fake']); - } - - /** - * @test - */ - public function canRunGenerateResourceClassesCommand() - { - $commandName = GenerateResourceClassesCommand::NAME; - $diKey = 'swagger.dev.command.generate_resources'; - - $mockGenerator = $this - ->getMockBuilder('KleijnWeb\SwaggerBundle\Dev\Generator\ResourceGenerator') - ->disableOriginalConstructor() - ->getMock(); - - $diStubs = [ - 'swagger.document.repository' => $this->getRepositoryStub(), - 'swagger.dev.resource_generator' => $mockGenerator - ]; - - $this->runWithMocks($diStubs, $diKey, $commandName, ['file' => '/fake', 'bundle' => 'PetStoreBundle']); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getRepositoryStub() - { - $mockStub = $this - ->getMockBuilder('KleijnWeb\SwaggerBundle\Document\DocumentRepository') - ->disableOriginalConstructor() - ->getMock(); - - $mockStub->expects($this->once())->method('get')->willReturn( - $this - ->getMockBuilder('KleijnWeb\SwaggerBundle\Document\SwaggerDocument') - ->disableOriginalConstructor() - ->getMock() - ); - - return $mockStub; - } - - /** - * @param array $diMocks - * @param string $diKey - * @param string $commandName - * @param array $arguments - * - * @return int - */ - private function runWithMocks(array $diMocks, $diKey, $commandName, array $arguments) - { - $kernel = $this->createKernel(); - $kernel->boot(); - $container = $kernel->getContainer(); - foreach ($diMocks as $key => $mock) { - $container->set($key, $mock); - } - - $application = new Application($kernel); - $application->add($container->get($diKey)); - - $command = $application->find($commandName); - $commandTester = new CommandTester($command); - - return $commandTester->execute($arguments); - } -} diff --git a/src/Tests/Functional/GenericDataApiTest.php b/src/Tests/Functional/GenericDataApiTest.php index 707719f..883698c 100644 --- a/src/Tests/Functional/GenericDataApiTest.php +++ b/src/Tests/Functional/GenericDataApiTest.php @@ -8,7 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Tests\Functional; -use KleijnWeb\SwaggerBundle\Dev\Test\ApiTestCase; +use KleijnWeb\SwaggerBundle\Test\ApiTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** diff --git a/src/Tests/Functional/PetStore/Controller/PetController.php b/src/Tests/Functional/PetStore/Controller/PetController.php index d70ed86..4c50b9a 100644 --- a/src/Tests/Functional/PetStore/Controller/PetController.php +++ b/src/Tests/Functional/PetStore/Controller/PetController.php @@ -23,7 +23,13 @@ class PetController */ public function findPetsByStatus(Request $request) { - return []; + return [ + [ + 'id' => 1, + 'name' => 'Scooby', + 'photoUrls' => [] + ] + ]; } /** @@ -46,8 +52,8 @@ public function addPet(array $body) public function getPetById($petId) { return [ - 'id' => $petId, - 'name' => 'Chuckie', + 'id' => $petId, + 'name' => 'Chuckie', 'photoUrls' => [] ]; } diff --git a/src/Tests/Functional/PetStore/Controller/UserController.php b/src/Tests/Functional/PetStore/Controller/UserController.php new file mode 100644 index 0000000..74b3c00 --- /dev/null +++ b/src/Tests/Functional/PetStore/Controller/UserController.php @@ -0,0 +1,28 @@ + + */ +class UserController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param string $username + * @param string $password + * + * @return array + */ + public function loginUser($username, $password) + { + return uniqid(); + } +} diff --git a/src/Tests/Functional/PetStore/EventListener/ResponseListener.php b/src/Tests/Functional/PetStore/EventListener/ResponseListener.php new file mode 100644 index 0000000..8f5147e --- /dev/null +++ b/src/Tests/Functional/PetStore/EventListener/ResponseListener.php @@ -0,0 +1,37 @@ + + */ +class ResponseListener +{ + /** + * @param FilterResponseEvent $event + */ + public function onKernelResponse(FilterResponseEvent $event) + { + if (!$event->isMasterRequest()) { + return; + } + $request = $event->getRequest(); + $headers = $event->getResponse()->headers; + switch ($request->attributes->get('_swagger_path')) { + case '/user/login': + $headers->set('X-Rate-Limit', 123456789); + $headers->set('X-Expires-After', date('Y-m-d\TH:i:s\Z')); + break; + default: + //noop + } + } +} diff --git a/src/Tests/Functional/PetStore/Model/Resources/Order.php b/src/Tests/Functional/PetStore/Model/Resources/Order.php index a1e1ff0..a22b1fe 100644 --- a/src/Tests/Functional/PetStore/Model/Resources/Order.php +++ b/src/Tests/Functional/PetStore/Model/Resources/Order.php @@ -13,37 +13,37 @@ class Order * @Type("integer") */ private $id; - + /** * @var integer * @Type("integer") */ private $petId; - + /** * @var integer * @Type("integer") */ private $quantity; - + /** * @var \DateTime * @Type("DateTime<'Y-m-d'>") */ private $shipDate; - + /** * @var string * @Type("string") */ private $status; - + /** * @var boolean * @Type("boolean") */ private $complete; - + /** * @param integer * @@ -55,7 +55,7 @@ public function setId($id) return $this; } - + /** * @param integer * @@ -67,7 +67,7 @@ public function setPetid($petId) return $this; } - + /** * @param integer * @@ -79,7 +79,7 @@ public function setQuantity($quantity) return $this; } - + /** * @param \DateTime * @@ -91,7 +91,7 @@ public function setShipdate($shipDate) return $this; } - + /** * @param string * @@ -103,7 +103,7 @@ public function setStatus($status) return $this; } - + /** * @param boolean * @@ -115,7 +115,7 @@ public function setComplete($complete) return $this; } - + /** * @return integer */ @@ -123,7 +123,7 @@ public function getId() { return $this->id; } - + /** * @return integer */ @@ -131,7 +131,7 @@ public function getPetid() { return $this->petId; } - + /** * @return integer */ @@ -139,16 +139,16 @@ public function getQuantity() { return $this->quantity; } - + /** * @return \DateTime */ - + public function getShipdate() { return $this->shipDate; } - + /** * @return string */ @@ -156,7 +156,7 @@ public function getStatus() { return $this->status; } - + /** * @return boolean */ diff --git a/src/Tests/Functional/PetStore/PetStoreBundle.php b/src/Tests/Functional/PetStore/PetStoreBundle.php index db445c3..aae1499 100644 --- a/src/Tests/Functional/PetStore/PetStoreBundle.php +++ b/src/Tests/Functional/PetStore/PetStoreBundle.php @@ -8,9 +8,6 @@ namespace KleijnWeb\SwaggerBundle\Tests\Functional\PetStore; -use org\bovigo\vfs\vfsStream; -use org\bovigo\vfs\vfsStreamDirectory; -use org\bovigo\vfs\vfsStreamWrapper; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -20,11 +17,13 @@ class PetStoreBundle extends Bundle { /** - * @return ExtensionInterface + * @return null */ public function getContainerExtension() { - return $this->extension = false; + $this->extension = false; + + return null; } /** @@ -34,27 +33,4 @@ public function getNamespace() { return __NAMESPACE__; } - - /** - * Gets the Bundle directory path. - * - * @return string The Bundle absolute path - * - * @api - */ - public function getPath() - { - if (!$this->path) { - vfsStreamWrapper::register(); - vfsStreamWrapper::setRoot(new vfsStreamDirectory('root')); - - $this->path = vfsStream::url('root/PetStoreBundle'); - - if (!is_dir($this->path)) { - mkdir($this->path); - } - } - - return $this->path; - } } diff --git a/src/Tests/Functional/PetStore/app/TestKernel.php b/src/Tests/Functional/PetStore/app/TestKernel.php index e72fd50..b41027b 100644 --- a/src/Tests/Functional/PetStore/app/TestKernel.php +++ b/src/Tests/Functional/PetStore/app/TestKernel.php @@ -1,5 +1,4 @@ + */ +class VndParameterValidationErrorTest extends WebTestCase +{ + use ApiTestCase; + + /** + * Use config_basic.yml + * + * @var bool + */ + protected $env = 'basic'; + + public static function setUpBeforeClass() + { + static::initSchemaManager(__DIR__ . '/PetStore/app/swagger/petstore.yml'); + } + + /** + * @test + */ + public function parameterValidationErrorWillContainDefaultMessageAndLogref() + { + try { + $this->get('/v2/pet/findByStatus', ['status' => 'bogus']); + } catch (ApiResponseErrorException $e) { + $data = Hal::fromJson($e->getJson(), 10)->getData(); + $this->assertSame(VndValidationErrorFactory::DEFAULT_MESSAGE, $data['message']); + $this->assertRegExp('/[0-9a-z]+/', $data['logref']); + + return; + } + $this->fail("Expected exception"); + } + + /** + * @test + */ + public function parameterValidationErrorWillContainSpecificationPointer() + { + try { + $this->get('/v2/pet/findByStatus', ['status' => 'bogus']); + } catch (ApiResponseErrorException $e) { + $error = Hal::fromJson($e->getJson(), 10); + $resource = $error->getFirstResource('errors'); + $specLink = 'http://petstore.swagger.io/swagger/petstore.yml#/paths/~1pet~1findByStatus/get/parameters/0'; + $this->assertSame($specLink, $resource->getUri()); + + return; + } + $this->fail("Expected exception"); + } + + /** + * @test + */ + public function parameterValidationErrorWillContainSchemaPointer() + { + try { + $this->get('/v2/pet/findByStatus', ['status' => 'bogus']); + } catch (ApiResponseErrorException $e) { + $error = Hal::fromJson($e->getJson(), 10); + $resource = $error->getFirstResource('errors'); + $data = $resource->getData(); + $this->assertSame('/paths/~1pet~1findByStatus/get/x-request-schema/properties/status', $data['path']); + + return; + } + $this->fail("Expected exception"); + } + + /** + * @test + */ + public function parameterValidationErrorCanContainMultipleErrors() + { + try { + $this->get('/v2/user/login'); + } catch (ApiResponseErrorException $e) { + $error = Hal::fromJson($e->getJson(), 10); + $resources = $error->getResources(); + $this->assertArrayHasKey('errors', $resources); + /** + * @var int $i + * @var Hal $resource + */ + foreach ($resources['errors'] as $i => $resource) { + $data = $resource->getData(); + if ($i == 0) { + $this->assertSame('/paths/~1user~1login/get/x-request-schema/properties/username', $data['path']); + $uri = 'http://petstore.swagger.io/swagger/petstore.yml#/paths/~1user~1login/get/parameters/0'; + $this->assertSame($uri, $resource->getUri()); + continue; + } + $this->assertSame('/paths/~1user~1login/get/x-request-schema/properties/password', $data['path']); + $uri = 'http://petstore.swagger.io/swagger/petstore.yml#/paths/~1user~1login/get/parameters/1'; + $this->assertSame($uri, $resource->getUri()); + } + + return; + } + $this->fail("Expected exception"); + + } +} diff --git a/src/Tests/Request/ContentDecoder/ContentDecoderJmsSerializerCompatibilityTest.php b/src/Tests/Request/ContentDecoder/ContentDecoderJmsSerializerCompatibilityTest.php index 33f44bb..57a88cc 100644 --- a/src/Tests/Request/ContentDecoder/ContentDecoderJmsSerializerCompatibilityTest.php +++ b/src/Tests/Request/ContentDecoder/ContentDecoderJmsSerializerCompatibilityTest.php @@ -9,7 +9,7 @@ namespace KleijnWeb\SwaggerBundle\Tests\Request\ContentDecoder; use JMS\Serializer\Serializer; -use JMS\Serializer\SerializerBuilder; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\Request\ContentDecoder; use KleijnWeb\SwaggerBundle\Serializer\JmsSerializerFactory; use KleijnWeb\SwaggerBundle\Serializer\SerializationTypeResolver; @@ -55,19 +55,21 @@ public function canDeserializeIntoObject() $request->headers->set('Content-Type', 'application/json'); - $operationDefinition = [ + $operationDefinition = (object)[ 'parameters' => [ - [ - "in" => "body", - "name" => "body", - "schema" => [ + (object)[ + "in" => "body", + "name" => "body", + "schema" => (object)[ '$ref' => "#/definitions/JmsAnnotatedResourceStub" ] ] ] ]; - $actual = $this->contentDecoder->decodeContent($request, $operationDefinition); + $operationObject = OperationObject::createFromOperationDefinition((object)$operationDefinition); + + $actual = $this->contentDecoder->decodeContent($request, $operationObject); $className = 'KleijnWeb\SwaggerBundle\Tests\Request\ContentDecoder\JmsAnnotatedResourceStub'; $expected = (new $className)->setFoo('bar'); @@ -86,8 +88,7 @@ public function willThrowMalformedContentExceptionWhenDecodingFails() $request = new Request([], [], [], [], [], [], $content); $request->headers->set('Content-Type', 'application/json'); - $operationDefinition = []; - - $this->contentDecoder->decodeContent($request, $operationDefinition); + $operationObject = OperationObject::createFromOperationDefinition((object)[]); + $this->contentDecoder->decodeContent($request, $operationObject); } } diff --git a/src/Tests/Request/ContentDecoder/ContentDecoderSymfonySerializerCompatibilityTest.php b/src/Tests/Request/ContentDecoder/ContentDecoderSymfonySerializerCompatibilityTest.php index 04a012d..c0b49a4 100644 --- a/src/Tests/Request/ContentDecoder/ContentDecoderSymfonySerializerCompatibilityTest.php +++ b/src/Tests/Request/ContentDecoder/ContentDecoderSymfonySerializerCompatibilityTest.php @@ -8,6 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Tests\Request\ContentDecoder; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\Request\ContentDecoder; use KleijnWeb\SwaggerBundle\Serializer\SerializationTypeResolver; use KleijnWeb\SwaggerBundle\Serializer\SerializerAdapter; @@ -90,19 +91,21 @@ public function getFoo(){ return \$this->foo; } } "); - $operationDefinition = [ + $operationDefinition = (object)[ 'parameters' => [ - [ + (object)[ "in" => "body", "name" => "body", - "schema" => [ + "schema" => (object)[ '$ref' => "#/definitions/$className" ] ] ] ]; - $actual = $this->contentDecoder->decodeContent($request, $operationDefinition); + $operationObject = OperationObject::createFromOperationDefinition((object)$operationDefinition); + + $actual = $this->contentDecoder->decodeContent($request, $operationObject); $expected = (new $className)->setFoo('bar'); @@ -120,12 +123,13 @@ public function willThrowMalformedContentExceptionWhenDecodingFails() $request = new Request([], [], [], [], [], [], $content); $request->headers->set('Content-Type', 'application/json'); - $operationDefinition = []; $this->jsonDecoderMock ->expects($this->once()) ->method('decode') ->with($request->getContent(), 'json'); - $this->contentDecoder->decodeContent($request, $operationDefinition); + $operationObject = OperationObject::createFromOperationDefinition((object)[]); + + $this->contentDecoder->decodeContent($request, $operationObject); } } diff --git a/src/Tests/Request/ContentDecoder/ContentDecoderTest.php b/src/Tests/Request/ContentDecoder/ContentDecoderTest.php index 9107b58..186316e 100644 --- a/src/Tests/Request/ContentDecoder/ContentDecoderTest.php +++ b/src/Tests/Request/ContentDecoder/ContentDecoderTest.php @@ -8,6 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Tests\Request\ContentDecoder; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\Request\ContentDecoder; use KleijnWeb\SwaggerBundle\Serializer\ArraySerializer; use KleijnWeb\SwaggerBundle\Serializer\SerializerAdapter; @@ -46,9 +47,9 @@ public function canDecodeValidJson() $request = new Request([], [], [], [], [], [], $content); $request->headers->set('Content-Type', 'application/json'); - $operationDefinition = []; + $operationObject = OperationObject::createFromOperationDefinition((object)[]); - $actual = $this->contentDecoder->decodeContent($request, $operationDefinition); + $actual = $this->contentDecoder->decodeContent($request, $operationObject); $expected = ['foo' => 'bar']; $this->assertSame($expected, $actual); } @@ -64,8 +65,8 @@ public function willThrowMalformedContentExceptionWhenDecodingFails() $request = new Request([], [], [], [], [], [], $content); $request->headers->set('Content-Type', 'application/json'); - $operationDefinition = []; + $operationObject = OperationObject::createFromOperationDefinition((object)[]); - $this->contentDecoder->decodeContent($request, $operationDefinition); + $this->contentDecoder->decodeContent($request, $operationObject); } } diff --git a/src/Tests/Request/ContentDecoder/ParameterCoercerTest.php b/src/Tests/Request/ContentDecoder/ParameterCoercerTest.php index b6699b4..1af7633 100644 --- a/src/Tests/Request/ContentDecoder/ParameterCoercerTest.php +++ b/src/Tests/Request/ContentDecoder/ParameterCoercerTest.php @@ -34,7 +34,7 @@ public function willInterpretPrimitivesAsExpected($type, $value, $expected, $for $spec['format'] = $format; } - $actual = ParameterCoercer::coerceParameter($spec, $value); + $actual = ParameterCoercer::coerceParameter((object)$spec, $value); $this->assertEquals($expected, $actual); } @@ -50,7 +50,7 @@ public function willInterpretPrimitivesAsExpected($type, $value, $expected, $for */ public function willFailToInterpretPrimitivesAsExpected($type, $value) { - ParameterCoercer::coerceParameter(['type' => $type, 'name' => $value], $value); + ParameterCoercer::coerceParameter((object)['type' => $type, 'name' => $value], $value); } /** @@ -64,7 +64,7 @@ public function willFailToInterpretPrimitivesAsExpected($type, $value) */ public function willFailToInterpretDateTimeAsExpected($format, $value) { - ParameterCoercer::coerceParameter(['type' => 'string', 'format' => $format, 'name' => $value], $value); + ParameterCoercer::coerceParameter((object)['type' => 'string', 'format' => $format, 'name' => $value], $value); } /** @@ -79,7 +79,7 @@ public function willFailToInterpretDateTimeAsExpected($format, $value) public function willThrowUnsupportedExceptionInPredefinedCases($spec, $value) { $spec = array_merge(['type' => 'string', 'name' => $value], $spec); - ParameterCoercer::coerceParameter($spec, $value); + ParameterCoercer::coerceParameter((object)$spec, $value); } /** diff --git a/src/Tests/Request/RequestCoercerTest.php b/src/Tests/Request/RequestCoercerTest.php index 399fba2..e01c552 100644 --- a/src/Tests/Request/RequestCoercerTest.php +++ b/src/Tests/Request/RequestCoercerTest.php @@ -8,6 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Tests\Request; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\Request\ContentDecoder; use KleijnWeb\SwaggerBundle\Request\RequestCoercer; use KleijnWeb\SwaggerBundle\Request\ParameterCoercer; @@ -46,16 +47,20 @@ public function willAddDecodedContentAsAttribute() $content = '[1,2,3,4]'; $request = new Request([], [], [], [], [], [], $content); - $operationDefinition = [ + $operationDefinition = (object)[ 'parameters' => [ - [ - 'name' => 'myContent', - 'in' => 'body' + (object)[ + 'name' => 'myContent', + 'in' => 'body', + 'schema' => (object)[ + 'type' => 'array' + ] ] ] ]; - $coercer->coerceRequest($request, $operationDefinition); + $operationObject = OperationObject::createFromOperationDefinition((object)$operationDefinition); + $coercer->coerceRequest($request, $operationObject); $this->assertSame([1, 2, 3, 4], $request->attributes->get('myContent')); } @@ -68,9 +73,9 @@ public function willConstructDate() $coercer = new RequestCoercer($this->contentDecoderMock); $request = new Request(['foo' => "2015-01-01"], [], [], [], [], []); - $operationDefinition = [ + $operationDefinition = (object)[ 'parameters' => [ - [ + (object)[ 'name' => 'foo', 'in' => 'query', 'type' => 'string', @@ -79,9 +84,10 @@ public function willConstructDate() ] ]; - $coercer->coerceRequest($request, $operationDefinition); + $operationObject = OperationObject::createFromOperationDefinition((object)$operationDefinition); + $coercer->coerceRequest($request, $operationObject); - $expected = ParameterCoercer::coerceParameter($operationDefinition['parameters'][0], "2015-01-01"); + $expected = ParameterCoercer::coerceParameter($operationDefinition->parameters[0], "2015-01-01"); // Sanity check $this->assertInstanceOf('DateTime', $expected); diff --git a/src/Tests/Request/RequestProcessorTest.php b/src/Tests/Request/RequestProcessorTest.php index e09c5b9..cb6ce5b 100644 --- a/src/Tests/Request/RequestProcessorTest.php +++ b/src/Tests/Request/RequestProcessorTest.php @@ -8,7 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Tests\Request; -use KleijnWeb\SwaggerBundle\Request\ContentDecoder; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\Request\RequestCoercer; use KleijnWeb\SwaggerBundle\Request\RequestProcessor; use KleijnWeb\SwaggerBundle\Request\RequestValidator; @@ -30,15 +30,17 @@ public function willValidateRequest() ->disableOriginalConstructor() ->getMock(); - $operationDefinition = [ + $operationDefinition = (object)[ 'parameters' => [ - [ - 'name' => 'myContent', - 'in' => 'body' + (object)[ + 'name' => 'find', + 'in' => 'query' ] ] ]; + $operationObject = OperationObject::createFromOperationDefinition((object)$operationDefinition); + $request = new Request(); $validatorMock ->expects($this->once()) @@ -46,8 +48,8 @@ public function willValidateRequest() ->with($request); $validatorMock ->expects($this->once()) - ->method('setOperationDefinition') - ->with($operationDefinition); + ->method('setOperationObject') + ->with($operationObject); /** @var RequestCoercer $contentDecoderMock */ $coercerMock = $this @@ -57,9 +59,7 @@ public function willValidateRequest() $processor = new RequestProcessor($validatorMock, $coercerMock); - - - $processor->process($request, $operationDefinition); + $processor->process($request, $operationObject); } /** @@ -89,14 +89,16 @@ public function willCoerceRequest() $processor = new RequestProcessor($validatorMock, $coercerMock); $operationDefinition = [ - 'parameters' => [ - [ + 'parameters' => (object)[ + (object)[ 'name' => 'myContent', 'in' => 'body' ] ] ]; - $processor->process($request, $operationDefinition); + $operationObject = OperationObject::createFromOperationDefinition((object)$operationDefinition); + + $processor->process($request, $operationObject); } } diff --git a/src/Tests/Request/RequestValidatorTest.php b/src/Tests/Request/RequestValidatorTest.php index b2fc894..f09710d 100644 --- a/src/Tests/Request/RequestValidatorTest.php +++ b/src/Tests/Request/RequestValidatorTest.php @@ -8,6 +8,7 @@ namespace KleijnWeb\SwaggerBundle\Tests\Request; +use KleijnWeb\SwaggerBundle\Document\OperationObject; use KleijnWeb\SwaggerBundle\Request\RequestValidator; use Symfony\Component\HttpFoundation\Request; @@ -21,18 +22,18 @@ class RequestValidatorTest extends \PHPUnit_Framework_TestCase */ public function canOmitParameterWhenNotExplicitlyMarkedAsRequired() { - $operationDefinition = [ + $operationDefinition = (object)[ 'parameters' => [ - [ + (object)[ 'name' => 'foo', 'in' => 'body', - 'schema' => [ + 'schema' => (object)[ 'type' => 'integer' ] ] ] ]; - $validator = new RequestValidator($operationDefinition); + $validator = new RequestValidator(OperationObject::createFromOperationDefinition($operationDefinition)); $request = new Request(); $validator->validateRequest($request); } @@ -45,9 +46,9 @@ public function cannotOmitParameterWhenExplicitlyMarkedAsRequired() { $request = new Request(); - $operationDefinition = [ + $operationDefinition = (object)[ 'parameters' => [ - [ + (object)[ 'name' => 'foo', 'required' => true, 'in' => 'query', @@ -55,7 +56,7 @@ public function cannotOmitParameterWhenExplicitlyMarkedAsRequired() ] ] ]; - $validator = new RequestValidator($operationDefinition); + $validator = new RequestValidator(OperationObject::createFromOperationDefinition($operationDefinition)); $validator->validateRequest($request); } @@ -67,9 +68,9 @@ public function cannotOmitBodyWhenExplicitlyMarkedAsRequired() { $request = new Request(); - $operationDefinition = [ + $operationDefinition = (object)[ 'parameters' => [ - [ + (object)[ 'name' => 'foo', 'required' => true, 'in' => 'query', @@ -77,7 +78,7 @@ public function cannotOmitBodyWhenExplicitlyMarkedAsRequired() ] ] ]; - $validator = new RequestValidator($operationDefinition); + $validator = new RequestValidator(OperationObject::createFromOperationDefinition($operationDefinition)); $validator->validateRequest($request); } } diff --git a/src/Tests/Response/ResponseFactoryJmsSerializerCompatibilityTest.php b/src/Tests/Response/ResponseFactoryJmsSerializerCompatibilityTest.php index 8621017..4919e1f 100644 --- a/src/Tests/Response/ResponseFactoryJmsSerializerCompatibilityTest.php +++ b/src/Tests/Response/ResponseFactoryJmsSerializerCompatibilityTest.php @@ -27,8 +27,10 @@ public function willCreateJsonResponseFromObject() { $serializer = new SerializerAdapter(JmsSerializerFactory::factory()); $factory = new ResponseFactory(new DocumentRepository(), $serializer); - - $response = $factory->createResponse(new Request(), (new JmsAnnotatedResourceStub())->setFoo('bar')); + $request = new Request(); + $request->attributes->set('_definition', 'src/Tests/Functional/PetStore/app/swagger/composite.yml'); + $request->attributes->set('_swagger_path', '/pet/{id}'); + $response = $factory->createResponse($request, (new JmsAnnotatedResourceStub())->setFoo('bar')); $expected = json_encode( ['foo' => 'bar'] diff --git a/src/Tests/Response/ResponseFactorySymfonySerializerCompatibilityTest.php b/src/Tests/Response/ResponseFactorySymfonySerializerCompatibilityTest.php index f12bde4..d5b3e4d 100644 --- a/src/Tests/Response/ResponseFactorySymfonySerializerCompatibilityTest.php +++ b/src/Tests/Response/ResponseFactorySymfonySerializerCompatibilityTest.php @@ -49,6 +49,7 @@ public function getFoo(){ return \$this->foo; } if (is_null($data)) { throw new \Exception(); } + return $data; }); @@ -59,12 +60,12 @@ public function getFoo(){ return \$this->foo; } $serializer = new SerializerAdapter(SymfonySerializerFactory::factory($jsonEncoderMock)); $factory = new ResponseFactory(new DocumentRepository(), $serializer); + $request = new Request(); + $request->attributes->set('_definition', 'src/Tests/Functional/PetStore/app/swagger/composite.yml'); + $request->attributes->set('_swagger_path', '/pet/{id}'); + $response = $factory->createResponse($request, (new $className)->setFoo('bar')); - $response = $factory->createResponse(new Request(), (new $className)->setFoo('bar')); - - $expected = json_encode( - ['foo' => 'bar'] - ); + $expected = json_encode(['foo' => 'bar']); $this->assertEquals($expected, $response->getContent()); } } diff --git a/src/Tests/Response/ResponseFactoryTest.php b/src/Tests/Response/ResponseFactoryTest.php new file mode 100644 index 0000000..c52d9ba --- /dev/null +++ b/src/Tests/Response/ResponseFactoryTest.php @@ -0,0 +1,64 @@ + + */ +class ResponseFactoryTest extends \PHPUnit_Framework_TestCase +{ + /** + * @test + */ + public function willUseFirst2xxStatusCodeFromDocument() + { + $this->assertEquals(201, $this->createResponse([], '/pet', 'POST')->getStatusCode()); + } + + /** + * @test + */ + public function willUse204ForNullResponsesWhenFoundInDocument() + { + $this->assertEquals(204, $this->createResponse(null, '/pet/{id}', 'DELETE')->getStatusCode()); + } + + /** + * @test + */ + public function willNotUse204ForNullResponsesWhenNotInDocument() + { + $this->assertNotEquals(204, $this->createResponse(null, '/pet/{id}', 'PUT')->getStatusCode()); + } + + /** + * @param mixed $data + * @param string $path + * @param string $method + * + * @return \Symfony\Component\HttpFoundation\Response + */ + private function createResponse($data, $path, $method) + { + $serializer = new SerializerAdapter(new ArraySerializer()); + $factory = new ResponseFactory(new DocumentRepository(), $serializer); + $request = new Request(); + $request->server->set('REQUEST_METHOD', $method); + $request->attributes->set('_definition', 'src/Tests/Functional/PetStore/app/swagger/composite.yml'); + $request->attributes->set('_swagger_path', $path); + + return $factory->createResponse($request, $data); + } +} diff --git a/src/Tests/Response/VndValidationErrorFactoryTest.php b/src/Tests/Response/VndValidationErrorFactoryTest.php new file mode 100644 index 0000000..8b449a4 --- /dev/null +++ b/src/Tests/Response/VndValidationErrorFactoryTest.php @@ -0,0 +1,140 @@ + + */ +class VndValidationErrorFactoryTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var VndValidationErrorFactory + */ + private $factory; + + /** + * @var ParameterRefBuilder + */ + private $refBuilder; + + protected function setUp() + { + $this->refBuilder = $this + ->getMockBuilder('KleijnWeb\SwaggerBundle\Document\ParameterRefBuilder') + ->disableOriginalConstructor() + ->getMock(); + $this->factory = new VndValidationErrorFactory($this->refBuilder); + } + + /** + * @test + */ + public function createdErrorCanHaveLogRef() + { + $vndError = $this->factory->create( + $this->createSimpleRequest(), + new InvalidParametersException('Yikes', []), + 123456789 + ); + $this->assertInstanceOf('Ramsey\VndError\VndError', $vndError); + $this->assertSame(123456789, $vndError->getLogref()); + } + + /** + * @test + */ + public function createdErrorCanHaveNoLogRef() + { + $this->assertNull( + $this->factory->create( + $this->createSimpleRequest(), + new InvalidParametersException('Yikes', []) + )->getLogref() + ); + } + + /** + * @test + */ + public function resultIncludesErrorMessagesCreatedByJsonSchema() + { + $value = (object)[ + 'foo' => (object)[ + 'blah' => 'one' + ], + 'bar' => (object)[] + ]; + $validator = new Validator(); + $schema = (object)[ + 'type' => 'object', + 'required' => ['foo', 'bar'], + 'properties' => (object)[ + 'foo' => (object)[ + 'type' => 'object', + 'properties' => (object)[ + 'blah' => (object)[ + 'type' => 'integer' + ] + ] + ], + 'bar' => (object)[ + 'type' => 'object', + 'required' => ['blah'], + 'properties' => (object)[ + 'blah' => (object)[ + 'type' => 'string' + ] + ] + ] + ] + ]; + $validator->check($value, $schema); + $errors = $validator->getErrors(); + + $exception = new InvalidParametersException('Nope', $errors); + + $mock = $this->refBuilder; + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + $mock + ->expects($this->exactly(2)) + ->method('buildSpecificationLink') + ->willReturnOnConsecutiveCalls('http://1.net/1', 'http://2.net/2'); + + $vndError = $this->factory->create( + $this->createSimpleRequest(), + $exception + ); + + $resources = $vndError->getResources(); + $this->assertArrayHasKey('errors', $resources); + $errorResources = $resources['errors']; + $this->assertSame(count($errors), count($errorResources)); + + $resources = array_values($errorResources); + + foreach ($errors as $i => $spec) { + $data = $resources[$i]->getData(); + $this->assertContains($spec['message'], $data['message']); + } + } + + /** + * @return Request + */ + private function createSimpleRequest() + { + return new Request; + } +} diff --git a/src/Tests/Routing/SwaggerRouteLoaderTest.php b/src/Tests/Routing/SwaggerRouteLoaderTest.php index 1c598d2..96c1fd4 100644 --- a/src/Tests/Routing/SwaggerRouteLoaderTest.php +++ b/src/Tests/Routing/SwaggerRouteLoaderTest.php @@ -6,10 +6,9 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Tests\Dev\Routing; +namespace KleijnWeb\SwaggerBundle\Tests\Routing; use KleijnWeb\SwaggerBundle\Routing\SwaggerRouteLoader; -use Symfony\Component\Routing\Route; /** * @author John Kleijn @@ -84,10 +83,10 @@ public function canLoadMultipleDocuments() */ public function loadingMultipleDocumentWillPreventRouteKeyCollisions() { - $pathDefinitions = [ - '/a' => ['get' => []], - '/a/b' => ['get' => [], 'post' => []], - '/a/b/c' => ['put' => []], + $pathDefinitions = (object)[ + '/a' => (object)['get' => (object)[]], + '/a/b' => (object)['get' => (object)[], 'post' => (object)[]], + '/a/b/c' => (object)['put' => (object)[]], ]; $this->documentMock @@ -134,9 +133,9 @@ public function willReturnRouteCollection() */ public function routeCollectionWillContainOneRouteForEveryPathAndMethod() { - $pathDefinitions = [ - '/a' => ['get' => [], 'post' => []], - '/b' => ['get' => []], + $pathDefinitions = (object)[ + '/a' => (object)['get' => (object)[], 'post' => (object)[]], + '/b' => (object)['get' => (object)[]], ]; $this->documentMock @@ -154,10 +153,10 @@ public function routeCollectionWillContainOneRouteForEveryPathAndMethod() */ public function routeCollectionWillIncludeSeparateRoutesForSubPaths() { - $pathDefinitions = [ - '/a' => ['get' => []], - '/a/b' => ['get' => []], - '/a/b/c' => ['get' => []], + $pathDefinitions = (object)[ + '/a' => (object)['get' => (object)[]], + '/a/b' => (object)['get' => (object)[]], + '/a/b/c' => (object)['get' => (object)[]], ]; $this->documentMock @@ -176,14 +175,12 @@ public function routeCollectionWillIncludeSeparateRoutesForSubPaths() public function canUseDiKeyAsOperationId() { $expected = 'my.controller.key:methodName'; - $pathDefinitions = [ + $pathDefinitions = (object)[ '/a' => [ - 'get' => [], - 'post' => [ - 'operationId' => $expected - ] + 'get' => (object)[], + 'post' => (object)['operationId' => $expected] ], - '/b' => ['get' => []], + '/b' => (object)['get' => (object)[]], ]; $this->documentMock @@ -203,10 +200,10 @@ public function canUseDiKeyAsOperationId() */ public function routeCollectionWillIncludeSeparateRoutesForSubPathMethodCombinations() { - $pathDefinitions = [ - '/a' => ['get' => []], - '/a/b' => ['get' => [], 'post' => []], - '/a/b/c' => ['put' => []], + $pathDefinitions = (object)[ + '/a' => (object)['get' => (object)[]], + '/a/b' => (object)['get' => (object)[], 'post' => (object)[]], + '/a/b/c' => (object)['put' => (object)[]], ]; $this->documentMock @@ -224,14 +221,14 @@ public function routeCollectionWillIncludeSeparateRoutesForSubPathMethodCombinat */ public function routeCollectionWillContainPathFromSwaggerDoc() { - $pathDefinitions = [ - '/a' => ['get' => []], - '/a/b' => ['get' => []], - '/a/b/c' => ['get' => []], - '/d/f/g' => ['get' => []], - '/1/2/3' => ['get' => []], - '/foo/{bar}/{blah}' => ['get' => []], - '/z' => ['get' => []], + $pathDefinitions = (object)[ + '/a' => (object)['get' => (object)[]], + '/a/b' => (object)['get' => (object)[]], + '/a/b/c' => (object)['get' => (object)[]], + '/d/f/g' => (object)['get' => (object)[]], + '/1/2/3' => (object)['get' => (object)[]], + '/foo/{bar}/{blah}' => (object)['get' => (object)[]], + '/z' => (object)['get' => (object)[]], ]; $this->documentMock @@ -241,7 +238,7 @@ public function routeCollectionWillContainPathFromSwaggerDoc() $routes = $this->loader->load(self::DOCUMENT_PATH); - $definitionPaths = array_keys($pathDefinitions); + $definitionPaths = array_keys((array)$pathDefinitions); sort($definitionPaths); $routePaths = array_map(function ($route) { return $route->getPath(); @@ -255,11 +252,11 @@ public function routeCollectionWillContainPathFromSwaggerDoc() */ public function willAddRequirementsForIntegerPathParams() { - $pathDefinitions = [ - '/a' => [ - 'get' => [ - 'parameters' => [ - ['name' => 'foo', 'in' => 'path', 'type' => 'integer'] + $pathDefinitions = (object)[ + '/a' => (object)[ + 'get' => (object)[ + 'parameters' => (object)[ + (object)['name' => 'foo', 'in' => 'path', 'type' => 'integer'] ] ] ], @@ -274,7 +271,7 @@ public function willAddRequirementsForIntegerPathParams() ->expects($this->once()) ->method('getOperationDefinition') ->with('/a', 'get') - ->willReturn($pathDefinitions['/a']['get']); + ->willReturn($pathDefinitions->{'/a'}->get); $routes = $this->loader->load(self::DOCUMENT_PATH); $actual = $routes->get('swagger.path.a.get'); @@ -291,11 +288,11 @@ public function willAddRequirementsForIntegerPathParams() public function willAddRequirementsForStringPatternParams() { $expected = '\d{2}hello'; - $pathDefinitions = [ - '/a' => [ - 'get' => [ - 'parameters' => [ - ['name' => 'aString', 'in' => 'path', 'type' => 'string', 'pattern' => $expected] + $pathDefinitions = (object)[ + '/a' => (object)[ + 'get' => (object)[ + 'parameters' => (object)[ + (object)['name' => 'aString', 'in' => 'path', 'type' => 'string', 'pattern' => $expected] ] ] ], @@ -310,7 +307,7 @@ public function willAddRequirementsForStringPatternParams() ->expects($this->once()) ->method('getOperationDefinition') ->with('/a', 'get') - ->willReturn($pathDefinitions['/a']['get']); + ->willReturn($pathDefinitions->{'/a'}->get); $routes = $this->loader->load(self::DOCUMENT_PATH); $actual = $routes->get('swagger.path.a.get'); @@ -328,11 +325,11 @@ public function willAddRequirementsForStringEnumParams() { $enum = ['a', 'b', 'c']; $expected = '(a|b|c)'; - $pathDefinitions = [ - '/a' => [ - 'get' => [ - 'parameters' => [ - ['name' => 'aString', 'in' => 'path', 'type' => 'string', 'enum' => $enum] + $pathDefinitions = (object)[ + '/a' => (object)[ + 'get' => (object)[ + 'parameters' => (object)[ + (object)['name' => 'aString', 'in' => 'path', 'type' => 'string', 'enum' => $enum] ] ] ], @@ -347,7 +344,7 @@ public function willAddRequirementsForStringEnumParams() ->expects($this->once()) ->method('getOperationDefinition') ->with('/a', 'get') - ->willReturn($pathDefinitions['/a']['get']); + ->willReturn($pathDefinitions->{'/a'}->get); $routes = $this->loader->load(self::DOCUMENT_PATH); $actual = $routes->get('swagger.path.a.get'); diff --git a/src/Tests/Dev/Test/ApiRequestTest.php b/src/Tests/Test/ApiRequestTest.php similarity index 86% rename from src/Tests/Dev/Test/ApiRequestTest.php rename to src/Tests/Test/ApiRequestTest.php index 0d9041a..5ea719e 100644 --- a/src/Tests/Dev/Test/ApiRequestTest.php +++ b/src/Tests/Test/ApiRequestTest.php @@ -6,9 +6,9 @@ * file that was distributed with this source code. */ -namespace KleijnWeb\SwaggerBundle\Tests\Dev\Test; +namespace KleijnWeb\SwaggerBundle\Tests\Test; -use KleijnWeb\SwaggerBundle\Dev\Test\ApiRequest; +use KleijnWeb\SwaggerBundle\Test\ApiRequest; /** * @author John Kleijn