diff --git a/Directory.Packages.props b/Directory.Packages.props index 041434a47..194441b93 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,13 +10,19 @@ - - - - + + + + + - + @@ -42,4 +48,4 @@ - + \ No newline at end of file diff --git a/src/OpenFeature/Model/Value.cs b/src/OpenFeature/Model/Value.cs index f09a24667..41b15246b 100644 --- a/src/OpenFeature/Model/Value.cs +++ b/src/OpenFeature/Model/Value.cs @@ -6,7 +6,7 @@ namespace OpenFeature.Model; /// Values serve as a return type for provider objects. Providers may deal in JSON, protobuf, XML or some other data-interchange format. /// This intermediate representation provides a good medium of exchange. /// -public sealed class Value +public sealed class Value : IEquatable { private readonly object? _innerValue; @@ -184,4 +184,196 @@ public Value(Object value) /// /// Value as DateTime public DateTime? AsDateTime => this.IsDateTime ? (DateTime?)this._innerValue : null; + + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current ; otherwise, false. + public bool Equals(Value? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + + // Both are null + if (this.IsNull && other.IsNull) return true; + + // One is null, the other is not + if (this.IsNull != other.IsNull) return false; + + // Different types + if (this.GetValueType() != other.GetValueType()) return false; + + // Compare based on type + return this.GetValueType() switch + { + ValueType.Boolean => this.AsBoolean == other.AsBoolean, + ValueType.Number => this.AsDouble == other.AsDouble, + ValueType.String => this.AsString == other.AsString, + ValueType.DateTime => this.AsDateTime == other.AsDateTime, + ValueType.Structure => this.StructureEquals(other), + ValueType.List => this.ListEquals(other), + _ => false + }; + } + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current . + /// true if the specified object is equal to the current ; otherwise, false. + public override bool Equals(object? obj) => this.Equals(obj as Value); + + /// + /// Returns the hash code for this . + /// + /// A hash code for the current . + public override int GetHashCode() + { + if (this.IsNull) return 0; + + return this.GetValueType() switch + { + ValueType.Boolean => this.AsBoolean!.GetHashCode(), + ValueType.Number => this.AsDouble!.GetHashCode(), + ValueType.String => this.AsString!.GetHashCode(), + ValueType.DateTime => this.AsDateTime!.GetHashCode(), + ValueType.Structure => this.GetStructureHashCode(), + ValueType.List => this.GetListHashCode(), + _ => 0 + }; + } + + /// + /// Determines whether two instances are equal. + /// + /// The first to compare. + /// The second to compare. + /// true if the values are equal; otherwise, false. + public static bool operator ==(Value? left, Value? right) + { + if (left is null && right is null) return true; + if (left is null || right is null) return false; + return left.Equals(right); + } + + /// + /// Determines whether two instances are not equal. + /// + /// The first to compare. + /// The second to compare. + /// true if the values are not equal; otherwise, false. + public static bool operator !=(Value? left, Value? right) => !(left == right); + + /// + /// Gets the type of the current value. + /// + /// The of the current value. + private ValueType GetValueType() + { + if (this.IsNull) return ValueType.Null; + if (this.IsBoolean) return ValueType.Boolean; + if (this.IsNumber) return ValueType.Number; + if (this.IsString) return ValueType.String; + if (this.IsDateTime) return ValueType.DateTime; + if (this.IsStructure) return ValueType.Structure; + if (this.IsList) return ValueType.List; + return ValueType.Unknown; + } + + /// + /// Compares two Structure values for equality. + /// + /// The other to compare. + /// true if the structures are equal; otherwise, false. + private bool StructureEquals(Value other) + { + var thisStructure = this.AsStructure!; + var otherStructure = other.AsStructure!; + + if (thisStructure.Count != otherStructure.Count) return false; + + foreach (var kvp in thisStructure) + { + if (!otherStructure.TryGetValue(kvp.Key, out var otherValue) || !kvp.Value.Equals(otherValue)) + { + return false; + } + } + + return true; + } + + /// + /// Compares two List values for equality. + /// + /// The other to compare. + /// true if the lists are equal; otherwise, false. + private bool ListEquals(Value other) + { + var thisList = this.AsList!; + var otherList = other.AsList!; + + if (thisList.Count != otherList.Count) return false; + + for (int i = 0; i < thisList.Count; i++) + { + if (!thisList[i].Equals(otherList[i])) + { + return false; + } + } + + return true; + } + + /// + /// Gets the hash code for a Structure value. + /// + /// The hash code of the structure. + private int GetStructureHashCode() + { + var structure = this.AsStructure!; + var hash = new HashCode(); + + foreach (var kvp in structure) + { + hash.Add(kvp.Key); + hash.Add(kvp.Value); + } + + return hash.ToHashCode(); + } + + /// + /// Gets the hash code for a List value. + /// + /// The hash code of the list. + private int GetListHashCode() + { + var list = this.AsList!; + var hash = new HashCode(); + + foreach (var item in list) + { + hash.Add(item); + } + + return hash.ToHashCode(); + } + + /// + /// Represents the different types that a can contain. + /// + private enum ValueType + { + Null, + Boolean, + Number, + String, + DateTime, + Structure, + List, + Unknown + } } diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj index 3d81a99eb..732c92f3b 100644 --- a/src/OpenFeature/OpenFeature.csproj +++ b/src/OpenFeature/OpenFeature.csproj @@ -8,6 +8,7 @@ + diff --git a/test/OpenFeature.Tests/ValueTests.cs b/test/OpenFeature.Tests/ValueTests.cs index da76f29ad..9f94b5eaf 100644 --- a/test/OpenFeature.Tests/ValueTests.cs +++ b/test/OpenFeature.Tests/ValueTests.cs @@ -230,4 +230,561 @@ public void AsDateTime_WhenCalledWithNonDateTimeInnerValue_ReturnsNull() // Assert Assert.Null(actualValue); } + + #region Equality Tests + + [Fact] + public void Equals_WithNullValue_ReturnsFalse() + { + // Arrange + var value = new Value("test"); + + // Act & Assert + Assert.False(value.Equals(null)); + } + + [Fact] + public void Equals_WithSameReference_ReturnsTrue() + { + // Arrange + var value = new Value("test"); + + // Act & Assert + Assert.True(value.Equals(value)); + } + + [Fact] + public void Equals_WithBothNull_ReturnsTrue() + { + // Arrange + var value1 = new Value(); + var value2 = new Value(); + + // Act & Assert + Assert.True(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithOneNullOneNotNull_ReturnsFalse() + { + // Arrange + var nullValue = new Value(); + var stringValue = new Value("test"); + + // Act & Assert + Assert.False(nullValue.Equals(stringValue)); + Assert.False(stringValue.Equals(nullValue)); + } + + [Fact] + public void Equals_WithDifferentTypes_ReturnsFalse() + { + // Arrange + var stringValue = new Value("test"); + var intValue = new Value(42); + var boolValue = new Value(true); + + // Act & Assert + Assert.False(stringValue.Equals(intValue)); + Assert.False(stringValue.Equals(boolValue)); + Assert.False(intValue.Equals(boolValue)); + } + + [Fact] + public void Equals_WithSameStringValues_ReturnsTrue() + { + // Arrange + var value1 = new Value("test"); + var value2 = new Value("test"); + + // Act & Assert + Assert.True(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithDifferentStringValues_ReturnsFalse() + { + // Arrange + var value1 = new Value("test1"); + var value2 = new Value("test2"); + + // Act & Assert + Assert.False(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithSameBooleanValues_ReturnsTrue() + { + // Arrange + var value1 = new Value(true); + var value2 = new Value(true); + var value3 = new Value(false); + var value4 = new Value(false); + + // Act & Assert + Assert.True(value1.Equals(value2)); + Assert.True(value3.Equals(value4)); + } + + [Fact] + public void Equals_WithDifferentBooleanValues_ReturnsFalse() + { + // Arrange + var value1 = new Value(true); + var value2 = new Value(false); + + // Act & Assert + Assert.False(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithSameNumberValues_ReturnsTrue() + { + // Arrange + var value1 = new Value(42.5); + var value2 = new Value(42.5); + var intValue1 = new Value(42); + var intValue2 = new Value(42); + + // Act & Assert + Assert.True(value1.Equals(value2)); + Assert.True(intValue1.Equals(intValue2)); + } + + [Fact] + public void Equals_WithDifferentNumberValues_ReturnsFalse() + { + // Arrange + var value1 = new Value(42.5); + var value2 = new Value(42.6); + + // Act & Assert + Assert.False(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithSameDateTimeValues_ReturnsTrue() + { + // Arrange + var dateTime = DateTime.Now; + var value1 = new Value(dateTime); + var value2 = new Value(dateTime); + + // Act & Assert + Assert.True(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithDifferentDateTimeValues_ReturnsFalse() + { + // Arrange + var value1 = new Value(DateTime.Now); + var value2 = new Value(DateTime.Now.AddDays(1)); + + // Act & Assert + Assert.False(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithSameStructureValues_ReturnsTrue() + { + // Arrange + var structure1 = Structure.Builder() + .Set("key1", new Value("value1")) + .Set("key2", new Value(42)) + .Build(); + var structure2 = Structure.Builder() + .Set("key1", new Value("value1")) + .Set("key2", new Value(42)) + .Build(); + var value1 = new Value(structure1); + var value2 = new Value(structure2); + + // Act & Assert + Assert.True(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithDifferentStructureValues_ReturnsFalse() + { + // Arrange + var structure1 = Structure.Builder() + .Set("key1", new Value("value1")) + .Build(); + var structure2 = Structure.Builder() + .Set("key1", new Value("value2")) + .Build(); + var value1 = new Value(structure1); + var value2 = new Value(structure2); + + // Act & Assert + Assert.False(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithStructuresDifferentKeyCounts_ReturnsFalse() + { + // Arrange + var structure1 = Structure.Builder() + .Set("key1", new Value("value1")) + .Build(); + var structure2 = Structure.Builder() + .Set("key1", new Value("value1")) + .Set("key2", new Value("value2")) + .Build(); + var value1 = new Value(structure1); + var value2 = new Value(structure2); + + // Act & Assert + Assert.False(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithSameListValues_ReturnsTrue() + { + // Arrange + var list1 = new List { new("test"), new(42), new(true) }; + var list2 = new List { new("test"), new(42), new(true) }; + var value1 = new Value(list1); + var value2 = new Value(list2); + + // Act & Assert + Assert.True(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithDifferentListValues_ReturnsFalse() + { + // Arrange + var list1 = new List { new("test1"), new(42) }; + var list2 = new List { new("test2"), new(42) }; + var value1 = new Value(list1); + var value2 = new Value(list2); + + // Act & Assert + Assert.False(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithListsDifferentLengths_ReturnsFalse() + { + // Arrange + var list1 = new List { new("test") }; + var list2 = new List { new("test"), new(42) }; + var value1 = new Value(list1); + var value2 = new Value(list2); + + // Act & Assert + Assert.False(value1.Equals(value2)); + } + + [Fact] + public void Equals_WithObject_CallsTypedEquals() + { + // Arrange + var value1 = new Value("test"); + var value2 = new Value("test"); + object obj = value2; + + // Act & Assert + Assert.True(value1.Equals(obj)); + } + + [Fact] + public void Equals_WithNonValueObject_ReturnsFalse() + { + // Arrange + var value = new Value("test"); + object obj = "test"; + + // Act & Assert + Assert.False(value.Equals(obj)); + } + + #endregion + + #region Operator Tests + + [Fact] + public void OperatorEquals_WithBothNull_ReturnsTrue() + { + // Arrange + Value? value1 = null; + Value? value2 = null; + + // Act & Assert + Assert.True(value1 == value2); + } + + [Fact] + public void OperatorEquals_WithOneNull_ReturnsFalse() + { + // Arrange + Value? value1 = null; + Value value2 = new Value("test"); + + // Act & Assert + Assert.False(value1 == value2); + Assert.False(value2 == value1); + } + + [Fact] + public void OperatorEquals_WithEqualValues_ReturnsTrue() + { + // Arrange + var value1 = new Value("test"); + var value2 = new Value("test"); + + // Act & Assert + Assert.True(value1 == value2); + } + + [Fact] + public void OperatorEquals_WithDifferentValues_ReturnsFalse() + { + // Arrange + var value1 = new Value("test1"); + var value2 = new Value("test2"); + + // Act & Assert + Assert.False(value1 == value2); + } + + [Fact] + public void OperatorNotEquals_WithEqualValues_ReturnsFalse() + { + // Arrange + var value1 = new Value("test"); + var value2 = new Value("test"); + + // Act & Assert + Assert.False(value1 != value2); + } + + [Fact] + public void OperatorNotEquals_WithDifferentValues_ReturnsTrue() + { + // Arrange + var value1 = new Value("test1"); + var value2 = new Value("test2"); + + // Act & Assert + Assert.True(value1 != value2); + } + + #endregion + + #region GetHashCode Tests + + [Fact] + public void GetHashCode_WithNullValue_ReturnsZero() + { + // Arrange + var value = new Value(); + + // Act + var hashCode = value.GetHashCode(); + + // Assert + Assert.Equal(0, hashCode); + } + + [Fact] + public void GetHashCode_WithEqualValues_ReturnsSameHashCode() + { + // Arrange + var value1 = new Value("test"); + var value2 = new Value("test"); + + // Act + var hashCode1 = value1.GetHashCode(); + var hashCode2 = value2.GetHashCode(); + + // Assert + Assert.Equal(hashCode1, hashCode2); + } + + [Fact] + public void GetHashCode_WithBooleanValues_ReturnsConsistentHashCode() + { + // Arrange + var value1 = new Value(true); + var value2 = new Value(true); + var value3 = new Value(false); + + // Act + var hashCode1 = value1.GetHashCode(); + var hashCode2 = value2.GetHashCode(); + var hashCode3 = value3.GetHashCode(); + + // Assert + Assert.Equal(hashCode1, hashCode2); + Assert.NotEqual(hashCode1, hashCode3); + } + + [Fact] + public void GetHashCode_WithNumberValues_ReturnsConsistentHashCode() + { + // Arrange + var value1 = new Value(42.5); + var value2 = new Value(42.5); + var value3 = new Value(42.6); + + // Act + var hashCode1 = value1.GetHashCode(); + var hashCode2 = value2.GetHashCode(); + var hashCode3 = value3.GetHashCode(); + + // Assert + Assert.Equal(hashCode1, hashCode2); + Assert.NotEqual(hashCode1, hashCode3); + } + + [Fact] + public void GetHashCode_WithStringValues_ReturnsConsistentHashCode() + { + // Arrange + var value1 = new Value("test"); + var value2 = new Value("test"); + var value3 = new Value("different"); + + // Act + var hashCode1 = value1.GetHashCode(); + var hashCode2 = value2.GetHashCode(); + var hashCode3 = value3.GetHashCode(); + + // Assert + Assert.Equal(hashCode1, hashCode2); + Assert.NotEqual(hashCode1, hashCode3); + } + + [Fact] + public void GetHashCode_WithDateTimeValues_ReturnsConsistentHashCode() + { + // Arrange + var dateTime = DateTime.Now; + var value1 = new Value(dateTime); + var value2 = new Value(dateTime); + var value3 = new Value(dateTime.AddDays(1)); + + // Act + var hashCode1 = value1.GetHashCode(); + var hashCode2 = value2.GetHashCode(); + var hashCode3 = value3.GetHashCode(); + + // Assert + Assert.Equal(hashCode1, hashCode2); + Assert.NotEqual(hashCode1, hashCode3); + } + + [Fact] + public void GetHashCode_WithStructureValues_ReturnsConsistentHashCode() + { + // Arrange + var structure1 = Structure.Builder() + .Set("key1", new Value("value1")) + .Set("key2", new Value(42)) + .Build(); + var structure2 = Structure.Builder() + .Set("key1", new Value("value1")) + .Set("key2", new Value(42)) + .Build(); + var value1 = new Value(structure1); + var value2 = new Value(structure2); + + // Act + var hashCode1 = value1.GetHashCode(); + var hashCode2 = value2.GetHashCode(); + + // Assert + Assert.Equal(hashCode1, hashCode2); + } + + [Fact] + public void GetHashCode_WithListValues_ReturnsConsistentHashCode() + { + // Arrange + var list1 = new List { new("test"), new(42) }; + var list2 = new List { new("test"), new(42) }; + var value1 = new Value(list1); + var value2 = new Value(list2); + + // Act + var hashCode1 = value1.GetHashCode(); + var hashCode2 = value2.GetHashCode(); + + // Assert + Assert.Equal(hashCode1, hashCode2); + } + + #endregion + + #region Complex Nested Tests + + [Fact] + public void Equals_WithNestedStructuresAndLists_ReturnsTrue() + { + // Arrange + var innerList1 = new List { new("nested"), new(123) }; + var innerList2 = new List { new("nested"), new(123) }; + + var innerStructure1 = Structure.Builder() + .Set("nested_key", new Value("nested_value")) + .Set("nested_list", new Value(innerList1)) + .Build(); + var innerStructure2 = Structure.Builder() + .Set("nested_key", new Value("nested_value")) + .Set("nested_list", new Value(innerList2)) + .Build(); + + var outerStructure1 = Structure.Builder() + .Set("outer_key", new Value("outer_value")) + .Set("inner", new Value(innerStructure1)) + .Build(); + var outerStructure2 = Structure.Builder() + .Set("outer_key", new Value("outer_value")) + .Set("inner", new Value(innerStructure2)) + .Build(); + + var value1 = new Value(outerStructure1); + var value2 = new Value(outerStructure2); + + // Act & Assert + Assert.True(value1.Equals(value2)); + Assert.Equal(value1.GetHashCode(), value2.GetHashCode()); + } + + [Fact] + public void Equals_WithDeeplyNestedDifferences_ReturnsFalse() + { + // Arrange + var innerList1 = new List { new("nested"), new(123) }; + var innerList2 = new List { new("nested"), new(124) }; // Different value + + var innerStructure1 = Structure.Builder() + .Set("nested_key", new Value("nested_value")) + .Set("nested_list", new Value(innerList1)) + .Build(); + var innerStructure2 = Structure.Builder() + .Set("nested_key", new Value("nested_value")) + .Set("nested_list", new Value(innerList2)) + .Build(); + + var outerStructure1 = Structure.Builder() + .Set("outer_key", new Value("outer_value")) + .Set("inner", new Value(innerStructure1)) + .Build(); + var outerStructure2 = Structure.Builder() + .Set("outer_key", new Value("outer_value")) + .Set("inner", new Value(innerStructure2)) + .Build(); + + var value1 = new Value(outerStructure1); + var value2 = new Value(outerStructure2); + + // Act & Assert + Assert.False(value1.Equals(value2)); + } + + #endregion }