Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

182 add date time interface param #183

Merged
merged 8 commits into from
Dec 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading