Skip to content

Commit

Permalink
Merge pull request #16 from sitegeist/stringPropertyFormats
Browse files Browse the repository at this point in the history
String property formats
  • Loading branch information
nezaniel authored May 6, 2024
2 parents fdc1db7 + 6ac16a0 commit 6dc3da4
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 64 deletions.
2 changes: 1 addition & 1 deletion Classes/Application/ParameterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static function resolveParameters(string $className, string $methodName,
$parameterValueFromRequest = $parameterAttribute->style->decodeParameterValue($parameterValueFromRequest);
}

$parameters[$parameter->name] = SchemaDenormalizer::denormalizeValue($parameterValueFromRequest, $type->getName());
$parameters[$parameter->name] = SchemaDenormalizer::denormalizeValue($parameterValueFromRequest, $type->getName(), $parameter);
}

return $parameters;
Expand Down
52 changes: 52 additions & 0 deletions Classes/Domain/Metadata/StringProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Sitegeist\SchemeOnYou\Domain\Metadata;

use Neos\Flow\Annotations as Flow;

/**
* @see https://swagger.io/docs/specification/data-models/data-types/#string
*/
#[Flow\Proxy(false)]
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final readonly class StringProperty
{
public const FORMAT_DATE = 'date';
public const FORMAT_DATE_TIME = 'date-time';
public const FORMAT_PASSWORD = 'password';
public const FORMAT_BYTE = 'byte';
public const FORMAT_BINARY = 'binary';

public function __construct(
public ?string $format = null,
public ?string $description = null,
) {
}

public static function tryFromReflectionParameter(\ReflectionParameter $reflectionParameter): ?self
{
$parameterAttributes = $reflectionParameter->getAttributes(self::class);
switch (count($parameterAttributes)) {
case 0:
return null;
case 1:
$arguments = $parameterAttributes[0]->getArguments();

return new self(
$arguments['format'] ?? $arguments[0] ?? null,
$arguments['description'] ?? $arguments[1] ?? null,
);
default:
throw new \DomainException(
'There must be no or exactly one string property attribute declared in parameter '
. $reflectionParameter->getDeclaringClass()?->name
. '::' . $reflectionParameter->getDeclaringFunction()->name
. '::' . $reflectionParameter->name
. ', ' . count($parameterAttributes) . ' given',
1715000621
);
}
}
}
30 changes: 21 additions & 9 deletions Classes/Domain/Schema/OpenApiSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Neos\Flow\Annotations as Flow;
use Sitegeist\SchemeOnYou\Domain\Metadata\Schema as SchemaMetadata;
use Sitegeist\SchemeOnYou\Domain\Metadata\StringProperty;

#[Flow\Proxy(false)]
final readonly class OpenApiSchema implements \JsonSerializable
Expand Down Expand Up @@ -47,8 +48,8 @@ private static function fromReflectionEnum(\ReflectionEnum $reflection): self
$definitionMetadata = SchemaMetadata::fromReflectionClass($reflection);
return match ($reflection->getBackingType()?->getName()) {
'string' => new self(
name: $definitionMetadata->name ?: $reflection->getShortName(),
type: 'string',
name: $definitionMetadata->name ?: $reflection->getShortName(),
description: $definitionMetadata->description,
enum: array_map(
/** @phpstan-ignore-next-line parameter and return types are enforced before */
Expand All @@ -57,8 +58,8 @@ enum: array_map(
)
),
'int' => new self(
name: $definitionMetadata->name ?: $reflection->getShortName(),
type: 'integer',
name: $definitionMetadata->name ?: $reflection->getShortName(),
description: $definitionMetadata->description,
enum: array_map(
/** @phpstan-ignore-next-line parameter and return types are enforced before */
Expand Down Expand Up @@ -87,11 +88,16 @@ public static function fromReflectionParameter(\ReflectionParameter $reflection)
'float' => 'number',
default => throw new \DomainException('Unsupported type ' . $typeName)
},
format: match ($typeName) {
'string' => StringProperty::tryfromReflectionParameter($reflection)?->format ?: null,
default => null,
}
);
} elseif (in_array($typeName, [\DateTime::class, \DateTimeImmutable::class])) {
$propertyAttribute = StringProperty::tryfromReflectionParameter($reflection);
return new self(
type: 'string',
format: 'date-time',
format: $propertyAttribute?->format ?: 'date-time',
);
} elseif ($typeName === \DateInterval::class) {
return new self(
Expand Down Expand Up @@ -148,8 +154,8 @@ private static function fromCollectionReflectionClass(\ReflectionClass $reflecti
$parameterSchema = self::fromReflectionClass(new \ReflectionClass($parameterClassName));

return new self(
name: $definitionMetadata->name ?: $reflection->getShortName(),
type: 'array',
name: $definitionMetadata->name ?: $reflection->getShortName(),
description: $definitionMetadata->description,
items: $parameterSchema->toReference()
);
Expand All @@ -169,8 +175,8 @@ private static function fromObjectReflectionClass(\ReflectionClass $reflectionCl
$singleConstructorParameter->getType() instanceof \ReflectionNamedType
&& $singleConstructorParameter->name === 'value'
) {
$propertyAttribute = StringProperty::tryfromReflectionParameter($singleConstructorParameter);
return new self(
name: $schemaMetadata->name ?: $reflectionClass->getShortName(),
type: match ($singleConstructorParameter->getType()->getName()) {
'string', 'DateTimeImmutable', 'DateTime', 'DateInterval' => 'string',
'int' => 'integer',
Expand All @@ -182,10 +188,12 @@ private static function fromObjectReflectionClass(\ReflectionClass $reflectionCl
. ' of class ' . $reflectionClass->name
)
},
name: $schemaMetadata->name ?: $reflectionClass->getShortName(),
description: $schemaMetadata->description,
format: match ($singleConstructorParameter->getType()->getName()) {
'DateTimeImmutable' => 'date-time',
'DateTimeImmutable' => $propertyAttribute?->format ?: 'date-time',
'DateInterval' => 'duration',
'string' => $propertyAttribute?->format ?: null,
default => null
},
);
Expand All @@ -204,7 +212,10 @@ private static function fromObjectReflectionClass(\ReflectionClass $reflectionCl
);
}
$properties[$reflectionParameter->name] = match (get_class($type)) {
\ReflectionNamedType::class => SchemaType::selfOrReferenceFromReflectionNamedType($type),
\ReflectionNamedType::class => SchemaType::selfOrReferenceFromReflectionNamedType(
$type,
$reflectionParameter
),
\ReflectionUnionType::class => [
'oneOf' => array_map(
fn (\ReflectionType $singleType): SchemaType|OpenApiReference
Expand All @@ -217,7 +228,8 @@ private static function fromObjectReflectionClass(\ReflectionClass $reflectionCl
1709560366
),
\ReflectionNamedType::class => SchemaType::selfOrReferenceFromReflectionNamedType(
$singleType
$singleType,
$reflectionParameter,
),
default => throw new \DomainException('wat')
},
Expand All @@ -244,8 +256,8 @@ private static function fromObjectReflectionClass(\ReflectionClass $reflectionCl
}

return new self(
name: $schemaMetadata->name ?: $reflectionClass->getShortName(),
type: 'object',
name: $schemaMetadata->name ?: $reflectionClass->getShortName(),
description: $schemaMetadata->description,
properties: $properties,
required: $required
Expand Down
47 changes: 32 additions & 15 deletions Classes/Domain/Schema/SchemaDenormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@
namespace Sitegeist\SchemeOnYou\Domain\Schema;

use Neos\Flow\Reflection\ClassReflection;
use Sitegeist\SchemeOnYou\Domain\Metadata\StringProperty;

class SchemaDenormalizer
{
/**
* @param int|bool|string|float|array<mixed>|null $value
* @return object|int|bool|string|float|null
*/
public static function denormalizeValue(null|int|bool|string|float|array $value, string $targetType): object|int|bool|string|float|null
public static function denormalizeValue(null|int|bool|string|float|array $value, string $targetType, ?\ReflectionParameter $reflectionParameter = null): object|int|bool|string|float|null
{
return self::convertValue($value, $targetType);
return self::convertValue($value, $targetType, $reflectionParameter);
}

/**
* @param null|int|bool|string|float|array<mixed> $value
* @return object|int|bool|string|float|null
*/
private static function convertValue(null|int|bool|string|float|array $value, string $targetType): object|int|bool|string|float|null
private static function convertValue(null|int|bool|string|float|array $value, string $targetType, ?\ReflectionParameter $reflectionParameter = null): object|int|bool|string|float|null
{
if ($value === null) {
return null;
Expand All @@ -37,9 +38,9 @@ private static function convertValue(null|int|bool|string|float|array $value, st
} elseif ($targetType === 'bool') {
return (bool) $value;
} elseif ($targetType === \DateTime::class) {
return self::convertDateTime($value);
return self::convertDateTime($value, $reflectionParameter);
} elseif ($targetType === \DateTimeImmutable::class) {
return self::convertDateTimeImmutable($value);
return self::convertDateTimeImmutable($value, $reflectionParameter);
} elseif ($targetType === \DateInterval::class) {
return self::convertDateInterval($value);
} elseif (
Expand Down Expand Up @@ -87,20 +88,20 @@ private static function convertValueObject(array|int|float|string|bool $value, s
$parameterReflections = $reflection->getConstructor()->getParameters();
$convertedArguments = [];
if (is_array($value)) {
foreach ($parameterReflections as $name => $parameter) {
$type = $parameter->getType();
if ($parameter->isDefaultValueAvailable() && !array_key_exists($parameter->getName(), $value)) {
foreach ($parameterReflections as $name => $reflectionParameter) {
$type = $reflectionParameter->getType();
if ($reflectionParameter->isDefaultValueAvailable() && !array_key_exists($reflectionParameter->getName(), $value)) {
continue;
}
$convertedArguments[$name] = match (true) {
$type === null => throw new \DomainException('Cannot convert untyped property ' . $parameter->getName()),
$type instanceof \ReflectionNamedType => self::convertValue($value[$parameter->getName()], $type->getName()),
$type === null => throw new \DomainException('Cannot convert untyped property ' . $reflectionParameter->getName()),
$type instanceof \ReflectionNamedType => self::convertValue($value[$reflectionParameter->getName()], $type->getName(), $reflectionParameter),
default => throw new \DomainException('Cannot convert ' . get_class($type) . ' yet'),
};
}
return new $targetType(...$convertedArguments);
} elseif (count($parameterReflections) === 1 && $parameterReflections[0]->getName() === 'value' && $parameterReflections[0]->getType() instanceof \ReflectionNamedType) {
$convertedValue = self::convertValue($value, $parameterReflections[0]->getType()->getName());
$convertedValue = self::convertValue($value, $parameterReflections[0]->getType()->getName(), $parameterReflections[0]);
return new $targetType(value: $convertedValue);
}
throw new \DomainException('Only single value objects can be serialized as single value');
Expand All @@ -109,30 +110,46 @@ private static function convertValueObject(array|int|float|string|bool $value, s
/**
* @param array<string,mixed>|int|float|string|bool $value
*/
protected static function convertDateTime(array|float|bool|int|string $value): \DateTime
protected static function convertDateTime(array|float|bool|int|string $value, ?\ReflectionParameter $reflectionParameter = null): \DateTime
{
$propertyAttribute = $reflectionParameter ? StringProperty::tryFromReflectionParameter($reflectionParameter) : null;
$format = match ($propertyAttribute?->format) {
StringProperty::FORMAT_DATE => 'Y-m-d',
default => \DateTimeInterface::RFC3339
};
$converted = match (true) {
is_string($value) => \DateTime::createFromFormat(\DateTimeInterface::RFC3339, $value),
is_string($value) => \DateTime::createFromFormat($format, $value),
default => false,
};
if ($converted === false) {
throw new \DomainException('Can only denormalize \DateTime from an RFC 3339 string');
}
if ($propertyAttribute?->format === StringProperty::FORMAT_DATE) {
$converted->setTime(0, 0, 0);
}
return $converted;
}

/**
* @param array<string,mixed>|int|float|string|bool $value
*/
protected static function convertDateTimeImmutable(array|float|bool|int|string $value): \DateTimeImmutable
protected static function convertDateTimeImmutable(array|float|bool|int|string $value, ?\ReflectionParameter $reflectionParameter = null): \DateTimeImmutable
{
$propertyAttribute = $reflectionParameter ? StringProperty::tryFromReflectionParameter($reflectionParameter) : null;
$format = match ($propertyAttribute?->format) {
StringProperty::FORMAT_DATE => 'Y-m-d',
default => \DateTimeInterface::RFC3339
};
$converted = match (true) {
is_string($value) => \DateTimeImmutable::createFromFormat(\DateTimeInterface::RFC3339, $value),
is_string($value) => \DateTimeImmutable::createFromFormat($format, $value),
default => false,
};
if ($converted === false) {
throw new \DomainException('Can only denormalize \DateTimeImmutable from an RFC 3339 string');
}
if ($propertyAttribute?->format === StringProperty::FORMAT_DATE) {
$converted = $converted->setTime(0, 0, 0);
}
return $converted;
}

Expand Down
Loading

0 comments on commit 6dc3da4

Please sign in to comment.