diff --git a/composer.json b/composer.json index a417955..cba6b96 100644 --- a/composer.json +++ b/composer.json @@ -48,8 +48,8 @@ "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:7.4-tests -f hack/7.4.Dockerfile hack", "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.0-tests -f hack/8.0.Dockerfile hack", "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.1-tests -f hack/8.1.Dockerfile hack", - "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.2-tests -f hack/8.1.Dockerfile hack", - "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.3-tests -f hack/8.1.Dockerfile hack" + "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.2-tests -f hack/8.2.Dockerfile hack", + "DOCKER_BUILDKIT=1 docker build -t cloudevents/sdk-php:8.3-tests -f hack/8.3.Dockerfile hack" ], "tests-docker": [ "docker run -it -v $(pwd):/var/www cloudevents/sdk-php:7.4-tests --coverage-html=coverage", @@ -76,7 +76,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.0-dev" + "dev-main": "1.1-dev" } }, "minimum-stability": "dev", diff --git a/src/Serializers/Normalizers/V1/Normalizer.php b/src/Serializers/Normalizers/V1/Normalizer.php index d5116bf..3ba9cff 100644 --- a/src/Serializers/Normalizers/V1/Normalizer.php +++ b/src/Serializers/Normalizers/V1/Normalizer.php @@ -10,13 +10,26 @@ final class Normalizer implements NormalizerInterface { + /** + * @var array{subsecondPrecision?: int<0, 6>} + */ + private array $configuration; + + /** + * @param array{subsecondPrecision?: int<0, 6>} $configuration + */ + public function __construct(array $configuration = []) + { + $this->configuration = $configuration; + } + /** * @return array */ public function normalize(CloudEventInterface $cloudEvent, bool $rawData): array { return array_merge( - AttributeConverter::toArray($cloudEvent), + AttributeConverter::toArray($cloudEvent, $this->configuration), DataFormatter::encode($cloudEvent->getData(), $rawData) ); } diff --git a/src/Utilities/AttributeConverter.php b/src/Utilities/AttributeConverter.php index 880c591..512ccdb 100644 --- a/src/Utilities/AttributeConverter.php +++ b/src/Utilities/AttributeConverter.php @@ -13,9 +13,11 @@ final class AttributeConverter { /** + * @param array{subsecondPrecision?: int<0, 6>} $configuration + * * @return array */ - public static function toArray(CloudEventInterface $cloudEvent): array + public static function toArray(CloudEventInterface $cloudEvent, array $configuration): array { /** @var array */ $attributes = array_filter([ @@ -26,7 +28,7 @@ public static function toArray(CloudEventInterface $cloudEvent): array 'datacontenttype' => $cloudEvent->getDataContentType(), 'dataschema' => $cloudEvent->getDataSchema(), 'subject' => $cloudEvent->getSubject(), - 'time' => TimeFormatter::encode($cloudEvent->getTime()), + 'time' => TimeFormatter::encode($cloudEvent->getTime(), $configuration['subsecondPrecision'] ?? 0), ], fn ($attr) => $attr !== null); return array_merge($attributes, $cloudEvent->getExtensions()); diff --git a/src/Utilities/TimeFormatter.php b/src/Utilities/TimeFormatter.php index 6d6bbe4..b27f9c7 100644 --- a/src/Utilities/TimeFormatter.php +++ b/src/Utilities/TimeFormatter.php @@ -13,19 +13,41 @@ */ final class TimeFormatter { - private const TIME_FORMAT = 'Y-m-d\TH:i:s\Z'; + private const TIME_FORMAT = 'Y-m-d\TH:i:s'; + private const TIME_FORMAT_EXTENDED = 'Y-m-d\TH:i:s.u'; private const TIME_ZONE = 'UTC'; private const RFC3339_FORMAT = 'Y-m-d\TH:i:sP'; private const RFC3339_EXTENDED_FORMAT = 'Y-m-d\TH:i:s.uP'; - public static function encode(?DateTimeImmutable $time): ?string + /** + * @param int<0, 6> $subsecondPrecision + */ + public static function encode(?DateTimeImmutable $time, int $subsecondPrecision): ?string { if ($time === null) { return null; } - return $time->setTimezone(new DateTimeZone(self::TIME_ZONE))->format(self::TIME_FORMAT); + return sprintf('%sZ', self::encodeWithoutTimezone($time, $subsecondPrecision)); + } + + /** + * @param int<0, 6> $subsecondPrecision + */ + private static function encodeWithoutTimezone(DateTimeImmutable $time, int $subsecondPrecision): string + { + $utcTime = $time->setTimezone(new DateTimeZone(self::TIME_ZONE)); + + if ($subsecondPrecision <= 0) { + return $utcTime->format(self::TIME_FORMAT); + } + + if ($subsecondPrecision >= 6) { + return $utcTime->format(self::TIME_FORMAT_EXTENDED); + } + + return substr($utcTime->format(self::TIME_FORMAT_EXTENDED), 0, $subsecondPrecision - 6); } public static function decode(?string $time): ?DateTimeImmutable diff --git a/tests/Unit/Serializers/Normalizers/V1/NormalizerTest.php b/tests/Unit/Serializers/Normalizers/V1/NormalizerTest.php index 19487b0..6842417 100644 --- a/tests/Unit/Serializers/Normalizers/V1/NormalizerTest.php +++ b/tests/Unit/Serializers/Normalizers/V1/NormalizerTest.php @@ -80,4 +80,41 @@ public function testNormalizerWithUnsetAttributes(): void $formatter->normalize($event, false) ); } + + public function testNormalizerWithSubsecondPrecisionConfiguration(): void + { + /** @var CloudEventInterface|Stub $event */ + $event = $this->createStub(CloudEventInterface::class); + $event->method('getSpecVersion')->willReturn('1.0'); + $event->method('getId')->willReturn('1234-1234-1234'); + $event->method('getSource')->willReturn('/var/data'); + $event->method('getType')->willReturn('com.example.someevent'); + $event->method('getDataContentType')->willReturn('application/json'); + $event->method('getDataSchema')->willReturn('com.example/schema'); + $event->method('getSubject')->willReturn('larger-context'); + $event->method('getTime')->willReturn(new DateTimeImmutable('2018-04-05T17:31:00.123456Z')); + $event->method('getData')->willReturn(['key' => 'value']); + $event->method('getExtensions')->willReturn(['comacme' => 'foo']); + + $formatter = new Normalizer(['subsecondPrecision' => 3]); + + self::assertSame( + [ + 'specversion' => '1.0', + 'id' => '1234-1234-1234', + 'source' => '/var/data', + 'type' => 'com.example.someevent', + 'datacontenttype' => 'application/json', + 'dataschema' => 'com.example/schema', + 'subject' => 'larger-context', + 'time' => '2018-04-05T17:31:00.123Z', + 'comacme' => 'foo', + 'data' => [ + 'key' => 'value', + ], + ], + $formatter->normalize($event, false) + ); + } + } diff --git a/tests/Unit/Utilities/TimeFormatterTest.php b/tests/Unit/Utilities/TimeFormatterTest.php index 06d8d44..00426e2 100644 --- a/tests/Unit/Utilities/TimeFormatterTest.php +++ b/tests/Unit/Utilities/TimeFormatterTest.php @@ -11,11 +11,27 @@ class TimeFormatterTest extends TestCase { - public function testEncode(): void + public static function providesValidEncodeCases(): array + { + return [ + ['2018-04-05T17:31:00Z', '2018-04-05T17:31:00.123456Z', 0], + ['2018-04-05T17:31:00.1Z', '2018-04-05T17:31:00.123456Z', 1], + ['2018-04-05T17:31:00.12Z', '2018-04-05T17:31:00.123456Z', 2], + ['2018-04-05T17:31:00.123Z', '2018-04-05T17:31:00.123456Z', 3], + ['2018-04-05T17:31:00.1234Z', '2018-04-05T17:31:00.123456Z', 4], + ['2018-04-05T17:31:00.12345Z', '2018-04-05T17:31:00.123456Z', 5], + ['2018-04-05T17:31:00.123456Z', '2018-04-05T17:31:00.123456Z', 6], + ]; + } + + /** + * @dataProvider providesValidEncodeCases + */ + public function testEncode(string $expected, string $input, int $subsecondPrecision): void { self::assertEquals( - '2018-04-05T17:31:00Z', - TimeFormatter::encode(new DateTimeImmutable('2018-04-05T17:31:00Z')) + $expected, + TimeFormatter::encode(new DateTimeImmutable($input), $subsecondPrecision) ); } @@ -83,7 +99,7 @@ public function testEncodeEmpty(): void { self::assertEquals( null, - TimeFormatter::encode(null) + TimeFormatter::encode(null, 0) ); }