From fd0a54110866f3245152b28b64dedd286a752f64 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)
---
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);
+ }
+}