Skip to content

Commit

Permalink
182 add date time interface param (#183)
Browse files Browse the repository at this point in the history
* Added dateTimeClass specification parameter (#182)

* fixed time serialize format

* Refactoring and tests

* Code style fixes

* Fixed mutation test

* mutation test

* Fixed FQCN bug and multiple custom datetime class bug. Added tests

* Added settings example in readme file. Changed type in dateTime serialization and deserialization methods
  • Loading branch information
msalakhov authored Dec 23, 2023
1 parent 1aec01f commit f85a01f
Show file tree
Hide file tree
Showing 17 changed files with 518 additions and 27 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/CodeGenerator/PhpParserGenerators/CodeGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->isRequired()
->cannotBeEmpty()
->end()
->scalarNode('date_time_class')->end()
->end()
->end()
->end();
Expand Down
3 changes: 2 additions & 1 deletion src/DependencyInjection/OpenApiServerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
10 changes: 10 additions & 0 deletions src/Exception/CannotParseOpenApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
4 changes: 3 additions & 1 deletion src/Serializer/ArrayDtoSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
13 changes: 13 additions & 0 deletions src/Specification/Definitions/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
}
}
17 changes: 12 additions & 5 deletions src/Specification/Definitions/SpecificationConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,4 +40,9 @@ public function getMediaType(): string
{
return $this->mediaType;
}

public function getDateTimeClass(): ?string
{
return $this->dateTimeClass;
}
}
5 changes: 3 additions & 2 deletions src/Specification/SpecificationLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ 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
{
$this->specs[$name] = new SpecificationConfig(
$spec['path'],
$spec['type'] ?? null,
$spec['name_space'],
$spec['media_type']
$spec['media_type'],
$spec['date_time_class'] ?? null
);
}

Expand Down
30 changes: 28 additions & 2 deletions src/Specification/SpecificationParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -44,12 +47,12 @@ class SpecificationParser
private ScalarTypesResolver $typeResolver;
/** @var string[] */
private array $skipHttpCodes;
private ?string $dateTimeClass = null;

/** @param array<array-key, string|int> $skipHttpCodes */
public function __construct(ScalarTypesResolver $typeResolver, array $skipHttpCodes)
{
$this->typeResolver = $typeResolver;

$this->typeResolver = $typeResolver;
$this->skipHttpCodes = array_map(static fn ($code) => (string) $code, $skipHttpCodes);
}

Expand All @@ -58,6 +61,9 @@ public function parseOpenApi(string $specificationName, SpecificationConfig $spe
$componentSchemas = new ComponentArray();

$operationDefinitions = [];

$this->dateTimeClass = $specificationConfig->getDateTimeClass();

/**
* @var string $url
*/
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/Types/ScalarTypesResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'])) {
Expand Down
62 changes: 53 additions & 9 deletions src/Types/TypeSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> $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<T> $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
Expand Down
16 changes: 14 additions & 2 deletions test/functional/DependencyInjection/OpenApiServerExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,27 @@ 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(
SpecificationLoader::class,
'registerSpec',
[
0,
['path' => 'test', 'name_space' => 'test', 'media_type' => 'application/json'],
[
'path' => 'test',
'name_space' => 'test',
'media_type' => 'application/json',
'date_time_class' => 'TestClass',
],
]
);
}
Expand Down
6 changes: 3 additions & 3 deletions test/unit/CodeGenerator/AttributeGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions test/unit/CodeGenerator/PhpParserGenerators/CodeGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
Loading

0 comments on commit f85a01f

Please sign in to comment.