diff --git a/src/implementation/flags/EvaluationDetails.php b/src/implementation/flags/EvaluationDetails.php index d96c46b..744b21b 100644 --- a/src/implementation/flags/EvaluationDetails.php +++ b/src/implementation/flags/EvaluationDetails.php @@ -8,6 +8,8 @@ use OpenFeature\interfaces\flags\EvaluationDetails as EvaluationDetailsInterface; use OpenFeature\interfaces\provider\ResolutionError; +use function is_array; + class EvaluationDetails implements EvaluationDetailsInterface { private string $flagKey = ''; @@ -17,6 +19,8 @@ class EvaluationDetails implements EvaluationDetailsInterface private ?ResolutionError $error = null; private ?string $reason = null; private ?string $variant = null; + /** @var array|null $metadata */ + private ?array $metadata = null; public function __construct() { @@ -83,4 +87,36 @@ public function setVariant(?string $variant): void { $this->variant = $variant; } + + /** + * @param array|null $metadata + */ + public function setMetadata(?array $metadata): void + { + if (is_array($metadata)) { + $this->metadata = []; + foreach ($metadata as $key => $value) { + $this->metadata[$key] = $value; + } + } else { + $this->metadata = null; + } + } + + /** + * @return array + */ + public function getMetadata(): array + { + if ($this->metadata === null) { + return []; + } + /** @var array $metadata */ + $metadata = []; + foreach ($this->metadata as $key => $value) { + $metadata[$key] = $value; + } + + return $metadata; + } } diff --git a/src/implementation/flags/EvaluationDetailsBuilder.php b/src/implementation/flags/EvaluationDetailsBuilder.php index 7cb4dc6..ce2a45d 100644 --- a/src/implementation/flags/EvaluationDetailsBuilder.php +++ b/src/implementation/flags/EvaluationDetailsBuilder.php @@ -55,6 +55,16 @@ public function withVariant(?string $variant): EvaluationDetailsBuilder return $this; } + /** + * @param array|null $metadata + */ + public function withMetadata(?array $metadata): EvaluationDetailsBuilder + { + $this->details->setMetadata($metadata); + + return $this; + } + public function build(): EvaluationDetailsInterface { return $this->details; diff --git a/src/implementation/flags/EvaluationDetailsFactory.php b/src/implementation/flags/EvaluationDetailsFactory.php index 6115799..a48b3c7 100644 --- a/src/implementation/flags/EvaluationDetailsFactory.php +++ b/src/implementation/flags/EvaluationDetailsFactory.php @@ -34,6 +34,7 @@ public static function fromResolution(string $flagKey, ResolutionDetails $detail ->withError($details->getError()) ->withReason($details->getReason()) ->withVariant($details->getVariant()) + ->withMetadata($details->getMetadata()) ->build(); } } diff --git a/src/implementation/provider/ResolutionDetails.php b/src/implementation/provider/ResolutionDetails.php index 29750d9..1f015f3 100644 --- a/src/implementation/provider/ResolutionDetails.php +++ b/src/implementation/provider/ResolutionDetails.php @@ -8,6 +8,8 @@ use OpenFeature\interfaces\provider\ResolutionDetails as ResolutionDetailsInterface; use OpenFeature\interfaces\provider\ResolutionError; +use function is_array; + class ResolutionDetails implements ResolutionDetailsInterface { /** @var bool|string|int|float|DateTime|mixed[]|null $value */ @@ -15,6 +17,8 @@ class ResolutionDetails implements ResolutionDetailsInterface private ?ResolutionError $error = null; private ?string $reason = null; private ?string $variant = null; + /** @var array|null $metadata */ + private ?array $metadata = null; /** * @return bool|string|int|float|DateTime|mixed[]|null @@ -61,4 +65,36 @@ public function setVariant(?string $variant): void { $this->variant = $variant; } + + /** + * @param array|null $metadata + */ + public function setMetadata(?array $metadata): void + { + if (is_array($metadata)) { + $this->metadata = []; + foreach ($metadata as $key => $value) { + $this->metadata[$key] = $value; + } + } else { + $this->metadata = null; + } + } + + /** + * @return array|null + */ + public function getMetadata(): ?array + { + if ($this->metadata === null) { + return null; + } + /** @var array $metadata */ + $metadata = []; + foreach ($this->metadata as $key => $value) { + $metadata[$key] = $value; + } + + return $metadata; + } } diff --git a/src/implementation/provider/ResolutionDetailsBuilder.php b/src/implementation/provider/ResolutionDetailsBuilder.php index 73ae2c7..45579fa 100644 --- a/src/implementation/provider/ResolutionDetailsBuilder.php +++ b/src/implementation/provider/ResolutionDetailsBuilder.php @@ -48,6 +48,16 @@ public function withVariant(string $variant): ResolutionDetailsBuilder return $this; } + /** + * @param array|null $metadata + */ + public function withMetadata(?array $metadata): ResolutionDetailsBuilder + { + $this->details->setMetadata($metadata); + + return $this; + } + public function build(): ResolutionDetailsInterface { return $this->details; diff --git a/src/interfaces/flags/EvaluationDetails.php b/src/interfaces/flags/EvaluationDetails.php index 4dd269e..7af0f51 100644 --- a/src/interfaces/flags/EvaluationDetails.php +++ b/src/interfaces/flags/EvaluationDetails.php @@ -64,4 +64,16 @@ public function getReason(): ?string; * the configured provider, if the field is set. */ public function getVariant(): ?string; + + /** + * ------------------ + * Requirement 1.4.14 + * ------------------ + * If the flag metadata field in the flag resolution structure returned by the configured + * provider is set, the evaluation details structure's flag metadata field MUST contain that + * value. Otherwise, it MUST contain an empty record. + * + * @return array + */ + public function getMetadata(): array; } diff --git a/src/interfaces/provider/ResolutionDetails.php b/src/interfaces/provider/ResolutionDetails.php index c50edb2..64a2d42 100644 --- a/src/interfaces/provider/ResolutionDetails.php +++ b/src/interfaces/provider/ResolutionDetails.php @@ -51,4 +51,14 @@ public function getReason(): ?string; * with a string identifier corresponding to the returned flag value. */ public function getVariant(): ?string; + + /** + * ----------------- + * Requirement 2.2.9 + * ----------------- + * The provider SHOULD populate the resolution details structure's flag metadata field. + * + * @return array|null + */ + public function getMetadata(): ?array; } diff --git a/tests/unit/EvaluationDetailsMetadataTest.php b/tests/unit/EvaluationDetailsMetadataTest.php new file mode 100644 index 0000000..aa370b2 --- /dev/null +++ b/tests/unit/EvaluationDetailsMetadataTest.php @@ -0,0 +1,134 @@ +provider = Mockery::mock(Provider::class); + $this->provider->shouldReceive('getMetadata->getName')->andReturn('TestProvider'); + } + + /** + * @param bool|string|int|float|DateTime|array|null $value + */ + private function details(bool | string | int | float | DateTime | array | null $value): EvaluationDetails + { + return (new EvaluationDetailsBuilder())->withValue($value)->build(); + } + + /** + * @param bool|string|int|float|DateTime|array|null $value + * @param array $metadata + */ + private function detailsWithMetadata(bool | string | int | float | DateTime | array | null $value, array $metadata): EvaluationDetails + { + return (new EvaluationDetailsBuilder())->withValue($value)->withMetadata($metadata)->build(); + } + + /** + * @param bool|string|int|float|DateTime|array|null $value + */ + private function resolution(bool | string | int | float | DateTime | array | null $value): ResolutionDetails + { + return (new ResolutionDetailsBuilder())->withValue($value)->build(); + } + + /** + * @param bool|string|int|float|DateTime|array|null $value + * @param array $metadata + */ + private function resolutionWithMetadata(bool | string | int | float | DateTime | array | null $value, array $metadata): ResolutionDetails + { + return (new ResolutionDetailsBuilder())->withValue($value)->withMetadata($metadata)->build(); + } + + public function testEvaluationResultWithEmptyMetadata(): void + { + $details = $this->details(true); + $metadata = $details->getMetadata(); + $this->assertNotNull($metadata); + $this->assertIsArray($metadata); + $this->assertEmpty($metadata); + } + + public function testEvaluationResultWithNotEmptyMetadata(): void + { + $details = $this->detailsWithMetadata(true, [ + 'bool_value' => true, + 'string_value' => 'OK', + ]); + $metadata = $details->getMetadata(); + $this->assertNotNull($metadata); + $this->assertIsArray($metadata); + $this->assertNotEmpty($metadata); + $this->assertArrayHasKey('bool_value', $metadata); + $this->assertArrayHasKey('string_value', $metadata); + $this->assertEquals(true, $metadata['bool_value']); + $this->assertEquals('OK', $metadata['string_value']); + } + + public function testEvaluationResultMetadataImmutability(): void + { + $details = $this->detailsWithMetadata(true, [ + 'bool_value' => true, + 'string_value' => 'OK', + ]); + $metadata = $details->getMetadata(); + // let's add a new key/value and let's change the value of "bool_value" + $metadata['number_value'] = 7; + $metadata['bool_value'] = false; + // get again the metadata + $newMetadata = $details->getMetadata(); + + $this->assertNotSame($metadata, $newMetadata); + $this->assertArrayNotHasKey('number_value', $newMetadata); + $this->assertArrayHasKey('bool_value', $newMetadata); + $this->assertNotEquals(false, $newMetadata['bool_value']); + $this->assertNotSameSize($metadata, $newMetadata); + } + + public function testEvaluationDetailsFromResolutionDetailsWithoutMetadata(): void + { + $resolution = $this->resolution(true); + $details = EvaluationDetailsFactory::fromResolution('test-key', $resolution); + + $this->assertNull($resolution->getMetadata()); + $this->assertNotNull($details->getMetadata()); + $this->assertIsArray($details->getMetadata()); + $this->assertEmpty($details->getMetadata()); + $this->assertNotSame($resolution->getMetadata(), $details->getMetadata()); + } + + public function testEvaluationDetailsFromResolutionDetailsWithMetadata(): void + { + $resolution = $this->resolutionWithMetadata(true, [ + 'key' => 'value', + ]); + $details = EvaluationDetailsFactory::fromResolution('test-key', $resolution); + + $this->assertNotNull($resolution->getMetadata()); + $this->assertNotNull($details->getMetadata()); + $this->assertIsArray($details->getMetadata()); + $this->assertNotEmpty($details->getMetadata()); + $this->assertArrayHasKey('key', $details->getMetadata()); + } +} diff --git a/tests/unit/ProviderResolutionResultTest.php b/tests/unit/ProviderResolutionResultTest.php index 577fc7d..021f7ec 100644 --- a/tests/unit/ProviderResolutionResultTest.php +++ b/tests/unit/ProviderResolutionResultTest.php @@ -34,6 +34,15 @@ private function details(bool | string | int | float | DateTime | array | null $ return (new ResolutionDetailsBuilder())->withValue($value)->build(); } + /** + * @param bool|string|int|float|DateTime|array|null $value + * @param array|null $metadata + */ + private function detailsWithMetadata(bool | string | int | float | DateTime | array | null $value, ?array $metadata): ResolutionDetails + { + return (new ResolutionDetailsBuilder())->withValue($value)->withMetadata($metadata)->build(); + } + public function testSuccessfulResult(): void { $details = $this->details(true); @@ -68,4 +77,52 @@ public function testEmptyResultNeitherSuccessNorError(): void $this->assertFalse($result->hasError()); $this->assertFalse($result->isSuccessful()); } + + public function testResultWithNotEmptyMetadata(): void + { + $details = $this->detailsWithMetadata(true, [ + 'test_bool' => true, + 'test_int' => 10, + 'test_string' => 'OK', + ]); + $result = new ProviderResolutionResult('TestProvider', $this->provider, $details, null); + + $this->assertSame($details, $result->getDetails()); + $this->assertNull($result->getError()); + $this->assertFalse($result->hasError()); + $this->assertTrue($result->isSuccessful()); + $this->assertIsArray($details->getMetadata()); + $this->assertArrayHasKey('test_bool', $details->getMetadata()); + $this->assertArrayHasKey('test_int', $details->getMetadata()); + $this->assertArrayHasKey('test_string', $details->getMetadata()); + $this->assertEquals(true, $details->getMetadata()['test_bool']); + $this->assertEquals('OK', $details->getMetadata()['test_string']); + $this->assertEquals(10, $details->getMetadata()['test_int']); + } + + public function testResultWithEmptyMetadata(): void + { + $details = $this->detailsWithMetadata(true, []); + $result = new ProviderResolutionResult('TestProvider', $this->provider, $details, null); + + $this->assertSame($details, $result->getDetails()); + $this->assertNull($result->getError()); + $this->assertFalse($result->hasError()); + $this->assertTrue($result->isSuccessful()); + $this->assertIsArray($details->getMetadata()); + $this->assertSame([], $details->getMetadata()); + } + + public function testResultWithNullMetadata(): void + { + $details = $this->detailsWithMetadata(true, null); + $result = new ProviderResolutionResult('TestProvider', $this->provider, $details, null); + + $this->assertSame($details, $result->getDetails()); + $this->assertNull($result->getError()); + $this->assertFalse($result->hasError()); + $this->assertTrue($result->isSuccessful()); + $this->assertNull($details->getMetadata()); + $this->assertIsNotArray($details->getMetadata()); + } } diff --git a/tests/unit/ResolutionDetailsMetadataTest.php b/tests/unit/ResolutionDetailsMetadataTest.php new file mode 100644 index 0000000..0cb5bbb --- /dev/null +++ b/tests/unit/ResolutionDetailsMetadataTest.php @@ -0,0 +1,95 @@ +provider = Mockery::mock(Provider::class); + $this->provider->shouldReceive('getMetadata->getName')->andReturn('TestProvider'); + } + + /** + * @param bool|string|int|float|DateTime|array|null $value + */ + private function details(bool | string | int | float | DateTime | array | null $value): ResolutionDetails + { + return (new ResolutionDetailsBuilder())->withValue($value)->build(); + } + + /** + * @param bool|string|int|float|DateTime|array|null $value + * @param array $metadata + */ + private function detailsWithMetadata(bool | string | int | float | DateTime | array | null $value, array $metadata): ResolutionDetails + { + return (new ResolutionDetailsBuilder())->withValue($value)->withMetadata($metadata)->build(); + } + + public function testResolutionDetailsWithoutMetadata(): void + { + $details = $this->details(true); + $this->assertNull($details->getMetadata()); + } + + public function testResolutionDetailsWithEmptyMetadata(): void + { + $details = $this->detailsWithMetadata(true, []); + $metadata = $details->getMetadata(); + $this->assertNotNull($metadata); + $this->assertIsArray($metadata); + $this->assertEmpty($metadata); + } + + public function testResolutionDetailsWithNotEmptyMetadata(): void + { + $details = $this->detailsWithMetadata(true, [ + 'bool_value' => true, + 'string_value' => 'OK', + ]); + $metadata = $details->getMetadata(); + $this->assertNotNull($metadata); + $this->assertIsArray($metadata); + $this->assertNotEmpty($metadata); + $this->assertArrayHasKey('bool_value', $metadata); + $this->assertArrayHasKey('string_value', $metadata); + $this->assertEquals(true, $metadata['bool_value']); + $this->assertEquals('OK', $metadata['string_value']); + } + + public function testResolutionDetailsMetadataImmutability(): void + { + $details = $this->detailsWithMetadata(true, [ + 'bool_value' => true, + 'string_value' => 'OK', + ]); + $metadata = $details->getMetadata(); + // let's add a new key/value and let's change the value of "bool_value" + $metadata['number_value'] = 7; + $metadata['bool_value'] = false; + // get again the metadata + $newMetadata = $details->getMetadata(); + + $this->assertNotSame($metadata, $newMetadata); + $this->assertIsArray($newMetadata); + $this->assertArrayNotHasKey('number_value', $newMetadata); + $this->assertArrayHasKey('bool_value', $newMetadata); + $this->assertNotEquals(false, $newMetadata['bool_value']); + $this->assertNotSameSize($metadata, $newMetadata); + } +}