diff --git a/README.md b/README.md index 6491ebf..a634eb6 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ open_api_server: type: yaml # Specification format, either yaml or json. If omitted, the specification file extension will be used. name_space: PetStore # Namespace for generated DTOs and Interfaces media_type: 'application/json' # media type from the specification files to use for generating request and response DTOs + #date_time_class: '\Carbon\CarbonImmutable' # FQCN which implements \DateTimeInterface. + ## If set up, then generated DTOs will return instances of this class in DateTime parameters ``` Add your OpenApi specifications to the application routes configuration file using standard `resource` keyword diff --git a/src/CodeGenerator/PhpParserGenerators/CodeGenerator.php b/src/CodeGenerator/PhpParserGenerators/CodeGenerator.php index 5211e87..be417a1 100644 --- a/src/CodeGenerator/PhpParserGenerators/CodeGenerator.php +++ b/src/CodeGenerator/PhpParserGenerators/CodeGenerator.php @@ -63,6 +63,10 @@ public function getTypeName(FileBuilder $builder, PropertyDefinition $definition return $builder->getReference($objectType); } + if ($definition->getSpecProperty()->getOutputType() !== null) { + return $definition->getSpecProperty()->getOutputType(); + } + if ($scalarType === null) { throw new Exception('One of ObjectTypeDefinition and ScalarTypeId should not be null'); } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 07a8b06..c106206 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -33,6 +33,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->isRequired() ->cannotBeEmpty() ->end() + ->scalarNode('date_time_class')->end() ->end() ->end() ->end(); diff --git a/src/DependencyInjection/OpenApiServerExtension.php b/src/DependencyInjection/OpenApiServerExtension.php index fea53b2..6a8d03b 100644 --- a/src/DependencyInjection/OpenApiServerExtension.php +++ b/src/DependencyInjection/OpenApiServerExtension.php @@ -40,7 +40,8 @@ public function load(array $configs, ContainerBuilder $container): void * path: string, * type?: string, * name_space: string, - * media_type: string + * media_type: string, + * date_time_class?: string, * } * } $config */ diff --git a/src/Exception/CannotParseOpenApi.php b/src/Exception/CannotParseOpenApi.php index 809f3b5..4e08891 100644 --- a/src/Exception/CannotParseOpenApi.php +++ b/src/Exception/CannotParseOpenApi.php @@ -112,4 +112,14 @@ public static function becauseTypeNotSupported(string $propertyName, string $typ ) ); } + + public static function becauseUnknownType(string $name): self + { + return new self(sprintf('Class "%s" does not exist', $name)); + } + + public static function becauseNotFQCN(string $name): self + { + return new self(sprintf('Class "%s" should have fully qualified name', $name)); + } } diff --git a/src/Serializer/ArrayDtoSerializer.php b/src/Serializer/ArrayDtoSerializer.php index 9343b94..3d4fe9c 100644 --- a/src/Serializer/ArrayDtoSerializer.php +++ b/src/Serializer/ArrayDtoSerializer.php @@ -113,8 +113,10 @@ private function convert(bool $deserialize, array $source, ObjectSchema $params) /** @psalm-suppress MissingClosureParamType */ $converter = fn ($v) => $this->convert($deserialize, $v, $objectType->getSchema()); } else { + $outputClass = $property->getOutputType(); + /** @psalm-suppress MissingClosureParamType */ - $converter = fn ($v) => $this->resolver->convert($deserialize, $typeId ?? 0, $v); + $converter = fn ($v) => $this->resolver->convert($deserialize, $typeId ?? 0, $v, $outputClass); } if ($property->isArray()) { diff --git a/src/Specification/Definitions/Property.php b/src/Specification/Definitions/Property.php index a346983..3063da0 100644 --- a/src/Specification/Definitions/Property.php +++ b/src/Specification/Definitions/Property.php @@ -17,6 +17,7 @@ final class Property private ObjectSchema|ObjectReference|null $objectTypeDefinition = null; private ?string $description = null; private ?string $pattern = null; + private ?string $outputType = null; public function __construct(string $name) { @@ -147,4 +148,16 @@ public function setNullable(bool $nullable): self return $this; } + + public function getOutputType(): ?string + { + return $this->outputType; + } + + public function setOutputType(?string $outputType): self + { + $this->outputType = $outputType; + + return $this; + } } diff --git a/src/Specification/Definitions/SpecificationConfig.php b/src/Specification/Definitions/SpecificationConfig.php index b984fb9..0a32229 100644 --- a/src/Specification/Definitions/SpecificationConfig.php +++ b/src/Specification/Definitions/SpecificationConfig.php @@ -10,13 +10,15 @@ final class SpecificationConfig private ?string $type; private string $nameSpace; private string $mediaType; + private ?string $dateTimeClass; - public function __construct(string $path, ?string $type, string $nameSpace, string $mediaType) + public function __construct(string $path, ?string $type, string $nameSpace, string $mediaType, ?string $dateTimeClass = null) { - $this->path = $path; - $this->type = $type; - $this->nameSpace = $nameSpace; - $this->mediaType = $mediaType; + $this->path = $path; + $this->type = $type; + $this->nameSpace = $nameSpace; + $this->mediaType = $mediaType; + $this->dateTimeClass = $dateTimeClass; } public function getPath(): string @@ -38,4 +40,9 @@ public function getMediaType(): string { return $this->mediaType; } + + public function getDateTimeClass(): ?string + { + return $this->dateTimeClass; + } } diff --git a/src/Specification/SpecificationLoader.php b/src/Specification/SpecificationLoader.php index dc03486..704246f 100644 --- a/src/Specification/SpecificationLoader.php +++ b/src/Specification/SpecificationLoader.php @@ -45,7 +45,7 @@ public function __construct(SpecificationParser $parser, FileLocatorInterface $l } /** - * @param array{path:string,type:string|null,name_space:string,media_type:string} $spec + * @param array{path:string,type:string|null,name_space:string,media_type:string,date_time_class:string|null} $spec */ public function registerSpec(string $name, array $spec): void { @@ -53,7 +53,8 @@ public function registerSpec(string $name, array $spec): void $spec['path'], $spec['type'] ?? null, $spec['name_space'], - $spec['media_type'] + $spec['media_type'], + $spec['date_time_class'] ?? null ); } diff --git a/src/Specification/SpecificationParser.php b/src/Specification/SpecificationParser.php index 7612e35..65ba479 100644 --- a/src/Specification/SpecificationParser.php +++ b/src/Specification/SpecificationParser.php @@ -15,6 +15,7 @@ use cebe\openapi\spec\Responses; use cebe\openapi\spec\Schema; use cebe\openapi\spec\Type; +use DateTimeInterface; use OnMoon\OpenApiServerBundle\Exception\CannotParseOpenApi; use OnMoon\OpenApiServerBundle\Specification\Definitions\ComponentArray; use OnMoon\OpenApiServerBundle\Specification\Definitions\ObjectReference; @@ -30,8 +31,10 @@ use function array_key_exists; use function array_map; use function array_merge; +use function class_exists; use function count; use function in_array; +use function is_a; use function is_array; use function is_int; use function Safe\preg_match; @@ -44,12 +47,12 @@ class SpecificationParser private ScalarTypesResolver $typeResolver; /** @var string[] */ private array $skipHttpCodes; + private ?string $dateTimeClass = null; /** @param array $skipHttpCodes */ public function __construct(ScalarTypesResolver $typeResolver, array $skipHttpCodes) { - $this->typeResolver = $typeResolver; - + $this->typeResolver = $typeResolver; $this->skipHttpCodes = array_map(static fn ($code) => (string) $code, $skipHttpCodes); } @@ -58,6 +61,9 @@ public function parseOpenApi(string $specificationName, SpecificationConfig $spe $componentSchemas = new ComponentArray(); $operationDefinitions = []; + + $this->dateTimeClass = $specificationConfig->getDateTimeClass(); + /** * @var string $url */ @@ -385,6 +391,26 @@ private function getProperty( if (Type::isScalar($itemProperty->type)) { $scalarTypeId = $this->typeResolver->findScalarType($itemProperty->type, $itemProperty->format); $propertyDefinition->setScalarTypeId($scalarTypeId); + + if ($this->typeResolver->isDateTime($scalarTypeId) && $this->dateTimeClass !== null) { + if (preg_match('/^\\\\/', $this->dateTimeClass) !== 1) { + throw CannotParseOpenApi::becauseNotFQCN($this->dateTimeClass); + } + + if (! class_exists($this->dateTimeClass)) { + throw CannotParseOpenApi::becauseUnknownType($this->dateTimeClass); + } + + if (is_a($this->dateTimeClass, DateTimeInterface::class, true) === false) { + throw CannotParseOpenApi::becauseTypeNotSupported( + $propertyName, + $this->dateTimeClass, + $exceptionContext + ); + } + + $propertyDefinition->setOutputType($this->dateTimeClass); + } } elseif ($itemProperty->type === Type::OBJECT) { $objectType = $this->getObjectSchema( $itemProperty, diff --git a/src/Types/ScalarTypesResolver.php b/src/Types/ScalarTypesResolver.php index f48fd43..5b17ab5 100644 --- a/src/Types/ScalarTypesResolver.php +++ b/src/Types/ScalarTypesResolver.php @@ -71,7 +71,7 @@ public function __construct() * * @return mixed */ - public function convert(bool $deserialize, int $id, $value) + public function convert(bool $deserialize, int $id, $value, ?string $outputClass = null) { if ($value === null) { return null; @@ -80,7 +80,7 @@ public function convert(bool $deserialize, int $id, $value) $format = $this->scalarTypes[$id]; if ($deserialize && isset($format['deserializer'])) { - return TypeSerializer::{$format['deserializer']}($value); + return TypeSerializer::{$format['deserializer']}($value, $outputClass); } if (! $deserialize && isset($format['serializer'])) { diff --git a/src/Types/TypeSerializer.php b/src/Types/TypeSerializer.php index 654b7d8..a78f702 100644 --- a/src/Types/TypeSerializer.php +++ b/src/Types/TypeSerializer.php @@ -4,31 +4,75 @@ namespace OnMoon\OpenApiServerBundle\Types; -use DateTime; +use DateTimeInterface; +use Exception; +use Safe\DateTime; +use Safe\Exceptions\DatetimeException; use function base64_encode; +use function error_get_last; +use function method_exists; use function Safe\base64_decode; +use function sprintf; class TypeSerializer { - public static function deserializeDate(string $date): DateTime + private const DATE_FORMAT = 'Y-m-d'; + private const DATETIME_FORMAT = 'c'; + + /** + * @psalm-param class-string $dateTimeClass + * + * @template T of DateTimeInterface + */ + public static function deserializeDate(string $date, ?string $dateTimeClass = null): DateTimeInterface { - return \Safe\DateTime::createFromFormat('Y-m-d', $date); + if ($dateTimeClass === null) { + return DateTime::createFromFormat(self::DATE_FORMAT, $date); + } + + if (method_exists($dateTimeClass, 'createFromFormat') === false) { + throw new Exception(sprintf( + 'Method createFromFormat does not exist in class %s', + $dateTimeClass + )); + } + + /** @psalm-suppress UndefinedMethod */ + $deserializedDate = $dateTimeClass::createFromFormat(self::DATE_FORMAT, $date); + + if ($deserializedDate === false) { + $error = error_get_last(); + + throw new DatetimeException($error['message'] ?? 'An error occurred'); + } + + return $deserializedDate; } - public static function serializeDate(DateTime $date): string + public static function serializeDate(DateTimeInterface $date): string { - return $date->format('Y-m-d'); + return $date->format(self::DATE_FORMAT); } - public static function deserializeDateTime(string $date): DateTime + /** + * @psalm-param class-string $dateTimeClass + * + * @template T of DateTimeInterface + */ + public static function deserializeDateTime(string $date, ?string $dateTimeClass = null): DateTimeInterface { - return new \Safe\DateTime($date); + if ($dateTimeClass === null) { + return new DateTime($date); + } + + /** @psalm-suppress InvalidStringClass */ + return new $dateTimeClass($date); } - public static function serializeDateTime(DateTime $date): string + public static function serializeDateTime(DateTimeInterface $date): string { - return $date->format('c'); + return $date->format(self::DATETIME_FORMAT); } public static function deserializeByte(string $data): string diff --git a/test/functional/DependencyInjection/OpenApiServerExtensionTest.php b/test/functional/DependencyInjection/OpenApiServerExtensionTest.php index 68530f7..40aac38 100644 --- a/test/functional/DependencyInjection/OpenApiServerExtensionTest.php +++ b/test/functional/DependencyInjection/OpenApiServerExtensionTest.php @@ -70,7 +70,14 @@ public function testLoadServiceDefinitionWithMethodCall(): void 'generated_dir_permissions' => '0444', 'full_doc_blocks' => true, 'send_nulls' => true, - 'specs' => [['path' => 'test', 'name_space' => 'test', 'media_type' => 'application/json']], + 'specs' => [ + [ + 'path' => 'test', + 'name_space' => 'test', + 'media_type' => 'application/json', + 'date_time_class' => 'TestClass', + ], + ], ]); $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( @@ -78,7 +85,12 @@ public function testLoadServiceDefinitionWithMethodCall(): void 'registerSpec', [ 0, - ['path' => 'test', 'name_space' => 'test', 'media_type' => 'application/json'], + [ + 'path' => 'test', + 'name_space' => 'test', + 'media_type' => 'application/json', + 'date_time_class' => 'TestClass', + ], ] ); } diff --git a/test/unit/CodeGenerator/AttributeGeneratorTest.php b/test/unit/CodeGenerator/AttributeGeneratorTest.php index 06eb805..7e9dfba 100644 --- a/test/unit/CodeGenerator/AttributeGeneratorTest.php +++ b/test/unit/CodeGenerator/AttributeGeneratorTest.php @@ -195,9 +195,9 @@ public function testRequestPassDefault(): void $property->setDefaultValue('test'); $propertyTwo = new Property('two'); - $propertyTwo->setNullable(true); - $propertyTwo->setRequired(true); - $propertyTwo->setDefaultValue('testTwo'); + $propertyTwo->setNullable(false); + $propertyTwo->setRequired(false); + $propertyTwo->setDefaultValue(null); $propertyDefinition = new PropertyDefinition($property); $propertyDefinitionTwo = new PropertyDefinition($propertyTwo); diff --git a/test/unit/CodeGenerator/PhpParserGenerators/CodeGeneratorTest.php b/test/unit/CodeGenerator/PhpParserGenerators/CodeGeneratorTest.php index 07cda2a..bc1add5 100644 --- a/test/unit/CodeGenerator/PhpParserGenerators/CodeGeneratorTest.php +++ b/test/unit/CodeGenerator/PhpParserGenerators/CodeGeneratorTest.php @@ -85,6 +85,18 @@ public function testGetTypeNameReturnsClassViaObjectType(): void Assert::assertEquals($expectedClassName, $typeName); } + public function testGetTypeNameReturnsCustomDateTimeClass(): void + { + $expectedClassName = 'TestCustomDateTimeClass'; + $property = new Property('test'); + $property->setOutputType($expectedClassName); + $propertyDefinition = new PropertyDefinition($property); + $fileBuilderMock = $this->createMock(FileBuilder::class); + $typeName = $this->codeGenerator->getTypeName($fileBuilderMock, $propertyDefinition); + + Assert::assertEquals($expectedClassName, $typeName); + } + public function testGetTypeNameThrowsException(): void { $propertyDefinition = new PropertyDefinition(new Property('test')); diff --git a/test/unit/Specification/SpecificationParserTest.php b/test/unit/Specification/SpecificationParserTest.php index eda7f8b..c43b9dc 100644 --- a/test/unit/Specification/SpecificationParserTest.php +++ b/test/unit/Specification/SpecificationParserTest.php @@ -24,6 +24,7 @@ use PHPUnit\Framework\TestCase; use function array_map; +use function sprintf; /** * @covers \OnMoon\OpenApiServerBundle\Specification\SpecificationParser @@ -288,6 +289,101 @@ public function testParseOpenApiSuccess(): void ); } + public function testParseOpenApiWithCustomDateTimeClassSuccess(): void + { + $customDateTimeClass = '\DateTimeImmutable'; + $specificationNameOne = 'SomeCustomSpecificationOne'; + $specificationConfigOne = new SpecificationConfig( + '/some/custom/specification/one/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + $customDateTimeClass + ); + $specificationNameTwo = 'SomeCustomSpecificationTwo'; + $specificationConfigTwo = new SpecificationConfig( + '/some/custom/specification/two/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + null + ); + $parsedSpecification = new OpenApi([ + 'paths' => new Paths([ + '/some/custom/url' => [ + 'post' => new Operation([ + 'operationId' => 'SomeCustomOperationWithRequestAndResponses', + 'requestBody' => new RequestBody([ + 'description' => 'SomeCustomRequestParam', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + 'responses' => new Responses([ + '200' => new Response([ + 'description' => 'SomeCustomResponseParam200', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + ]), + ]), + ], + ]), + ]); + + $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); + + $specificationOne = $specificationParser->parseOpenApi( + $specificationNameOne, + $specificationConfigOne, + $parsedSpecification + ); + + $specificationOneRequestBody = $specificationOne + ->getOperation('SomeCustomOperationWithRequestAndResponses') + ->getRequestBody(); + Assert::assertNotNull($specificationOneRequestBody); + + $specificationOneRequestBodyProperties = $specificationOneRequestBody->getProperties(); + Assert::assertSame($customDateTimeClass, $specificationOneRequestBodyProperties[0]->getOutputType()); + + $specificationTwo = $specificationParser->parseOpenApi( + $specificationNameTwo, + $specificationConfigTwo, + $parsedSpecification + ); + $specificationTwoRequestBody = $specificationTwo + ->getOperation('SomeCustomOperationWithRequestAndResponses') + ->getRequestBody(); + Assert::assertNotNull($specificationTwoRequestBody); + + $specificationTwoRequestBodyProperties = $specificationTwoRequestBody->getProperties(); + Assert::assertNull($specificationTwoRequestBodyProperties[0]->getOutputType()); + } + public function testParseOpenApiSuccessRequestBadMediaType(): void { $specificationName = 'SomeCustomSpecification'; @@ -927,4 +1023,222 @@ public function testParseOpenApiThrowExceptionDuplicateOperationId(): void $parsedSpecification ); } + + public function testParseOpenApiWithCustomDateTimeClassThrowExceptionUnknownType(): void + { + $someNotExistedDateTimeClass = '\SomeNotExistedDateTimeClass'; + $specificationName = 'SomeCustomSpecification'; + $specificationConfig = new SpecificationConfig( + '/some/custom/specification/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + $someNotExistedDateTimeClass + ); + $parsedSpecification = new OpenApi([ + 'paths' => new Paths([ + '/some/custom/url' => [ + 'post' => new Operation([ + 'operationId' => 'SomeCustomOperationWithRequestAndResponses', + 'requestBody' => new RequestBody([ + 'description' => 'SomeCustomRequestParam', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + 'responses' => new Responses([ + '200' => new Response([ + 'description' => 'SomeCustomResponseParam200', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + ]), + ]), + ], + ]), + ]); + + $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); + + $this->expectException(CannotParseOpenApi::class); + $this->expectExceptionMessage(sprintf( + 'Class "%s" does not exist', + $someNotExistedDateTimeClass + )); + + $specificationParser->parseOpenApi( + $specificationName, + $specificationConfig, + $parsedSpecification + ); + } + + public function testParseOpenApiWithCustomDateTimeClassThrowExceptionTypeNotSupported(): void + { + $specificationName = 'SomeCustomSpecification'; + $someNotDateTimeClass = '\stdClass'; + $specificationConfig = new SpecificationConfig( + '/some/custom/specification/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + $someNotDateTimeClass + ); + $parsedSpecification = new OpenApi([ + 'paths' => new Paths([ + '/some/custom/url' => [ + 'post' => new Operation([ + 'operationId' => 'SomeCustomOperationWithRequestAndResponses', + 'requestBody' => new RequestBody([ + 'description' => 'SomeCustomRequestParam', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + 'responses' => new Responses([ + '200' => new Response([ + 'description' => 'SomeCustomResponseParam200', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + ]), + ]), + ], + ]), + ]); + + $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); + + $this->expectException(CannotParseOpenApi::class); + $this->expectExceptionMessage(sprintf( + 'Cannot generate property for DTO class, property "someDateTimeProperty" type "%s" is not supported ' . + 'in response (code "200") for operation: "post" of path: "/some/custom/url" in specification file: ' . + '"/some/custom/specification/path".', + $someNotDateTimeClass + )); + + $specificationParser->parseOpenApi( + $specificationName, + $specificationConfig, + $parsedSpecification + ); + } + + public function testParseOpenApiWithCustomDateTimeClassThrowExceptionNotFQCN(): void + { + $someNotFqcnClassName = 'SomeNotFqcnClassName'; + $specificationName = 'SomeCustomSpecification'; + $specificationConfig = new SpecificationConfig( + '/some/custom/specification/path', + null, + '\\Some\\Custom\\Namespace', + 'application/json', + $someNotFqcnClassName + ); + $parsedSpecification = new OpenApi([ + 'paths' => new Paths([ + '/some/custom/url' => [ + 'post' => new Operation([ + 'operationId' => 'SomeCustomOperationWithRequestAndResponses', + 'requestBody' => new RequestBody([ + 'description' => 'SomeCustomRequestParam', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + 'responses' => new Responses([ + '200' => new Response([ + 'description' => 'SomeCustomResponseParam200', + 'content' => [ + 'application/json' => new MediaType([ + 'schema' => new Schema([ + 'type' => Type::OBJECT, + 'properties' => [ + 'someDateTimeProperty' => new Schema([ + 'type' => Type::STRING, + 'format' => 'date-time', + 'default' => 1605101247, + ]), + ], + ]), + ]), + ], + ]), + ]), + ]), + ], + ]), + ]); + + $specificationParser = new SpecificationParser(new ScalarTypesResolver(), []); + + $this->expectException(CannotParseOpenApi::class); + $this->expectExceptionMessage(sprintf( + 'Class "%s" should have fully qualified name', + $someNotFqcnClassName + )); + + $specificationParser->parseOpenApi( + $specificationName, + $specificationConfig, + $parsedSpecification + ); + } } diff --git a/test/unit/Types/TypeSerializerTest.php b/test/unit/Types/TypeSerializerTest.php index 7001c20..33458bd 100644 --- a/test/unit/Types/TypeSerializerTest.php +++ b/test/unit/Types/TypeSerializerTest.php @@ -5,12 +5,15 @@ namespace OnMoon\OpenApiServerBundle\Test\Unit\Types; use DateTime; +use DateTimeImmutable; use OnMoon\OpenApiServerBundle\Types\TypeSerializer; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Safe\Exceptions\DatetimeException; use Throwable; +use function sprintf; + /** * @covers \OnMoon\OpenApiServerBundle\Types\TypeSerializer */ @@ -24,6 +27,14 @@ public function testDeserializeDateReturnsDateTime(): void Assert::assertEquals($expectedDate, $deserializedDate); } + public function testDeserializeDateWithCustomDateTimeClassReturnsDateTime(): void + { + $dateString = '2020-05-12'; + $expectedDate = DateTime::createFromFormat('Y-m-d', $dateString); + $deserializedDate = TypeSerializer::deserializeDate($dateString, DateTimeImmutable::class); + Assert::assertEquals($expectedDate, $deserializedDate); + } + public function testDeserializeDateThrowsException(): void { $dateString = '22-07-2020'; @@ -31,6 +42,29 @@ public function testDeserializeDateThrowsException(): void TypeSerializer::deserializeDate($dateString); } + public function testDeserializeDateWithCustomDateTimeClassThrowsException(): void + { + $dateString = '22-07-2020'; + $this->expectException(DatetimeException::class); + TypeSerializer::deserializeDate($dateString, DateTimeImmutable::class); + } + + public function testDeserializeDateWithCustomDateTimeClassThrowsNotExistMethodException(): void + { + $dateString = '2020-05-12'; + $wrongClass = $this + ->getMockBuilder(DateTimeImmutable::class) + ->setMockClassName('WrongClass') + ->disableProxyingToOriginalMethods(); + + $this->expectException(Throwable::class); + $this->expectExceptionMessage(sprintf( + 'Method createFromFormat does not exist in class %s', + $wrongClass::class + )); + TypeSerializer::deserializeDate($dateString, $wrongClass::class); + } + public function testSerializeDateReturnsSerializedDate(): void { $dateString = '25-12-2020'; @@ -48,6 +82,14 @@ public function testDeserializeDateTimeReturnsDateTime(): void Assert::assertEquals($expectedDateTime, $deserializedDateTime); } + public function testDeserializeDateTimeWithCustomDateTimeClassReturnsDateTime(): void + { + $dateString = '25-12-2020 12:01:55'; + $expectedDateTime = new \Safe\DateTime($dateString); + $deserializedDateTime = TypeSerializer::deserializeDateTime($dateString, DateTimeImmutable::class); + Assert::assertEquals($expectedDateTime, $deserializedDateTime); + } + public function testDeserializeDateTimeThrowsException(): void { $dateString = 'wrong date string';