-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from kynx/date-interval-hydrator
Add DateIntervalHydrator
- Loading branch information
Showing
3 changed files
with
221 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |