From 403c40fbbfb77286cf7f3414d2bbc96ef5f978e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 12 Feb 2024 20:51:22 +0000 Subject: [PATCH] feat: Flag metadata (#223) Signed-off-by: Artyom Tonoyan --- src/OpenFeature/Model/BaseMetadata.cs | 76 ++++++ .../Model/FlagEvaluationDetails.cs | 11 +- src/OpenFeature/Model/FlagMetadata.cs | 28 ++ src/OpenFeature/Model/ProviderEvents.cs | 1 + src/OpenFeature/Model/ResolutionDetails.cs | 11 +- test/OpenFeature.Tests/FlagMetadataTest.cs | 246 ++++++++++++++++++ 6 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 src/OpenFeature/Model/BaseMetadata.cs create mode 100644 src/OpenFeature/Model/FlagMetadata.cs create mode 100644 test/OpenFeature.Tests/FlagMetadataTest.cs diff --git a/src/OpenFeature/Model/BaseMetadata.cs b/src/OpenFeature/Model/BaseMetadata.cs new file mode 100644 index 00000000..1e1fa211 --- /dev/null +++ b/src/OpenFeature/Model/BaseMetadata.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +#nullable enable +namespace OpenFeature.Model; + +/// +/// Represents the base class for metadata objects. +/// +public abstract class BaseMetadata +{ + private readonly ImmutableDictionary _metadata; + + internal BaseMetadata(Dictionary metadata) + { + this._metadata = metadata.ToImmutableDictionary(); + } + + /// + /// Gets the boolean value associated with the specified key. + /// + /// The key of the value to retrieve. + /// The boolean value associated with the key, or null if the key is not found. + public virtual bool? GetBool(string key) + { + return this.GetValue(key); + } + + /// + /// Gets the integer value associated with the specified key. + /// + /// The key of the value to retrieve. + /// The integer value associated with the key, or null if the key is not found. + public virtual int? GetInt(string key) + { + return this.GetValue(key); + } + + /// + /// Gets the double value associated with the specified key. + /// + /// The key of the value to retrieve. + /// The double value associated with the key, or null if the key is not found. + public virtual double? GetDouble(string key) + { + return this.GetValue(key); + } + + /// + /// Gets the string value associated with the specified key. + /// + /// The key of the value to retrieve. + /// The string value associated with the key, or null if the key is not found. + public virtual string? GetString(string key) + { + var hasValue = this._metadata.TryGetValue(key, out var value); + if (!hasValue) + { + return null; + } + + return value as string ?? null; + } + + private T? GetValue(string key) where T : struct + { + var hasValue = this._metadata.TryGetValue(key, out var value); + if (!hasValue) + { + return null; + } + + return value is T tValue ? tValue : null; + } +} diff --git a/src/OpenFeature/Model/FlagEvaluationDetails.cs b/src/OpenFeature/Model/FlagEvaluationDetails.cs index af31ca6d..cff22a8b 100644 --- a/src/OpenFeature/Model/FlagEvaluationDetails.cs +++ b/src/OpenFeature/Model/FlagEvaluationDetails.cs @@ -6,7 +6,7 @@ namespace OpenFeature.Model /// The contract returned to the caller that describes the result of the flag evaluation process. /// /// Flag value type - /// + /// public sealed class FlagEvaluationDetails { /// @@ -45,6 +45,11 @@ public sealed class FlagEvaluationDetails /// public string Variant { get; } + /// + /// A structure which supports definition of arbitrary properties, with keys of type string, and values of type boolean, string, or number. + /// + public FlagMetadata FlagMetadata { get; } + /// /// Initializes a new instance of the class. /// @@ -54,8 +59,9 @@ public sealed class FlagEvaluationDetails /// Reason /// Variant /// Error message + /// Flag metadata public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, string reason, string variant, - string errorMessage = null) + string errorMessage = null, FlagMetadata flagMetadata = null) { this.Value = value; this.FlagKey = flagKey; @@ -63,6 +69,7 @@ public FlagEvaluationDetails(string flagKey, T value, ErrorType errorType, strin this.Reason = reason; this.Variant = variant; this.ErrorMessage = errorMessage; + this.FlagMetadata = flagMetadata; } } } diff --git a/src/OpenFeature/Model/FlagMetadata.cs b/src/OpenFeature/Model/FlagMetadata.cs new file mode 100644 index 00000000..db666b7f --- /dev/null +++ b/src/OpenFeature/Model/FlagMetadata.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +#nullable enable +namespace OpenFeature.Model; + +/// +/// Represents the metadata associated with a feature flag. +/// +/// +public sealed class FlagMetadata : BaseMetadata +{ + /// + /// Constructor for the class. + /// + public FlagMetadata() : this([]) + { + } + + /// + /// Constructor for the class. + /// + /// The dictionary containing the metadata. + public FlagMetadata(Dictionary metadata) : base(metadata) + { + } +} diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs index da68aef4..ca7c7e1a 100644 --- a/src/OpenFeature/Model/ProviderEvents.cs +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -36,6 +36,7 @@ public class ProviderEventPayload /// /// Metadata information for the event. /// + // TODO: This needs to be changed to a EventMetadata object public Dictionary EventMetadata { get; set; } } } diff --git a/src/OpenFeature/Model/ResolutionDetails.cs b/src/OpenFeature/Model/ResolutionDetails.cs index 024f36de..9319096f 100644 --- a/src/OpenFeature/Model/ResolutionDetails.cs +++ b/src/OpenFeature/Model/ResolutionDetails.cs @@ -7,7 +7,7 @@ namespace OpenFeature.Model /// Describes the details of the feature flag being evaluated /// /// Flag value type - /// + /// public sealed class ResolutionDetails { /// @@ -44,6 +44,11 @@ public sealed class ResolutionDetails /// public string Variant { get; } + /// + /// A structure which supports definition of arbitrary properties, with keys of type string, and values of type boolean, string, or number. + /// + public FlagMetadata FlagMetadata { get; } + /// /// Initializes a new instance of the class. /// @@ -53,8 +58,9 @@ public sealed class ResolutionDetails /// Reason /// Variant /// Error message + /// Flag metadata public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorType.None, string reason = null, - string variant = null, string errorMessage = null) + string variant = null, string errorMessage = null, FlagMetadata flagMetadata = null) { this.Value = value; this.FlagKey = flagKey; @@ -62,6 +68,7 @@ public ResolutionDetails(string flagKey, T value, ErrorType errorType = ErrorTyp this.Reason = reason; this.Variant = variant; this.ErrorMessage = errorMessage; + this.FlagMetadata = flagMetadata; } } } diff --git a/test/OpenFeature.Tests/FlagMetadataTest.cs b/test/OpenFeature.Tests/FlagMetadataTest.cs new file mode 100644 index 00000000..88d248de --- /dev/null +++ b/test/OpenFeature.Tests/FlagMetadataTest.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using OpenFeature.Model; +using OpenFeature.Tests.Internal; +using Xunit; + +#nullable enable +namespace OpenFeature.Tests; + +public class FlagMetadataTest +{ + [Fact] + [Specification("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.")] + public void GetBool_Should_Return_Null_If_Key_Not_Found() + { + // Arrange + var flagMetadata = new FlagMetadata(); + + // Act + var result = flagMetadata.GetBool("nonexistentKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("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.")] + [Specification("1.4.14.1", "Condition: `Flag metadata` MUST be immutable.")] + public void GetBool_Should_Return_Value_If_Key_Found() + { + // Arrange + var metadata = new Dictionary + { + { + "boolKey", true + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetBool("boolKey"); + + // Assert + Assert.True(result); + } + + [Fact] + [Specification("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.")] + public void GetBool_Should_Throw_Value_Is_Invalid() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", "11a" + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetBool("wrongKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("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.")] + public void GetInt_Should_Return_Null_If_Key_Not_Found() + { + // Arrange + var flagMetadata = new FlagMetadata(); + + // Act + var result = flagMetadata.GetInt("nonexistentKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("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.")] + [Specification("1.4.14.1", "Condition: `Flag metadata` MUST be immutable.")] + public void GetInt_Should_Return_Value_If_Key_Found() + { + // Arrange + var metadata = new Dictionary + { + { + "intKey", 1 + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetInt("intKey"); + + // Assert + Assert.NotNull(result); + Assert.Equal(1, result); + } + + [Fact] + [Specification("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.")] + public void GetInt_Should_Throw_Value_Is_Invalid() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", "11a" + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetInt("wrongKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("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.")] + public void GetDouble_Should_Return_Null_If_Key_Not_Found() + { + // Arrange + var flagMetadata = new FlagMetadata(); + + // Act + var result = flagMetadata.GetDouble("nonexistentKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("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.")] + [Specification("1.4.14.1", "Condition: `Flag metadata` MUST be immutable.")] + public void GetDouble_Should_Return_Value_If_Key_Found() + { + // Arrange + var metadata = new Dictionary + { + { + "doubleKey", 1.2 + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetDouble("doubleKey"); + + // Assert + Assert.NotNull(result); + Assert.Equal(1.2, result); + } + + [Fact] + [Specification("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.")] + public void GetDouble_Should_Throw_Value_Is_Invalid() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", "11a" + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetDouble("wrongKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("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.")] + public void GetString_Should_Return_Null_If_Key_Not_Found() + { + // Arrange + var flagMetadata = new FlagMetadata(); + + // Act + var result = flagMetadata.GetString("nonexistentKey"); + + // Assert + Assert.Null(result); + } + + [Fact] + [Specification("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.")] + [Specification("1.4.14.1", "Condition: `Flag metadata` MUST be immutable.")] + public void GetString_Should_Return_Value_If_Key_Found() + { + // Arrange + var metadata = new Dictionary + { + { + "stringKey", "11" + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetString("stringKey"); + + // Assert + Assert.NotNull(result); + Assert.Equal("11", result); + } + + [Fact] + [Specification("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.")] + public void GetString_Should_Throw_Value_Is_Invalid() + { + // Arrange + var metadata = new Dictionary + { + { + "wrongKey", new object() + } + }; + var flagMetadata = new FlagMetadata(metadata); + + // Act + var result = flagMetadata.GetString("wrongKey"); + + // Assert + Assert.Null(result); + } +}