diff --git a/features/swagger/doc.feature b/features/swagger/doc.feature index ef824dfea65..2637aa66f25 100644 --- a/features/swagger/doc.feature +++ b/features/swagger/doc.feature @@ -13,7 +13,6 @@ Feature: Documentation support # Root properties And the JSON node "info.title" should be equal to "My Dummy API" And the JSON node "info.description" should be equal to "This is a test API." - #And the JSON node "host" should be equal to "exemple.com" And the JSON node "basePath" should be equal to "/" # Supported classes And the Swagger class "CircularReference" exist diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 63a87ab06d5..b26ea843a12 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -68,6 +68,7 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('api_platform.title', $config['title']); $container->setParameter('api_platform.description', $config['description']); + $container->setParameter('api_platform.version', $config['version']); $container->setParameter('api_platform.formats', $formats); $container->setParameter('api_platform.collection.order', $config['collection']['order']); $container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']); diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index 4461bf1438f..346566f65a8 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -33,6 +33,7 @@ public function getConfigTreeBuilder() ->children() ->scalarNode('title')->defaultValue('')->info('The title of the API.')->end() ->scalarNode('description')->defaultValue('')->info('The description of the API.')->end() + ->scalarNode('version')->defaultValue('0.0.0')->info('The version of the API.')->end() ->arrayNode('formats') ->defaultValue(['jsonld' => ['mime_types' => ['application/ld+json']]]) ->info('The list of enabled formats. The first one will be the default.') diff --git a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml index 38a63e5c9f3..89108324757 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/swagger.xml @@ -16,11 +16,10 @@ + %api_platform.formats% %api_platform.title% %api_platform.description% - 2.0 - %router.request_context.host% - %router.request_context.scheme% + %api_platform.version% diff --git a/src/Swagger/ApiDocumentationBuilder.php b/src/Swagger/ApiDocumentationBuilder.php index 27d7f738d92..8ecc5289673 100644 --- a/src/Swagger/ApiDocumentationBuilder.php +++ b/src/Swagger/ApiDocumentationBuilder.php @@ -34,6 +34,8 @@ */ final class ApiDocumentationBuilder implements ApiDocumentationBuilderInterface { + const SWAGGER_VERSION = '2.0'; + private $resourceNameCollectionFactory; private $resourceMetadataFactory; private $propertyNameCollectionFactory; @@ -46,11 +48,9 @@ final class ApiDocumentationBuilder implements ApiDocumentationBuilderInterface private $description; private $iriConverter; private $version; - private $host; - private $schema; - const SWAGGER_VERSION = '2.0'; + private $formats; - public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, UrlGeneratorInterface $urlGenerator, IriConverterInterface $iriConverter, string $title, string $description, string $version = null, string $host, string $schema) + public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, UrlGeneratorInterface $urlGenerator, IriConverterInterface $iriConverter, array $formats, string $title, string $description, string $version = null) { $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; $this->resourceMetadataFactory = $resourceMetadataFactory; @@ -60,12 +60,11 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName $this->resourceClassResolver = $resourceClassResolver; $this->operationMethodResolver = $operationMethodResolver; $this->urlGenerator = $urlGenerator; + $this->formats = array_keys($formats); $this->title = $title; $this->description = $description; $this->iriConverter = $iriConverter; $this->version = $version; - $this->host = $host; - $this->schema[] = $schema; } /** @@ -74,11 +73,12 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName public function getApiDocumentation() { $classes = []; - $itemOperations = []; - $itemOperations['operation'] = []; + $operation = []; + $operation['item'] = []; + $operation['collection'] = []; $itemOperationsDocs = []; - $properties = []; + $definitions = []; foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); @@ -114,51 +114,61 @@ public function getApiDocumentation() $context['serializer_groups'] = isset($context['serializer_groups']) ? array_merge($context['serializer_groups'], $attributes['denormalization_context']['groups']) : $context['serializer_groups']; } + $definitions[$shortName] = ['type' => 'object']; foreach ($this->propertyNameCollectionFactory->create($resourceClass, $context) as $propertyName) { $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); - if ($propertyMetadata->isIdentifier() && !$propertyMetadata->isWritable()) { continue; } - $range = $this->getRange($propertyMetadata); - - $property[$propertyName] = [ - 'type' => $range, - ]; - if (is_array($range)) { - $property[$propertyName] = $range; + if ($propertyMetadata->isRequired()) { + $definitions[$shortName]['required'][] = $propertyName; } - $required = []; - if ($propertyMetadata->isRequired()) { - $required = array_merge($required, [$propertyName]); + $range = $this->getRange($propertyMetadata); + if (null === $range) { + continue; } - if (!empty($required)) { - $properties[$shortName]['required'] = $required; + if ($propertyMetadata->getDescription()) { + $definitions[$shortName]['properties'][$propertyName]['description'] = $propertyMetadata->getDescription(); } - $properties[$shortName]['type'] = 'object'; - $properties[$shortName]['properties'] = $property; + if ($range['complex']) { + $definitions[$shortName]['properties'][$propertyName] = ['$ref' => $range['value']]; + } else { + $definitions[$shortName]['properties'][$propertyName] = [ + 'type' => $range['value'], + ]; + } } if ($operations = $resourceMetadata->getItemOperations()) { foreach ($operations as $operationName => $itemOperation) { - $swaggerOperation = $this->getSwaggerOperation($resourceClass, $resourceMetadata, $operationName, $itemOperation, $prefixedShortName, false); - $itemOperations['operation'] = array_merge($itemOperations['operation'], $swaggerOperation); + $swaggerOperation = $this->getSwaggerOperation($resourceClass, $resourceMetadata, $operationName, $itemOperation, $prefixedShortName, false, $definitions); + $operation['item'] = array_merge($operation['item'], $swaggerOperation); + } + } + + if ($operations = $resourceMetadata->getCollectionOperations()) { + foreach ($operations as $operationName => $collectionOperation) { + $swaggerOperation = $this->getSwaggerOperation($resourceClass, $resourceMetadata, $operationName, $collectionOperation, $prefixedShortName, true, $definitions); + $operation['collection'] = array_merge($operation['collection'], $swaggerOperation); } } + try { $resourceClassIri = $this->iriConverter->getIriFromResourceClass($resourceClass); + $itemOperationsDocs[$resourceClassIri] = $operation['collection']; + + $resourceClassIri .= '/{id}'; + + $itemOperationsDocs[$resourceClassIri] = $operation['item']; } catch (InvalidArgumentException $e) { - $resourceClassIri = '/nopaths'; } - $resourceClassIri .= '/{id}'; - $itemOperationsDocs[$resourceClassIri] = $itemOperations['operation']; $classes[] = $class; } @@ -171,12 +181,10 @@ public function getApiDocumentation() $doc['info']['description'] = $this->description; } $doc['info']['version'] = $this->version ?? '0.0.0'; - $doc['host'] = $this->host; $doc['basePath'] = $this->urlGenerator->generate('api_jsonld_entrypoint'); - $doc['definitions'] = $properties; + $doc['definitions'] = $definitions; $doc['externalDocs'] = ['description' => 'Find more about API Platform', 'url' => 'https://api-platform.com']; $doc['tags'] = $classes; - $doc['schemes'] = $this->schema; // more schema ? $doc['paths'] = $itemOperationsDocs; return $doc; @@ -185,30 +193,43 @@ public function getApiDocumentation() /** * Gets and populates if applicable a Swagger operation. */ - private function getSwaggerOperation(string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $operation, string $prefixedShortName, bool $collection) : array + private function getSwaggerOperation(string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $operation, string $prefixedShortName, bool $collection, array $properties) : array { if ($collection) { $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName); } else { $method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName); } + $methodSwagger = strtolower($method); $swaggerOperation = $operation['swagger_context'] ?? []; $shortName = $resourceMetadata->getShortName(); $swaggerOperation[$methodSwagger] = []; $swaggerOperation[$methodSwagger]['tags'] = [$shortName]; - $swaggerOperation[$methodSwagger]['produces'] = ['application/ld+json']; + $swaggerOperation[$methodSwagger]['produces'] = $this->formats; $swaggerOperation[$methodSwagger]['consumes'] = $swaggerOperation[$methodSwagger]['produces']; + switch ($method) { case 'GET': if ($collection) { if (!isset($swaggerOperation[$methodSwagger]['title'])) { $swaggerOperation[$methodSwagger]['summary'] = sprintf('Retrieves the collection of %s resources.', $shortName); } + if ($this->resourceClassResolver->isResourceClass($resourceClass)) { + $swaggerOperation[$methodSwagger]['parameters'][] = [ + 'in' => 'body', + 'name' => 'body', + 'description' => sprintf('%s resource to be added', $shortName), + 'schema' => [ + '$ref' => sprintf('#/definitions/%s', $shortName), + ], + ]; + } } else { if (!isset($swaggerOperation[$methodSwagger]['title'])) { $swaggerOperation[$methodSwagger]['summary'] = sprintf('Retrieves %s resource.', $shortName); } + $swaggerOperation[$methodSwagger]['parameters'][] = [ 'name' => 'id', 'in' => 'path', @@ -225,8 +246,9 @@ private function getSwaggerOperation(string $resourceClass, ResourceMetadata $re if (!isset($swaggerOperation[$methodSwagger]['title'])) { $swaggerOperation[$methodSwagger]['summary'] = sprintf('Creates a %s resource.', $shortName); } - if ($this->resourceClassResolver->isResourceClass($shortName)) { - $swaggerOperation[$methodSwagger]['parameters'] = [ + + if ($this->resourceClassResolver->isResourceClass($resourceClass)) { + $swaggerOperation[$methodSwagger]['parameters'][] = [ 'in' => 'body', 'name' => 'body', 'description' => sprintf('%s resource to be added', $shortName), @@ -246,29 +268,25 @@ private function getSwaggerOperation(string $resourceClass, ResourceMetadata $re if (!isset($swaggerOperation[$methodSwagger]['title'])) { $swaggerOperation[$methodSwagger]['summary'] = sprintf('Replaces the %s resource.', $shortName); } - $swaggerOperation[$methodSwagger]['parameters'] = [[ - 'name' => 'id', - 'in' => 'path', - 'required' => true, - 'type' => 'integer', - ]]; - if ($this->resourceClassResolver->isResourceClass($shortName)) { - $swaggerOperation[$methodSwagger]['parameters'] = [[ - 'name' => 'id', - 'in' => 'path', - 'required' => true, - 'type' => 'integer', - ], + + if ($this->resourceClassResolver->isResourceClass($resourceClass)) { + $swaggerOperation[$methodSwagger]['parameters'] = [ [ - 'in' => 'body', - 'name' => 'body', - 'description' => sprintf('%s resource to be added', $shortName), - 'schema' => [ - '$ref' => sprintf('#/definitions/%s', $shortName), + 'name' => 'id', + 'in' => 'path', + 'required' => true, + 'type' => 'integer', ], - ], ]; + [ + 'in' => 'body', + 'name' => 'body', + 'description' => sprintf('%s resource to be added', $shortName), + 'schema' => [ + '$ref' => sprintf('#/definitions/%s', $shortName), + ], + ], + ]; } - $swaggerOperation[$methodSwagger]['responses'] = [ '200' => ['description' => 'Valid ID'], ]; @@ -296,9 +314,9 @@ private function getSwaggerOperation(string $resourceClass, ResourceMetadata $re * * @param PropertyMetadata $propertyMetadata * - * @return string|null + * @return array|null */ - private function getRange(PropertyMetadata $propertyMetadata) + private function getRange(PropertyMetadata $propertyMetadata) : array { $type = $propertyMetadata->getType(); if (!$type) { @@ -311,35 +329,39 @@ private function getRange(PropertyMetadata $propertyMetadata) switch ($type->getBuiltinType()) { case Type::BUILTIN_TYPE_STRING: - return 'string'; + return ['complex' => false, 'value' => 'string']; case Type::BUILTIN_TYPE_INT: - return 'integer'; + return ['complex' => false, 'value' => 'integer']; case Type::BUILTIN_TYPE_FLOAT: - return 'number'; + return ['complex' => false, 'value' => 'number']; case Type::BUILTIN_TYPE_BOOL: - return 'boolean'; + return ['complex' => false, 'value' => 'boolean']; case Type::BUILTIN_TYPE_OBJECT: $className = $type->getClassName(); + if (null === $className) { + return; + } - if (null !== $className) { - $reflection = new \ReflectionClass($className); - if ($reflection->implementsInterface(\DateTimeInterface::class)) { - return 'string'; - } + if (is_subclass_of($className, \DateTimeInterface::class)) { + return ['complex' => false, 'value' => 'string']; + } - $className = $type->getClassName(); - if ($this->resourceClassResolver->isResourceClass($className)) { - return ['$ref' => sprintf('#/definitions/%s', $this->resourceMetadataFactory->create($className)->getShortName())]; - } + if (!$this->resourceClassResolver->isResourceClass($className)) { + return; } - break; + + if ($propertyMetadata->isReadableLink()) { + return ['complex' => true, 'value' => sprintf('#/definitions/%s', $this->resourceMetadataFactory->create($className)->getShortName())]; + } + + return ['complex' => false, 'value' => 'string']; + default: - return 'null'; - break; + return ['complex' => false, 'value' => 'null']; } } } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 4b90dc6fb99..ff23dca6f91 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -32,6 +32,7 @@ class ApiPlatformExtensionTest extends \PHPUnit_Framework_TestCase 'api_platform' => [ 'title' => 'title', 'description' => 'description', + 'version' => 'version', ], ]; @@ -172,6 +173,7 @@ private function getContainerBuilderProphecy() $parameters = [ 'api_platform.title' => 'title', 'api_platform.description' => 'description', + 'api_platform.version' => 'version', 'api_platform.formats' => ['application/ld+json' => 'jsonld'], 'api_platform.collection.order' => null, 'api_platform.collection.order_parameter_name' => 'order', diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 2fe86407519..99ad63ce7c3 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -26,13 +26,14 @@ public function testDefaultConfig() $configuration = new Configuration(); $treeBuilder = $configuration->getConfigTreeBuilder(); $processor = new Processor(); - $config = $processor->processConfiguration($configuration, ['api_platform' => ['title' => 'title', 'description' => 'description']]); + $config = $processor->processConfiguration($configuration, ['api_platform' => ['title' => 'title', 'description' => 'description', 'version' => '1.0.0']]); $this->assertInstanceOf(ConfigurationInterface::class, $configuration); $this->assertInstanceOf(TreeBuilder::class, $treeBuilder); $this->assertEquals([ 'title' => 'title', 'description' => 'description', + 'version' => '1.0.0', 'formats' => ['jsonld' => ['mime_types' => ['application/ld+json']]], 'naming' => [ 'resource_path_naming_strategy' => 'api_platform.naming.resource_path_naming_strategy.underscore', diff --git a/tests/Swagger/ApiDocumentationBuilderTest.php b/tests/Swagger/ApiDocumentationBuilderTest.php new file mode 100644 index 00000000000..69c7ee72735 --- /dev/null +++ b/tests/Swagger/ApiDocumentationBuilderTest.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\Swagger; + +use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Core\Api\OperationMethodResolverInterface; +use ApiPlatform\Core\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\Api\UrlGeneratorInterface; +use ApiPlatform\Core\JsonLd\ContextBuilderInterface; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; +use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Core\Swagger\ApiDocumentationBuilder; +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Amrouche Hamza + */ +class ApiDocumentationBuilderTest extends \PHPUnit_Framework_TestCase /**/ +{ + public function testGetApiDocumention() + { + $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class); + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + $titre = 'Test Api'; + $desc = 'test ApiGerard'; + $iriConverter = $this->prophesize(IriConverterInterface::class); + $version = '1.0.0'; + $host = 'http://exemple.com'; + $schema = 'http'; + $formats = ['application/ld+json' => 'ld+json']; + $dummyMetadata = new ResourceMetadata('dummy', 'dummy', '#dummy', ['get' => ['method' => 'GET'], 'put' => ['method' => 'PUT']], ['get' => ['method' => 'GET'], 'post' => ['method' => 'POST']], []); + $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['dummy' => 'dummy']))->shouldBeCalled(); + $resourceMetadataFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn($dummyMetadata); + $propertyNameCollectionFactoryProphecy->create('dummy', [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['name'])); + $propertyMetadataFactoryProphecy->create('dummy', 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'name', true, true, true, true, false, false, null, [])); + $operationMethodResolverProphecy->getItemOperationMethod('dummy', 'get')->shouldBeCalled()->willReturn('get'); + $operationMethodResolverProphecy->getItemOperationMethod('dummy', 'put')->shouldBeCalled()->willReturn('put'); + $operationMethodResolverProphecy->getCollectionOperationMethod('dummy', 'get')->shouldBeCalled()->willReturn('get'); + $operationMethodResolverProphecy->getCollectionOperationMethod('dummy', 'post')->shouldBeCalled()->willReturn('post'); + $iriConverter->getIriFromResourceClass('dummy')->shouldBeCalled()->willReturn('/dummies'); + $apiDocumentationBuilder = new ApiDocumentationBuilder($resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $contextBuilderProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $operationMethodResolverProphecy->reveal(), $urlGeneratorProphecy->reveal(), $iriConverter->reveal(), $formats, $titre, $desc); + + $swaggerDocumentation = $apiDocumentationBuilder->getApiDocumentation(); + $this->assertEquals($swaggerDocumentation['swagger'], 2.0); + $this->assertEquals($swaggerDocumentation['info']['title'], $titre); + $this->assertEquals($swaggerDocumentation['info']['description'], $desc); + $this->assertEquals($swaggerDocumentation['definitions'], ['dummy' => ['type' => 'object', 'properties' => ['name' => ['type' => 'string']]]]); + $this->assertEquals($swaggerDocumentation['externalDocs' + ], ['description' => 'Find more about API Platform', 'url' => 'https://api-platform.com']); + + $this->assertEquals($swaggerDocumentation['paths']['/dummies']['get'], [ + 'tags' => [0 => 'dummy'], + 'produces' => ['application/ld+json'], + 'consumes' => ['application/ld+json'], + ] + ); + + $this->assertEquals($swaggerDocumentation['paths']['/dummies']['post'], [ + 'tags' => [0 => 'dummy'], + 'produces' => ['application/ld+json'], + 'consumes' => ['application/ld+json'], + ] + ); + $this->assertEquals($swaggerDocumentation['paths']['/dummies/{id}']['get'], [ + 'tags' => [0 => 'dummy'], + 'produces' => ['application/ld+json'], + 'consumes' => ['application/ld+json'], + ] + ); + $this->assertEquals($swaggerDocumentation['paths']['/dummies/{id}']['put'], [ + 'tags' => [0 => 'dummy'], + 'produces' => ['application/ld+json'], + 'consumes' => ['application/ld+json'], + ] + ); + } +}