Skip to content

Commit

Permalink
Enable configuration of subsecond precision of timestamps (#54)
Browse files Browse the repository at this point in the history
Signed-off-by: Graham Campbell <hello@gjcampbell.co.uk>
  • Loading branch information
GrahamCampbell committed Dec 18, 2023
1 parent 7e18f33 commit 45c5e1b
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 13 deletions.
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -76,7 +76,7 @@
},
"extra": {
"branch-alias": {
"dev-main": "1.0-dev"
"dev-main": "1.1-dev"
}
},
"minimum-stability": "dev",
Expand Down
15 changes: 14 additions & 1 deletion src/Serializers/Normalizers/V1/Normalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed>
*/
public function normalize(CloudEventInterface $cloudEvent, bool $rawData): array
{
return array_merge(
AttributeConverter::toArray($cloudEvent),
AttributeConverter::toArray($cloudEvent, $this->configuration),
DataFormatter::encode($cloudEvent->getData(), $rawData)
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/Utilities/AttributeConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
final class AttributeConverter
{
/**
* @param array{subsecondPrecision?: int<0, 6>} $configuration
*
* @return array<string, bool|int|string>
*/
public static function toArray(CloudEventInterface $cloudEvent): array
public static function toArray(CloudEventInterface $cloudEvent, array $configuration): array
{
/** @var array<string, bool|int|string> */
$attributes = array_filter([
Expand All @@ -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());
Expand Down
28 changes: 25 additions & 3 deletions src/Utilities/TimeFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions tests/Unit/Serializers/Normalizers/V1/NormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,40 @@ 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)
);
}
}
24 changes: 20 additions & 4 deletions tests/Unit/Utilities/TimeFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

Expand Down Expand Up @@ -83,7 +99,7 @@ public function testEncodeEmpty(): void
{
self::assertEquals(
null,
TimeFormatter::encode(null)
TimeFormatter::encode(null, 0)
);
}

Expand Down

0 comments on commit 45c5e1b

Please sign in to comment.