diff --git a/src/EvaluationContextConverter.php b/src/EvaluationContextConverter.php index 135ae89..0300e1f 100644 --- a/src/EvaluationContextConverter.php +++ b/src/EvaluationContextConverter.php @@ -5,6 +5,9 @@ namespace LaunchDarkly\OpenFeature; use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; use LaunchDarkly\LDContext; use LaunchDarkly\LDContextBuilder; use LaunchDarkly\LDContextMultiBuilder; @@ -157,10 +160,39 @@ private function buildSingleContext(array $attributes, string $kind, string $key $this->logger->error("'privateAttributes' in an evaluation context must be an array"); } else { // Catch all for remaining attributes - $builder->set($k, $v); + $builder->set((string) $k, $this->toLdValue($v, (string) $k)); } } return $builder->build(); } + + /** + * Convert an OpenFeature attribute value into a value the LaunchDarkly + * context model can hold. Dates become ISO-8601 strings in UTC, which + * the LaunchDarkly date operators accept. + */ + private function toLdValue(mixed $value, string $attributeName): mixed + { + if ($value === null || is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { + return $value; + } + + if ($value instanceof DateTimeInterface) { + return DateTimeImmutable::createFromInterface($value) + ->setTimezone(new DateTimeZone('UTC')) + ->format('Y-m-d\\TH:i:s.v\\Z'); + } + + if (is_array($value)) { + $converted = []; + foreach ($value as $k => $v) { + $converted[$k] = $this->toLdValue($v, $attributeName); + } + return $converted; + } + + $this->logger->error("The attribute '$attributeName' contained a value which could not be converted; using null"); + return null; + } } diff --git a/tests/EvaluationContextConverterTest.php b/tests/EvaluationContextConverterTest.php index ad5f157..b70c666 100644 --- a/tests/EvaluationContextConverterTest.php +++ b/tests/EvaluationContextConverterTest.php @@ -4,10 +4,14 @@ namespace LaunchDarkly\Tests; +use DateTime; +use DateTimeImmutable; +use DateTimeZone; use LaunchDarkly\OpenFeature\EvaluationContextConverter; use OpenFeature\implementation\flags\Attributes; use OpenFeature\implementation\flags\EvaluationContext; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; class EvaluationContextConverterTest extends TestCase @@ -208,4 +212,50 @@ public function testHandlesInvalidNestedContexts(): void $this->assertFalse($ldContext->isValid()); $this->assertEquals(0, $ldContext->getIndividualContextCount()); } + + public function testDateAttributesAreConvertedToUtcIso8601Strings(): void + { + $attributes = new Attributes([ + 'signupDate' => new DateTime('2024-03-01T09:30:00.123+09:00'), + 'address' => ['movedIn' => new DateTimeImmutable('2024-03-01T00:00:00', new DateTimeZone('UTC'))], + 'history' => [new DateTimeImmutable('2023-12-31T23:00:00-01:00'), 'other'], + ]); + $context = new EvaluationContext('user-key', $attributes); + + $ldContext = $this->contextConverter->toLdContext($context); + + $this->assertTrue($ldContext->isValid()); + $this->assertSame('2024-03-01T00:30:00.123Z', $ldContext->get('signupDate')); + $this->assertSame(['movedIn' => '2024-03-01T00:00:00.000Z'], $ldContext->get('address')); + $this->assertSame(['2024-01-01T00:00:00.000Z', 'other'], $ldContext->get('history')); + } + + public function testMultiContextDateAttributesAreConverted(): void + { + $attributes = new Attributes([ + 'kind' => 'multi', + 'org' => ['key' => 'org-key', 'founded' => new DateTimeImmutable('2001-02-03T04:05:06Z')], + ]); + $context = new EvaluationContext(null, $attributes); + + $ldContext = $this->contextConverter->toLdContext($context); + + /** @var \LaunchDarkly\LDContext */ + $orgContext = $ldContext->getIndividualContext('org'); + $this->assertSame('2001-02-03T04:05:06.000Z', $orgContext->get('founded')); + } + + public function testUnconvertibleValuesBecomeNullAndLogAnError(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once())->method('error'); + $converter = new EvaluationContextConverter($logger); + + $attributes = new Attributes(['list' => ['valid', new \stdClass()]]); + $context = new EvaluationContext('user-key', $attributes); + + $ldContext = $converter->toLdContext($context); + + $this->assertSame(['valid', null], $ldContext->get('list')); + } }