Skip to content

Commit

Permalink
Merge pull request #2 from kynx/date-interval-hydrator
Browse files Browse the repository at this point in the history
Add DateIntervalHydrator
  • Loading branch information
kynx authored Oct 25, 2023
2 parents df3a978 + 900032e commit ddf445e
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 2 deletions.
105 changes: 105 additions & 0 deletions src/Hydrator/DateIntervalHydrator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

namespace Kynx\Mezzio\OpenApi\Hydrator;

use DateInterval;
use Exception;
use Kynx\Mezzio\OpenApi\Hydrator\Exception\ExtractionException;
use Kynx\Mezzio\OpenApi\Hydrator\Exception\HydrationException;

use function assert;
use function current;
use function is_array;
use function is_string;

/**
* @see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
*/
final class DateIntervalHydrator implements HydratorInterface
{
/** @var array<string, string> */
private static array $dateMap = [
'y' => 'Y',
'm' => 'M',
'd' => 'D',
];

/** @var array<string, string> */
private static array $timeMap = [
'h' => 'H',
'i' => 'M',
's' => 'S',
];

public static function hydrate(mixed $data): DateInterval
{
if (is_array($data)) {
/** @var mixed $data */
$data = current($data);
}

assert(is_string($data));
// @todo Catch `DateMalformedIntervalStringException` instead once PHP8.2 support dropped
try {
return new DateInterval($data);
} catch (Exception $exception) {
throw HydrationException::fromThrowable(DateInterval::class, $exception);
}
}

public static function extract(mixed $object): string
{
if (! $object instanceof DateInterval) {
throw ExtractionException::invalidObject($object, DateInterval::class);
}

if ($object->invert) {
throw ExtractionException::unexpectedValue($object, "cannot extract inverted intervals");
}

if ($object->f > 0) {
throw ExtractionException::unexpectedValue($object, "cannot extract intervals with microseconds");
}

$duration = 'P';
foreach (self::$dateMap as $property => $designator) {
$duration .= self::getDesignatorValue(
$object,
$property,
$designator,
"cannot extract negative date intervals"
);
}
$time = '';
foreach (self::$timeMap as $property => $designator) {
$time .= self::getDesignatorValue(
$object,
$property,
$designator,
"cannot extract negative time intervals"
);
}

return $duration . ($time === '' ? '' : 'T' . $time);
}

private static function getDesignatorValue(
DateInterval $interval,
string $property,
string $designator,
string $error
): string {
/** @var int $value */
$value = $interval->$property;
if ($value < 0) {
throw ExtractionException::unexpectedValue($interval, $error);
}
if ($value === 0) {
return '';
}

return $value . $designator;
}
}
12 changes: 10 additions & 2 deletions src/Hydrator/Exception/ExtractionException.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@ private function __construct(string $message = "", int $code = 0, ?Throwable $pr
}

public static function invalidObject(mixed $object, string $expected): self
{
return self::unexpectedValue($object, sprintf(
"expected object of type %s",
$expected
));
}

public static function unexpectedValue(mixed $object, string $message): self
{
return new self(sprintf(
"Cannot extract %s: expected object of type %s",
"Cannot extract %s: %s",
get_debug_type($object),
$expected
$message
), 500);
}
}
106 changes: 106 additions & 0 deletions test/Hydrator/DateIntervalHydratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace KynxTest\Mezzio\OpenApi\Hydrator;

use DateInterval;
use Kynx\Mezzio\OpenApi\Hydrator\DateIntervalHydrator;
use Kynx\Mezzio\OpenApi\Hydrator\Exception\ExtractionException;
use Kynx\Mezzio\OpenApi\Hydrator\Exception\HydrationException;
use PHPUnit\Framework\TestCase;

/**
* @covers \Kynx\Mezzio\OpenApi\Hydrator\DateIntervalHydrator
*/
final class DateIntervalHydratorTest extends TestCase
{
/**
* @dataProvider durationProvider
*/
public function testHydrateReturnsValidDateInterval(mixed $duration, DateInterval $expected): void
{
$actual = DateIntervalHydrator::hydrate($duration);
self::assertEquals($expected, $actual);
}

/**
* @return array<string, array{0: string|list<string>, 1: DateInterval}>
*/
public static function durationProvider(): array
{
return [
'string' => ['P1Y', new DateInterval('P1Y')],
'array' => [['P1Y', 'invalid'], new DateInterval('P1Y')],
];
}

public function testHydrateInvalidThrowsException(): void
{
self::expectException(HydrationException::class);
DateIntervalHydrator::hydrate('invalid');
}

/**
* @dataProvider invalidIntervalProvider
*/
public function testExtractInvalidDateIntervalThrowsException(mixed $interval, string $expected): void
{
self::expectException(ExtractionException::class);
self::expectExceptionMessage($expected);
DateIntervalHydrator::extract($interval);
}

/**
* @return array<string, array{0: string|DateInterval, 1: string}>
*/
public static function invalidIntervalProvider(): array
{
$inverted = new DateInterval('P1Y');
$inverted->invert = 1; // note - this is supposed to be readonly, but can't figure out another way to set it

// phpcs:disable Generic.Files.LineLength.TooLong
return [
'not DateInterval' => ['foo', 'expected object of type ' . DateInterval::class],
'inverted' => [$inverted, 'cannot extract inverted intervals'],
'microseconds' => [DateInterval::createFromDateString('5 microseconds'), 'cannot extract intervals with microseconds'],
'negative date' => [DateInterval::createFromDateString('-1 year'), 'cannot extract negative date intervals'],
'negative time' => [DateInterval::createFromDateString('-1 hour'), 'cannot extract negative time intervals'],
];
// phpcs:enable
}

/**
* @dataProvider validIntervalProvider
*/
public function testExtractReturnsDuration(string $interval): void
{
$dateInterval = new DateInterval($interval);
$actual = DateIntervalHydrator::extract($dateInterval);
self::assertEquals($interval, $actual);
}

/**
* @return array<string, array{0: string}>
*/
public static function validIntervalProvider(): array
{
return [
'year' => ['P1Y'],
'month' => ['P2M'],
'day' => ['P3D'],
'hour' => ['PT4H'],
'minute' => ['PT5M'],
'second' => ['PT6S'],
'complex' => ['P1Y2M3DT4H5M6S'],
];
}

public function testExtractHandlesWeeks(): void
{
$expected = 'P14D';
$interval = new DateInterval('P2W');
$actual = DateIntervalHydrator::extract($interval);
self::assertSame($expected, $actual);
}
}

0 comments on commit ddf445e

Please sign in to comment.