diff --git a/src/Cortex.Tests/Cortex.Tests.csproj b/src/Cortex.Tests/Cortex.Tests.csproj index 7194f92..8ee5bc3 100644 --- a/src/Cortex.Tests/Cortex.Tests.csproj +++ b/src/Cortex.Tests/Cortex.Tests.csproj @@ -31,6 +31,7 @@ + diff --git a/src/Cortex.Tests/Types/Tests/AnyOfTests.cs b/src/Cortex.Tests/Types/Tests/AnyOfTests.cs new file mode 100644 index 0000000..45833b5 --- /dev/null +++ b/src/Cortex.Tests/Types/Tests/AnyOfTests.cs @@ -0,0 +1,649 @@ +using Cortex.Types; + +namespace Cortex.Tests.Types.Tests +{ + public class AnyOfTests + { + #region AnyOf Tests + + [Fact] + public void AnyOf2_ImplicitConversion_FromT1_SetsCorrectTypeIndex() + { + AnyOf value = 42; + + Assert.Contains(0, value.TypeIndices); + Assert.Equal(42, value.Value); + } + + [Fact] + public void AnyOf2_ImplicitConversion_FromT2_SetsCorrectTypeIndex() + { + AnyOf value = "hello"; + + Assert.Contains(1, value.TypeIndices); + Assert.Equal("hello", value.Value); + } + + [Fact] + public void AnyOf2_Is_ReturnsTrue_WhenTypeMatches() + { + AnyOf value = 42; + + Assert.True(value.Is()); + Assert.False(value.Is()); + } + + [Fact] + public void AnyOf2_As_ReturnsValue_WhenTypeMatches() + { + AnyOf value = 42; + + Assert.Equal(42, value.As()); + } + + [Fact] + public void AnyOf2_As_ThrowsInvalidCastException_WhenTypeMismatch() + { + AnyOf value = 42; + + Assert.Throws(() => value.As()); + } + + [Fact] + public void AnyOf2_TryGet_ReturnsTrue_WhenTypeMatches() + { + AnyOf value = 42; + + Assert.True(value.TryGet(out int result)); + Assert.Equal(42, result); + } + + [Fact] + public void AnyOf2_TryGet_ReturnsFalse_WhenTypeMismatch() + { + AnyOf value = 42; + + Assert.False(value.TryGet(out string? result)); + } + + [Fact] + public void AnyOf2_Match_ExecutesCorrectHandler() + { + AnyOf intValue = 42; + AnyOf stringValue = "hello"; + + var intResult = intValue.Match( + i => $"int: {i}", + s => $"string: {s}"); + + var stringResult = stringValue.Match( + i => $"int: {i}", + s => $"string: {s}"); + + Assert.Equal("int: 42", intResult); + Assert.Equal("string: hello", stringResult); + } + + [Fact] + public void AnyOf2_Switch_ExecutesCorrectAction() + { + AnyOf value = 42; + int? capturedInt = null; + string? capturedString = null; + + value.Switch( + i => capturedInt = i, + s => capturedString = s); + + Assert.Equal(42, capturedInt); + Assert.Null(capturedString); + } + + [Fact] + public void AnyOf2_GetMatchingTypes_ReturnsMatchingTypes() + { + AnyOf value = 42; + + var matchingTypes = value.GetMatchingTypes().ToList(); + + Assert.Single(matchingTypes); + Assert.Contains(typeof(int), matchingTypes); + } + + [Fact] + public void AnyOf2_Equals_ReturnsTrue_ForSameValues() + { + AnyOf value1 = 42; + AnyOf value2 = 42; + + Assert.Equal(value1, value2); + Assert.True(value1 == value2); + Assert.False(value1 != value2); + } + + [Fact] + public void AnyOf2_ToString_ReturnsValueString() + { + AnyOf value = 42; + + Assert.Equal("42", value.ToString()); + } + + #endregion + + #region AnyOf Tests + + [Fact] + public void AnyOf3_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + AnyOf intVal = 42; + AnyOf strVal = "hello"; + AnyOf dblVal = 3.14; + + Assert.Contains(0, intVal.TypeIndices); + Assert.Contains(1, strVal.TypeIndices); + Assert.Contains(2, dblVal.TypeIndices); + } + + [Fact] + public void AnyOf3_Match_ExecutesCorrectHandler() + { + AnyOf value = 3.14; + + var result = value.Match( + i => "int", + s => "string", + d => "double"); + + Assert.Equal("double", result); + } + + [Fact] + public void AnyOf3_Switch_ExecutesCorrectAction() + { + AnyOf value = "hello"; + string? captured = null; + + value.Switch( + i => { }, + s => captured = s, + d => { }); + + Assert.Equal("hello", captured); + } + + [Fact] + public void AnyOf3_GetMatchingTypes_ReturnsMatchingTypes() + { + AnyOf value = 3.14; + + var matchingTypes = value.GetMatchingTypes().ToList(); + + Assert.Single(matchingTypes); + Assert.Contains(typeof(double), matchingTypes); + } + + #endregion + + #region AnyOf Tests + + [Fact] + public void AnyOf4_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + AnyOf val1 = 42; + AnyOf val2 = "hello"; + AnyOf val3 = 3.14; + AnyOf val4 = true; + + Assert.Contains(0, val1.TypeIndices); + Assert.Contains(1, val2.TypeIndices); + Assert.Contains(2, val3.TypeIndices); + Assert.Contains(3, val4.TypeIndices); + } + + [Fact] + public void AnyOf4_Match_ExecutesCorrectHandler() + { + AnyOf value = true; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool"); + + Assert.Equal("bool", result); + } + + [Fact] + public void AnyOf4_GetMatchingTypes_ReturnsMatchingTypes() + { + AnyOf value = true; + + var matchingTypes = value.GetMatchingTypes().ToList(); + + Assert.Single(matchingTypes); + Assert.Contains(typeof(bool), matchingTypes); + } + + #endregion + + #region AnyOf Tests + + [Fact] + public void AnyOf5_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + AnyOf val1 = 42; + AnyOf val2 = "hello"; + AnyOf val3 = 3.14; + AnyOf val4 = true; + AnyOf val5 = 'x'; + + Assert.Contains(0, val1.TypeIndices); + Assert.Contains(1, val2.TypeIndices); + Assert.Contains(2, val3.TypeIndices); + Assert.Contains(3, val4.TypeIndices); + Assert.Contains(4, val5.TypeIndices); + } + + [Fact] + public void AnyOf5_Match_ExecutesCorrectHandler() + { + AnyOf value = 'x'; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool", + c => "char"); + + Assert.Equal("char", result); + } + + [Fact] + public void AnyOf5_Switch_ExecutesCorrectAction() + { + AnyOf value = 'x'; + char? captured = null; + + value.Switch( + i => { }, + s => { }, + d => { }, + b => { }, + c => captured = c); + + Assert.Equal('x', captured); + } + + [Fact] + public void AnyOf5_TryGet_WorksCorrectly() + { + AnyOf value = 3.14; + + Assert.True(value.TryGet(out double d)); + Assert.Equal(3.14, d); + Assert.False(value.TryGet(out int _)); + } + + [Fact] + public void AnyOf5_GetMatchingTypes_ReturnsMatchingTypes() + { + AnyOf value = 'x'; + + var matchingTypes = value.GetMatchingTypes().ToList(); + + Assert.Single(matchingTypes); + Assert.Contains(typeof(char), matchingTypes); + } + + [Fact] + public void AnyOf5_Equals_WorksCorrectly() + { + AnyOf val1 = 'x'; + AnyOf val2 = 'x'; + AnyOf val3 = 'y'; + + Assert.Equal(val1, val2); + Assert.NotEqual(val1, val3); + } + + #endregion + + #region AnyOf Tests + + [Fact] + public void AnyOf6_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + AnyOf val1 = 42; + AnyOf val2 = "hello"; + AnyOf val3 = 3.14; + AnyOf val4 = true; + AnyOf val5 = 'x'; + AnyOf val6 = 100L; + + Assert.Contains(0, val1.TypeIndices); + Assert.Contains(1, val2.TypeIndices); + Assert.Contains(2, val3.TypeIndices); + Assert.Contains(3, val4.TypeIndices); + Assert.Contains(4, val5.TypeIndices); + Assert.Contains(5, val6.TypeIndices); + } + + [Fact] + public void AnyOf6_Match_ExecutesCorrectHandler() + { + AnyOf value = 100L; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool", + c => "char", + l => "long"); + + Assert.Equal("long", result); + } + + [Fact] + public void AnyOf6_Switch_ExecutesCorrectAction() + { + AnyOf value = 100L; + long? captured = null; + + value.Switch( + i => { }, + s => { }, + d => { }, + b => { }, + c => { }, + l => captured = l); + + Assert.Equal(100L, captured); + } + + [Fact] + public void AnyOf6_GetMatchingTypes_ReturnsMatchingTypes() + { + AnyOf value = 100L; + + var matchingTypes = value.GetMatchingTypes().ToList(); + + Assert.Single(matchingTypes); + Assert.Contains(typeof(long), matchingTypes); + } + + #endregion + + #region AnyOf Tests + + [Fact] + public void AnyOf7_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + AnyOf val1 = 42; + AnyOf val2 = "hello"; + AnyOf val3 = 3.14; + AnyOf val4 = true; + AnyOf val5 = 'x'; + AnyOf val6 = 100L; + AnyOf val7 = 1.5f; + + Assert.Contains(0, val1.TypeIndices); + Assert.Contains(1, val2.TypeIndices); + Assert.Contains(2, val3.TypeIndices); + Assert.Contains(3, val4.TypeIndices); + Assert.Contains(4, val5.TypeIndices); + Assert.Contains(5, val6.TypeIndices); + Assert.Contains(6, val7.TypeIndices); + } + + [Fact] + public void AnyOf7_Match_ExecutesCorrectHandler() + { + AnyOf value = 1.5f; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool", + c => "char", + l => "long", + f => "float"); + + Assert.Equal("float", result); + } + + [Fact] + public void AnyOf7_Switch_ExecutesCorrectAction() + { + AnyOf value = 1.5f; + float? captured = null; + + value.Switch( + i => { }, + s => { }, + d => { }, + b => { }, + c => { }, + l => { }, + f => captured = f); + + Assert.Equal(1.5f, captured); + } + + [Fact] + public void AnyOf7_GetMatchingTypes_ReturnsMatchingTypes() + { + AnyOf value = 1.5f; + + var matchingTypes = value.GetMatchingTypes().ToList(); + + Assert.Single(matchingTypes); + Assert.Contains(typeof(float), matchingTypes); + } + + [Fact] + public void AnyOf7_Equality_WorksCorrectly() + { + AnyOf val1 = 1.5f; + AnyOf val2 = 1.5f; + + Assert.True(val1 == val2); + Assert.False(val1 != val2); + } + + #endregion + + #region AnyOf Tests + + [Fact] + public void AnyOf8_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + AnyOf val1 = 42; + AnyOf val2 = "hello"; + AnyOf val3 = 3.14; + AnyOf val4 = true; + AnyOf val5 = 'x'; + AnyOf val6 = 100L; + AnyOf val7 = 1.5f; + AnyOf val8 = 99.99m; + + Assert.Contains(0, val1.TypeIndices); + Assert.Contains(1, val2.TypeIndices); + Assert.Contains(2, val3.TypeIndices); + Assert.Contains(3, val4.TypeIndices); + Assert.Contains(4, val5.TypeIndices); + Assert.Contains(5, val6.TypeIndices); + Assert.Contains(6, val7.TypeIndices); + Assert.Contains(7, val8.TypeIndices); + } + + [Fact] + public void AnyOf8_Match_ExecutesCorrectHandler() + { + AnyOf value = 99.99m; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool", + c => "char", + l => "long", + f => "float", + m => "decimal"); + + Assert.Equal("decimal", result); + } + + [Fact] + public void AnyOf8_Switch_ExecutesCorrectAction() + { + AnyOf value = 99.99m; + decimal? captured = null; + + value.Switch( + i => { }, + s => { }, + d => { }, + b => { }, + c => { }, + l => { }, + f => { }, + m => captured = m); + + Assert.Equal(99.99m, captured); + } + + [Fact] + public void AnyOf8_Is_WorksForAllTypes() + { + AnyOf value = 99.99m; + + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.True(value.Is()); + } + + [Fact] + public void AnyOf8_As_WorksCorrectly() + { + AnyOf value = 99.99m; + + Assert.Equal(99.99m, value.As()); + Assert.Throws(() => value.As()); + } + + [Fact] + public void AnyOf8_TryGet_WorksCorrectly() + { + AnyOf value = 99.99m; + + Assert.True(value.TryGet(out decimal d)); + Assert.Equal(99.99m, d); + Assert.False(value.TryGet(out int _)); + } + + [Fact] + public void AnyOf8_GetMatchingTypes_ReturnsMatchingTypes() + { + AnyOf value = 99.99m; + + var matchingTypes = value.GetMatchingTypes().ToList(); + + Assert.Single(matchingTypes); + Assert.Contains(typeof(decimal), matchingTypes); + } + + [Fact] + public void AnyOf8_Equality_WorksCorrectly() + { + AnyOf val1 = 99.99m; + AnyOf val2 = 99.99m; + AnyOf val3 = 100m; + + Assert.True(val1 == val2); + Assert.False(val1 != val2); + Assert.True(val1 != val3); + } + + [Fact] + public void AnyOf8_ToString_ReturnsValueString() + { + AnyOf value = "hello"; + + Assert.Equal("hello", value.ToString()); + } + + #endregion + + #region IAnyOf Interface Tests + + [Fact] + public void AllAnyOfTypes_ImplementIAnyOf() + { + IAnyOf anyOf2 = (AnyOf)42; + IAnyOf anyOf3 = (AnyOf)42; + IAnyOf anyOf4 = (AnyOf)42; + IAnyOf anyOf5 = (AnyOf)42; + IAnyOf anyOf6 = (AnyOf)42; + IAnyOf anyOf7 = (AnyOf)42; + IAnyOf anyOf8 = (AnyOf)42; + + Assert.Equal(42, anyOf2.Value); + Assert.Contains(0, anyOf2.TypeIndices); + + Assert.Equal(42, anyOf3.Value); + Assert.Equal(42, anyOf4.Value); + Assert.Equal(42, anyOf5.Value); + Assert.Equal(42, anyOf6.Value); + Assert.Equal(42, anyOf7.Value); + Assert.Equal(42, anyOf8.Value); + } + + #endregion + + #region Inheritance/Polymorphism Tests + + [Fact] + public void AnyOf_GetMatchingTypes_IncludesBaseTypes() + { + // ArgumentException derives from Exception + AnyOf value = new ArgumentException("test"); + + var matchingTypes = value.GetMatchingTypes().ToList(); + + // Both Exception (base) and the actual type should match + Assert.Contains(typeof(Exception), matchingTypes); + } + + [Fact] + public void AnyOf_Is_WorksWithDerivedTypes() + { + AnyOf value = new ArgumentException("test"); + + Assert.True(value.Is()); + Assert.True(value.Is()); + Assert.False(value.Is()); + } + + [Fact] + public void AnyOf_As_WorksWithDerivedTypes() + { + AnyOf value = new ArgumentException("test"); + + Assert.IsType(value.As()); + Assert.IsType(value.As()); + } + + #endregion + } +} diff --git a/src/Cortex.Tests/Types/Tests/OneOfTests.cs b/src/Cortex.Tests/Types/Tests/OneOfTests.cs new file mode 100644 index 0000000..cb8f04b --- /dev/null +++ b/src/Cortex.Tests/Types/Tests/OneOfTests.cs @@ -0,0 +1,571 @@ +using Cortex.Types; + +namespace Cortex.Tests.Types.Tests +{ + public class OneOfTests + { + #region OneOf Tests + + [Fact] + public void OneOf2_ImplicitConversion_FromT1_SetsCorrectTypeIndex() + { + OneOf value = 42; + + Assert.Equal(0, value.TypeIndex); + Assert.Equal(42, value.Value); + } + + [Fact] + public void OneOf2_ImplicitConversion_FromT2_SetsCorrectTypeIndex() + { + OneOf value = "hello"; + + Assert.Equal(1, value.TypeIndex); + Assert.Equal("hello", value.Value); + } + + [Fact] + public void OneOf2_Is_ReturnsTrue_WhenTypeMatches() + { + OneOf value = 42; + + Assert.True(value.Is()); + Assert.False(value.Is()); + } + + [Fact] + public void OneOf2_As_ReturnsValue_WhenTypeMatches() + { + OneOf value = 42; + + Assert.Equal(42, value.As()); + } + + [Fact] + public void OneOf2_As_ThrowsInvalidCastException_WhenTypeMismatch() + { + OneOf value = 42; + + Assert.Throws(() => value.As()); + } + + [Fact] + public void OneOf2_TryGet_ReturnsTrue_WhenTypeMatches() + { + OneOf value = 42; + + Assert.True(value.TryGet(out int result)); + Assert.Equal(42, result); + } + + [Fact] + public void OneOf2_TryGet_ReturnsFalse_WhenTypeMismatch() + { + OneOf value = 42; + + Assert.False(value.TryGet(out string? result)); + } + + [Fact] + public void OneOf2_Match_ExecutesCorrectHandler() + { + OneOf intValue = 42; + OneOf stringValue = "hello"; + + var intResult = intValue.Match( + i => $"int: {i}", + s => $"string: {s}"); + + var stringResult = stringValue.Match( + i => $"int: {i}", + s => $"string: {s}"); + + Assert.Equal("int: 42", intResult); + Assert.Equal("string: hello", stringResult); + } + + [Fact] + public void OneOf2_Switch_ExecutesCorrectAction() + { + OneOf value = 42; + int? capturedInt = null; + string? capturedString = null; + + value.Switch( + i => capturedInt = i, + s => capturedString = s); + + Assert.Equal(42, capturedInt); + Assert.Null(capturedString); + } + + [Fact] + public void OneOf2_Equals_ReturnsTrue_ForSameValues() + { + OneOf value1 = 42; + OneOf value2 = 42; + + Assert.Equal(value1, value2); + Assert.True(value1 == value2); + Assert.False(value1 != value2); + } + + [Fact] + public void OneOf2_Equals_ReturnsFalse_ForDifferentValues() + { + OneOf value1 = 42; + OneOf value2 = "hello"; + + Assert.NotEqual(value1, value2); + } + + [Fact] + public void OneOf2_ToString_ReturnsValueString() + { + OneOf value = 42; + + Assert.Equal("42", value.ToString()); + } + + #endregion + + #region OneOf Tests + + [Fact] + public void OneOf3_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + OneOf intVal = 42; + OneOf strVal = "hello"; + OneOf dblVal = 3.14; + + Assert.Equal(0, intVal.TypeIndex); + Assert.Equal(1, strVal.TypeIndex); + Assert.Equal(2, dblVal.TypeIndex); + } + + [Fact] + public void OneOf3_Match_ExecutesCorrectHandler() + { + OneOf value = 3.14; + + var result = value.Match( + i => "int", + s => "string", + d => "double"); + + Assert.Equal("double", result); + } + + [Fact] + public void OneOf3_Switch_ExecutesCorrectAction() + { + OneOf value = "hello"; + string? captured = null; + + value.Switch( + i => { }, + s => captured = s, + d => { }); + + Assert.Equal("hello", captured); + } + + #endregion + + #region OneOf Tests + + [Fact] + public void OneOf4_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + OneOf val1 = 42; + OneOf val2 = "hello"; + OneOf val3 = 3.14; + OneOf val4 = true; + + Assert.Equal(0, val1.TypeIndex); + Assert.Equal(1, val2.TypeIndex); + Assert.Equal(2, val3.TypeIndex); + Assert.Equal(3, val4.TypeIndex); + } + + [Fact] + public void OneOf4_Match_ExecutesCorrectHandler() + { + OneOf value = true; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool"); + + Assert.Equal("bool", result); + } + + #endregion + + #region OneOf Tests + + [Fact] + public void OneOf5_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + OneOf val1 = 42; + OneOf val2 = "hello"; + OneOf val3 = 3.14; + OneOf val4 = true; + OneOf val5 = 'x'; + + Assert.Equal(0, val1.TypeIndex); + Assert.Equal(1, val2.TypeIndex); + Assert.Equal(2, val3.TypeIndex); + Assert.Equal(3, val4.TypeIndex); + Assert.Equal(4, val5.TypeIndex); + } + + [Fact] + public void OneOf5_Match_ExecutesCorrectHandler() + { + OneOf value = 'x'; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool", + c => "char"); + + Assert.Equal("char", result); + } + + [Fact] + public void OneOf5_Switch_ExecutesCorrectAction() + { + OneOf value = 'x'; + char? captured = null; + + value.Switch( + i => { }, + s => { }, + d => { }, + b => { }, + c => captured = c); + + Assert.Equal('x', captured); + } + + [Fact] + public void OneOf5_TryGet_WorksCorrectly() + { + OneOf value = 3.14; + + Assert.True(value.TryGet(out double d)); + Assert.Equal(3.14, d); + Assert.False(value.TryGet(out int _)); + } + + [Fact] + public void OneOf5_Equals_WorksCorrectly() + { + OneOf val1 = 'x'; + OneOf val2 = 'x'; + OneOf val3 = 'y'; + + Assert.Equal(val1, val2); + Assert.NotEqual(val1, val3); + } + + #endregion + + #region OneOf Tests + + [Fact] + public void OneOf6_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + OneOf val1 = 42; + OneOf val2 = "hello"; + OneOf val3 = 3.14; + OneOf val4 = true; + OneOf val5 = 'x'; + OneOf val6 = 100L; + + Assert.Equal(0, val1.TypeIndex); + Assert.Equal(1, val2.TypeIndex); + Assert.Equal(2, val3.TypeIndex); + Assert.Equal(3, val4.TypeIndex); + Assert.Equal(4, val5.TypeIndex); + Assert.Equal(5, val6.TypeIndex); + } + + [Fact] + public void OneOf6_Match_ExecutesCorrectHandler() + { + OneOf value = 100L; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool", + c => "char", + l => "long"); + + Assert.Equal("long", result); + } + + [Fact] + public void OneOf6_Switch_ExecutesCorrectAction() + { + OneOf value = 100L; + long? captured = null; + + value.Switch( + i => { }, + s => { }, + d => { }, + b => { }, + c => { }, + l => captured = l); + + Assert.Equal(100L, captured); + } + + #endregion + + #region OneOf Tests + + [Fact] + public void OneOf7_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + OneOf val1 = 42; + OneOf val2 = "hello"; + OneOf val3 = 3.14; + OneOf val4 = true; + OneOf val5 = 'x'; + OneOf val6 = 100L; + OneOf val7 = 1.5f; + + Assert.Equal(0, val1.TypeIndex); + Assert.Equal(1, val2.TypeIndex); + Assert.Equal(2, val3.TypeIndex); + Assert.Equal(3, val4.TypeIndex); + Assert.Equal(4, val5.TypeIndex); + Assert.Equal(5, val6.TypeIndex); + Assert.Equal(6, val7.TypeIndex); + } + + [Fact] + public void OneOf7_Match_ExecutesCorrectHandler() + { + OneOf value = 1.5f; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool", + c => "char", + l => "long", + f => "float"); + + Assert.Equal("float", result); + } + + [Fact] + public void OneOf7_Switch_ExecutesCorrectAction() + { + OneOf value = 1.5f; + float? captured = null; + + value.Switch( + i => { }, + s => { }, + d => { }, + b => { }, + c => { }, + l => { }, + f => captured = f); + + Assert.Equal(1.5f, captured); + } + + [Fact] + public void OneOf7_Equality_WorksCorrectly() + { + OneOf val1 = 1.5f; + OneOf val2 = 1.5f; + + Assert.True(val1 == val2); + Assert.False(val1 != val2); + Assert.Equal(val1.GetHashCode(), val2.GetHashCode()); + } + + #endregion + + #region OneOf Tests + + [Fact] + public void OneOf8_ImplicitConversion_FromEachType_SetsCorrectTypeIndex() + { + OneOf val1 = 42; + OneOf val2 = "hello"; + OneOf val3 = 3.14; + OneOf val4 = true; + OneOf val5 = 'x'; + OneOf val6 = 100L; + OneOf val7 = 1.5f; + OneOf val8 = 99.99m; + + Assert.Equal(0, val1.TypeIndex); + Assert.Equal(1, val2.TypeIndex); + Assert.Equal(2, val3.TypeIndex); + Assert.Equal(3, val4.TypeIndex); + Assert.Equal(4, val5.TypeIndex); + Assert.Equal(5, val6.TypeIndex); + Assert.Equal(6, val7.TypeIndex); + Assert.Equal(7, val8.TypeIndex); + } + + [Fact] + public void OneOf8_Match_ExecutesCorrectHandler() + { + OneOf value = 99.99m; + + var result = value.Match( + i => "int", + s => "string", + d => "double", + b => "bool", + c => "char", + l => "long", + f => "float", + m => "decimal"); + + Assert.Equal("decimal", result); + } + + [Fact] + public void OneOf8_Switch_ExecutesCorrectAction() + { + OneOf value = 99.99m; + decimal? captured = null; + + value.Switch( + i => { }, + s => { }, + d => { }, + b => { }, + c => { }, + l => { }, + f => { }, + m => captured = m); + + Assert.Equal(99.99m, captured); + } + + [Fact] + public void OneOf8_Is_WorksForAllTypes() + { + OneOf value = 99.99m; + + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.False(value.Is()); + Assert.True(value.Is()); + } + + [Fact] + public void OneOf8_As_WorksCorrectly() + { + OneOf value = 99.99m; + + Assert.Equal(99.99m, value.As()); + Assert.Throws(() => value.As()); + } + + [Fact] + public void OneOf8_TryGet_WorksCorrectly() + { + OneOf value = 99.99m; + + Assert.True(value.TryGet(out decimal d)); + Assert.Equal(99.99m, d); + Assert.False(value.TryGet(out int _)); + } + + [Fact] + public void OneOf8_Equality_WorksCorrectly() + { + OneOf val1 = 99.99m; + OneOf val2 = 99.99m; + OneOf val3 = 100m; + + Assert.True(val1 == val2); + Assert.False(val1 != val2); + Assert.True(val1 != val3); + Assert.Equal(val1.GetHashCode(), val2.GetHashCode()); + } + + [Fact] + public void OneOf8_ToString_ReturnsValueString() + { + OneOf value = "hello"; + + Assert.Equal("hello", value.ToString()); + } + + #endregion + + #region IOneOf Interface Tests + + [Fact] + public void AllOneOfTypes_ImplementIOneOf() + { + IOneOf oneOf2 = (OneOf)42; + IOneOf oneOf3 = (OneOf)42; + IOneOf oneOf4 = (OneOf)42; + IOneOf oneOf5 = (OneOf)42; + IOneOf oneOf6 = (OneOf)42; + IOneOf oneOf7 = (OneOf)42; + IOneOf oneOf8 = (OneOf)42; + + Assert.Equal(42, oneOf2.Value); + Assert.Equal(0, oneOf2.TypeIndex); + + Assert.Equal(42, oneOf3.Value); + Assert.Equal(42, oneOf4.Value); + Assert.Equal(42, oneOf5.Value); + Assert.Equal(42, oneOf6.Value); + Assert.Equal(42, oneOf7.Value); + Assert.Equal(42, oneOf8.Value); + } + + #endregion + + #region Inheritance Tests + + [Fact] + public void OneOf_Is_WorksWithDerivedTypes() + { + OneOf value = new ArgumentException("test"); + + Assert.True(value.Is()); + Assert.True(value.Is()); + Assert.False(value.Is()); + } + + [Fact] + public void OneOf_As_WorksWithDerivedTypes() + { + OneOf value = new ArgumentException("test"); + + Assert.IsType(value.As()); + Assert.IsType(value.As()); + } + + #endregion + } +} diff --git a/src/Cortex.Tests/Types/Tests/Result2Tests.cs b/src/Cortex.Tests/Types/Tests/Result2Tests.cs new file mode 100644 index 0000000..49c32fd --- /dev/null +++ b/src/Cortex.Tests/Types/Tests/Result2Tests.cs @@ -0,0 +1,665 @@ +using Cortex.Types; + +namespace Cortex.Tests.Types.Tests +{ + public class Result2Tests + { + #region Custom Error Type for Testing + + private record TestError(string Code, string Description); + + #endregion + + #region Creation Tests + + [Fact] + public void Success_CreatesSuccessfulResult() + { + // Act + var result = Result.Success(42); + + // Assert + Assert.True(result.IsSuccess); + Assert.False(result.IsFailure); + Assert.Equal(42, result.Value); + } + + [Fact] + public void Failure_CreatesFailedResult() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + + // Act + var result = Result.Failure(error); + + // Assert + Assert.False(result.IsSuccess); + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } + + #endregion + + #region Implicit Conversion Tests + + [Fact] + public void ImplicitConversion_FromValue_CreatesSuccessResult() + { + // Act + Result result = "test value"; + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal("test value", result.Value); + } + + #endregion + + #region Value Access Tests + + [Fact] + public void Value_OnSuccess_ReturnsValue() + { + // Arrange + var result = Result.Success(42); + + // Act & Assert + Assert.Equal(42, result.Value); + } + + [Fact] + public void Value_OnFailure_ThrowsInvalidOperationException() + { + // Arrange + var result = Result.Failure(new TestError("ERR", "Error")); + + // Act & Assert + var exception = Assert.Throws(() => result.Value); + Assert.Contains("Cannot access Value", exception.Message); + } + + [Fact] + public void Error_OnFailure_ReturnsError() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + var result = Result.Failure(error); + + // Act & Assert + Assert.Equal(error, result.Error); + } + + [Fact] + public void Error_OnSuccess_ThrowsInvalidOperationException() + { + // Arrange + var result = Result.Success(42); + + // Act & Assert + var exception = Assert.Throws(() => result.Error); + Assert.Contains("Cannot access Error", exception.Message); + } + + #endregion + + #region TryGet Tests + + [Fact] + public void TryGetValue_OnSuccess_ReturnsTrueAndValue() + { + // Arrange + var result = Result.Success(42); + + // Act + var success = result.TryGetValue(out var value); + + // Assert + Assert.True(success); + Assert.Equal(42, value); + } + + [Fact] + public void TryGetValue_OnFailure_ReturnsFalse() + { + // Arrange + var result = Result.Failure(new TestError("ERR", "Error")); + + // Act + var success = result.TryGetValue(out var value); + + // Assert + Assert.False(success); + Assert.Equal(default, value); + } + + [Fact] + public void TryGetError_OnFailure_ReturnsTrueAndError() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + var result = Result.Failure(error); + + // Act + var hasError = result.TryGetError(out var retrievedError); + + // Assert + Assert.True(hasError); + Assert.Equal(error, retrievedError); + } + + [Fact] + public void TryGetError_OnSuccess_ReturnsFalse() + { + // Arrange + var result = Result.Success(42); + + // Act + var hasError = result.TryGetError(out var error); + + // Assert + Assert.False(hasError); + Assert.Null(error); + } + + #endregion + + #region GetValueOrDefault Tests + + [Fact] + public void GetValueOrDefault_OnSuccess_ReturnsValue() + { + // Arrange + var result = Result.Success(42); + + // Act + var value = result.GetValueOrDefault(0); + + // Assert + Assert.Equal(42, value); + } + + [Fact] + public void GetValueOrDefault_OnFailure_ReturnsDefault() + { + // Arrange + var result = Result.Failure(new TestError("ERR", "Error")); + + // Act + var value = result.GetValueOrDefault(99); + + // Assert + Assert.Equal(99, value); + } + + [Fact] + public void GetValueOrDefault_WithFactory_OnFailure_CallsFactory() + { + // Arrange + var result = Result.Failure(new TestError("ERR", "Error")); + + // Act + var value = result.GetValueOrDefault(() => 99); + + // Assert + Assert.Equal(99, value); + } + + [Fact] + public void GetValueOrDefault_WithErrorHandler_OnFailure_PassesError() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + var result = Result.Failure(error); + TestError? capturedError = null; + + // Act + var value = result.GetValueOrDefault(e => { capturedError = e; return "default"; }); + + // Assert + Assert.Equal("default", value); + Assert.Equal(error, capturedError); + } + + #endregion + + #region Match Tests + + [Fact] + public void Match_OnSuccess_ExecutesSuccessHandler() + { + // Arrange + var result = Result.Success(42); + + // Act + var output = result.Match( + onSuccess: v => $"Success: {v}", + onFailure: e => $"Failure: {e.Code}"); + + // Assert + Assert.Equal("Success: 42", output); + } + + [Fact] + public void Match_OnFailure_ExecutesFailureHandler() + { + // Arrange + var result = Result.Failure(new TestError("ERR001", "Error")); + + // Act + var output = result.Match( + onSuccess: v => $"Success: {v}", + onFailure: e => $"Failure: {e.Code}"); + + // Assert + Assert.Equal("Failure: ERR001", output); + } + + #endregion + + #region Switch Tests + + [Fact] + public void Switch_OnSuccess_ExecutesSuccessAction() + { + // Arrange + var result = Result.Success(42); + int? capturedValue = null; + TestError? capturedError = null; + + // Act + result.Switch( + onSuccess: v => capturedValue = v, + onFailure: e => capturedError = e); + + // Assert + Assert.Equal(42, capturedValue); + Assert.Null(capturedError); + } + + [Fact] + public void Switch_OnFailure_ExecutesFailureAction() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + var result = Result.Failure(error); + int? capturedValue = null; + TestError? capturedError = null; + + // Act + result.Switch( + onSuccess: v => capturedValue = v, + onFailure: e => capturedError = e); + + // Assert + Assert.Null(capturedValue); + Assert.Equal(error, capturedError); + } + + #endregion + + #region Map Tests + + [Fact] + public void Map_OnSuccess_TransformsValue() + { + // Arrange + var result = Result.Success(42); + + // Act + var mapped = result.Map(v => v.ToString()); + + // Assert + Assert.True(mapped.IsSuccess); + Assert.Equal("42", mapped.Value); + } + + [Fact] + public void Map_OnFailure_PreservesError() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + var result = Result.Failure(error); + + // Act + var mapped = result.Map(v => v.ToString()); + + // Assert + Assert.True(mapped.IsFailure); + Assert.Equal(error, mapped.Error); + } + + [Fact] + public void MapError_OnFailure_TransformsError() + { + // Arrange + var result = Result.Failure(new TestError("ERR001", "Original")); + + // Act + var mapped = result.MapError(e => new TestError(e.Code, $"Mapped: {e.Description}")); + + // Assert + Assert.True(mapped.IsFailure); + Assert.Equal("Mapped: Original", mapped.Error.Description); + } + + [Fact] + public void MapError_CanChangeErrorType() + { + // Arrange + var result = Result.Failure(new TestError("ERR001", "Test")); + + // Act + var mapped = result.MapError(e => e.Code); // Transform to string error + + // Assert + Assert.True(mapped.IsFailure); + Assert.Equal("ERR001", mapped.Error); + } + + [Fact] + public void MapError_OnSuccess_PreservesValue() + { + // Arrange + var result = Result.Success(42); + + // Act + var mapped = result.MapError(e => new TestError("NEW", "Should not happen")); + + // Assert + Assert.True(mapped.IsSuccess); + Assert.Equal(42, mapped.Value); + } + + #endregion + + #region Bind Tests + + [Fact] + public void Bind_OnSuccess_ChainsOperation() + { + // Arrange + var result = Result.Success(42); + + // Act + var bound = result.Bind(v => Result.Success($"Value: {v}")); + + // Assert + Assert.True(bound.IsSuccess); + Assert.Equal("Value: 42", bound.Value); + } + + [Fact] + public void Bind_OnSuccess_CanReturnFailure() + { + // Arrange + var result = Result.Success(42); + var error = new TestError("VAL001", "Validation failed"); + + // Act + var bound = result.Bind(v => Result.Failure(error)); + + // Assert + Assert.True(bound.IsFailure); + Assert.Equal(error, bound.Error); + } + + [Fact] + public void Bind_OnFailure_SkipsOperation() + { + // Arrange + var error = new TestError("ERR001", "Original error"); + var result = Result.Failure(error); + var operationCalled = false; + + // Act + var bound = result.Bind(v => { operationCalled = true; return Result.Success("test"); }); + + // Assert + Assert.True(bound.IsFailure); + Assert.Equal(error, bound.Error); + Assert.False(operationCalled); + } + + #endregion + + #region Tap Tests + + [Fact] + public void Tap_OnSuccess_ExecutesAction() + { + // Arrange + var result = Result.Success(42); + int? capturedValue = null; + + // Act + var tapped = result.Tap(v => capturedValue = v); + + // Assert + Assert.Equal(42, capturedValue); + Assert.Equal(result, tapped); + } + + [Fact] + public void Tap_OnFailure_SkipsAction() + { + // Arrange + var result = Result.Failure(new TestError("ERR", "Error")); + var actionCalled = false; + + // Act + var tapped = result.Tap(v => actionCalled = true); + + // Assert + Assert.False(actionCalled); + } + + [Fact] + public void TapError_OnFailure_ExecutesAction() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + var result = Result.Failure(error); + TestError? capturedError = null; + + // Act + var tapped = result.TapError(e => capturedError = e); + + // Assert + Assert.Equal(error, capturedError); + } + + [Fact] + public void TapError_OnSuccess_SkipsAction() + { + // Arrange + var result = Result.Success(42); + var actionCalled = false; + + // Act + var tapped = result.TapError(e => actionCalled = true); + + // Assert + Assert.False(actionCalled); + } + + #endregion + + #region Ensure Tests + + [Fact] + public void Ensure_WhenPredicatePasses_ReturnsOriginalResult() + { + // Arrange + var result = Result.Success(42); + + // Act + var ensured = result.Ensure(v => v > 0, new TestError("VAL001", "Must be positive")); + + // Assert + Assert.True(ensured.IsSuccess); + Assert.Equal(42, ensured.Value); + } + + [Fact] + public void Ensure_WhenPredicateFails_ReturnsFailure() + { + // Arrange + var result = Result.Success(-5); + var error = new TestError("VAL001", "Must be positive"); + + // Act + var ensured = result.Ensure(v => v > 0, error); + + // Assert + Assert.True(ensured.IsFailure); + Assert.Equal(error, ensured.Error); + } + + [Fact] + public void Ensure_OnFailure_SkipsPredicate() + { + // Arrange + var originalError = new TestError("ERR001", "Original error"); + var result = Result.Failure(originalError); + var predicateCalled = false; + + // Act + var ensured = result.Ensure(v => { predicateCalled = true; return v > 0; }, new TestError("NEW", "New")); + + // Assert + Assert.True(ensured.IsFailure); + Assert.Equal(originalError, ensured.Error); + Assert.False(predicateCalled); + } + + #endregion + + #region ToResult Tests + + [Fact] + public void ToResult_OnSuccess_ConvertsToBuiltInResult() + { + // Arrange + var result = Result.Success(42); + + // Act + var converted = result.ToResult(e => new ResultError(e.Description, e.Code)); + + // Assert + Assert.True(converted.IsSuccess); + Assert.Equal(42, converted.Value); + } + + [Fact] + public void ToResult_OnFailure_ConvertsErrorToResultError() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + var result = Result.Failure(error); + + // Act + var converted = result.ToResult(e => new ResultError(e.Description, e.Code)); + + // Assert + Assert.True(converted.IsFailure); + Assert.Equal("Test error", converted.Error.Message); + Assert.Equal("ERR001", converted.Error.Code); + } + + #endregion + + #region Equality Tests + + [Fact] + public void Equals_SuccessResultsWithSameValue_ReturnsTrue() + { + // Arrange + var result1 = Result.Success(42); + var result2 = Result.Success(42); + + // Act & Assert + Assert.Equal(result1, result2); + Assert.True(result1 == result2); + Assert.False(result1 != result2); + } + + [Fact] + public void Equals_SuccessResultsWithDifferentValues_ReturnsFalse() + { + // Arrange + var result1 = Result.Success(42); + var result2 = Result.Success(99); + + // Act & Assert + Assert.NotEqual(result1, result2); + } + + [Fact] + public void Equals_FailureResultsWithSameError_ReturnsTrue() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + var result1 = Result.Failure(error); + var result2 = Result.Failure(error); + + // Act & Assert + Assert.Equal(result1, result2); + } + + [Fact] + public void Equals_SuccessAndFailure_ReturnsFalse() + { + // Arrange + var success = Result.Success(42); + var failure = Result.Failure(new TestError("ERR", "Error")); + + // Act & Assert + Assert.NotEqual(success, failure); + } + + [Fact] + public void GetHashCode_SameResults_ReturnsSameHashCode() + { + // Arrange + var result1 = Result.Success(42); + var result2 = Result.Success(42); + + // Act & Assert + Assert.Equal(result1.GetHashCode(), result2.GetHashCode()); + } + + #endregion + + #region ToString Tests + + [Fact] + public void ToString_OnSuccess_ReturnsFormattedString() + { + // Arrange + var result = Result.Success(42); + + // Act + var str = result.ToString(); + + // Assert + Assert.Equal("Success(42)", str); + } + + [Fact] + public void ToString_OnFailure_ReturnsFormattedString() + { + // Arrange + var error = new TestError("ERR001", "Test error"); + var result = Result.Failure(error); + + // Act + var str = result.ToString(); + + // Assert + Assert.Contains("Failure", str); + } + + #endregion + } +} diff --git a/src/Cortex.Tests/Types/Tests/ResultErrorTests.cs b/src/Cortex.Tests/Types/Tests/ResultErrorTests.cs new file mode 100644 index 0000000..f78acab --- /dev/null +++ b/src/Cortex.Tests/Types/Tests/ResultErrorTests.cs @@ -0,0 +1,221 @@ +using Cortex.Types; + +namespace Cortex.Tests.Types.Tests +{ + public class ResultErrorTests + { + [Fact] + public void Constructor_WithMessage_SetsMessageProperty() + { + // Arrange & Act + var error = new ResultError("Test error"); + + // Assert + Assert.Equal("Test error", error.Message); + Assert.Null(error.Code); + Assert.Null(error.Exception); + Assert.Empty(error.Metadata); + } + + [Fact] + public void Constructor_WithMessageAndCode_SetsBothProperties() + { + // Arrange & Act + var error = new ResultError("Test error", "ERR001"); + + // Assert + Assert.Equal("Test error", error.Message); + Assert.Equal("ERR001", error.Code); + Assert.Null(error.Exception); + } + + [Fact] + public void Constructor_WithMessageAndException_SetsBothProperties() + { + // Arrange + var exception = new InvalidOperationException("Inner exception"); + + // Act + var error = new ResultError("Test error", exception); + + // Assert + Assert.Equal("Test error", error.Message); + Assert.Null(error.Code); + Assert.Same(exception, error.Exception); + } + + [Fact] + public void Constructor_WithAllParameters_SetsAllProperties() + { + // Arrange + var exception = new InvalidOperationException("Inner exception"); + var metadata = new Dictionary { ["key"] = "value" }; + + // Act + var error = new ResultError("Test error", "ERR001", exception, metadata); + + // Assert + Assert.Equal("Test error", error.Message); + Assert.Equal("ERR001", error.Code); + Assert.Same(exception, error.Exception); + Assert.Equal("value", error.Metadata["key"]); + } + + [Fact] + public void Constructor_WithNullMessage_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => new ResultError(null!)); + } + + [Fact] + public void FromException_CreatesErrorFromException() + { + // Arrange + var exception = new ArgumentException("Argument error"); + + // Act + var error = ResultError.FromException(exception); + + // Assert + Assert.Equal("Argument error", error.Message); + Assert.Equal("ArgumentException", error.Code); + Assert.Same(exception, error.Exception); + } + + [Fact] + public void FromException_WithNullException_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => ResultError.FromException(null!)); + } + + [Fact] + public void Aggregate_WithSingleError_ReturnsSameError() + { + // Arrange + var error = new ResultError("Single error"); + + // Act + var result = ResultError.Aggregate(new[] { error }); + + // Assert + Assert.Same(error, result); + } + + [Fact] + public void Aggregate_WithMultipleErrors_CreatesCompositeError() + { + // Arrange + var error1 = new ResultError("Error 1"); + var error2 = new ResultError("Error 2"); + + // Act + var result = ResultError.Aggregate(new[] { error1, error2 }); + + // Assert + Assert.Contains("Error 1", result.Message); + Assert.Contains("Error 2", result.Message); + Assert.Equal("AGGREGATE_ERROR", result.Code); + Assert.True(result.Metadata.ContainsKey("InnerErrors")); + } + + [Fact] + public void Aggregate_WithNullCollection_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => ResultError.Aggregate(null!)); + } + + [Fact] + public void Aggregate_WithEmptyCollection_ThrowsArgumentException() + { + // Act & Assert + Assert.Throws(() => ResultError.Aggregate(Array.Empty())); + } + + [Fact] + public void Equals_WithSameMessageAndCode_ReturnsTrue() + { + // Arrange + var error1 = new ResultError("Test error", "ERR001"); + var error2 = new ResultError("Test error", "ERR001"); + + // Act & Assert + Assert.Equal(error1, error2); + Assert.True(error1 == error2); + Assert.False(error1 != error2); + } + + [Fact] + public void Equals_WithDifferentMessage_ReturnsFalse() + { + // Arrange + var error1 = new ResultError("Error 1"); + var error2 = new ResultError("Error 2"); + + // Act & Assert + Assert.NotEqual(error1, error2); + Assert.False(error1 == error2); + Assert.True(error1 != error2); + } + + [Fact] + public void Equals_WithDifferentCode_ReturnsFalse() + { + // Arrange + var error1 = new ResultError("Test error", "ERR001"); + var error2 = new ResultError("Test error", "ERR002"); + + // Act & Assert + Assert.NotEqual(error1, error2); + } + + [Fact] + public void Equals_WithNull_ReturnsFalse() + { + // Arrange + var error = new ResultError("Test error"); + + // Act & Assert + Assert.False(error.Equals(null)); + } + + [Fact] + public void GetHashCode_SameErrors_ReturnsSameHashCode() + { + // Arrange + var error1 = new ResultError("Test error", "ERR001"); + var error2 = new ResultError("Test error", "ERR001"); + + // Act & Assert + Assert.Equal(error1.GetHashCode(), error2.GetHashCode()); + } + + [Fact] + public void ToString_WithoutCode_ReturnsMessage() + { + // Arrange + var error = new ResultError("Test error"); + + // Act + var result = error.ToString(); + + // Assert + Assert.Equal("Test error", result); + } + + [Fact] + public void ToString_WithCode_ReturnsFormattedString() + { + // Arrange + var error = new ResultError("Test error", "ERR001"); + + // Act + var result = error.ToString(); + + // Assert + Assert.Equal("[ERR001] Test error", result); + } + } +} diff --git a/src/Cortex.Tests/Types/Tests/ResultExtensionsTests.cs b/src/Cortex.Tests/Types/Tests/ResultExtensionsTests.cs new file mode 100644 index 0000000..138f652 --- /dev/null +++ b/src/Cortex.Tests/Types/Tests/ResultExtensionsTests.cs @@ -0,0 +1,483 @@ +using Cortex.Types; + +namespace Cortex.Tests.Types.Tests +{ + public class ResultExtensionsTests + { + #region Static Factory Methods Tests + + [Fact] + public void Success_CreatesSuccessfulResult() + { + // Act + var result = Result.Success(42); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact] + public void Success_WithCustomErrorType_CreatesSuccessfulResult() + { + // Act + var result = Result.Success(42); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact] + public void Failure_WithError_CreatesFailedResult() + { + // Arrange + var error = new ResultError("Test error"); + + // Act + var result = Result.Failure(error); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } + + [Fact] + public void Failure_WithMessage_CreatesFailedResult() + { + // Act + var result = Result.Failure("Test error"); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Test error", result.Error.Message); + } + + [Fact] + public void Failure_WithException_CreatesFailedResult() + { + // Arrange + var exception = new InvalidOperationException("Test exception"); + + // Act + var result = Result.Failure(exception); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Test exception", result.Error.Message); + Assert.Same(exception, result.Error.Exception); + } + + [Fact] + public void Failure_WithCustomErrorType_CreatesFailedResult() + { + // Act + var result = Result.Failure("Custom error"); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Custom error", result.Error); + } + + #endregion + + #region Try Tests + + [Fact] + public void Try_WhenFunctionSucceeds_ReturnsSuccessResult() + { + // Act + var result = Result.Try(() => 42); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact] + public void Try_WhenFunctionThrows_ReturnsFailureResult() + { + // Arrange + var exception = new InvalidOperationException("Test exception"); + + // Act + var result = Result.Try(() => throw exception); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Test exception", result.Error.Message); + Assert.Same(exception, result.Error.Exception); + } + + [Fact] + public void Try_WithExceptionHandler_WhenFunctionSucceeds_ReturnsSuccessResult() + { + // Act + var result = Result.Try( + () => 42, + ex => new ResultError($"Handled: {ex.Message}")); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact] + public void Try_WithExceptionHandler_WhenFunctionThrows_UsesHandler() + { + // Arrange + var exception = new InvalidOperationException("Test exception"); + + // Act + var result = Result.Try( + () => throw exception, + ex => new ResultError($"Handled: {ex.Message}", "HANDLED")); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Handled: Test exception", result.Error.Message); + Assert.Equal("HANDLED", result.Error.Code); + } + + #endregion + + #region TryAsync Tests + + [Fact] + public async Task TryAsync_WhenFunctionSucceeds_ReturnsSuccessResult() + { + // Act + var result = await Result.TryAsync(async () => + { + await Task.Delay(1); + return 42; + }); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact] + public async Task TryAsync_WhenFunctionThrows_ReturnsFailureResult() + { + // Arrange + var exception = new InvalidOperationException("Test exception"); + + // Act + var result = await Result.TryAsync(async () => + { + await Task.Delay(1); + throw exception; + }); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Test exception", result.Error.Message); + Assert.Same(exception, result.Error.Exception); + } + + #endregion + + #region Combine Tests + + [Fact] + public void Combine_TwoSuccessResults_ReturnsCombinedSuccess() + { + // Arrange + var result1 = Result.Success(42); + var result2 = Result.Success("test"); + + // Act + var combined = Result.Combine(result1, result2); + + // Assert + Assert.True(combined.IsSuccess); + Assert.Equal((42, "test"), combined.Value); + } + + [Fact] + public void Combine_FirstResultFails_ReturnsFirstError() + { + // Arrange + var error = new ResultError("First error"); + var result1 = Result.Failure(error); + var result2 = Result.Success("test"); + + // Act + var combined = Result.Combine(result1, result2); + + // Assert + Assert.True(combined.IsFailure); + Assert.Equal(error, combined.Error); + } + + [Fact] + public void Combine_SecondResultFails_ReturnsSecondError() + { + // Arrange + var result1 = Result.Success(42); + var error = new ResultError("Second error"); + var result2 = Result.Failure(error); + + // Act + var combined = Result.Combine(result1, result2); + + // Assert + Assert.True(combined.IsFailure); + Assert.Equal(error, combined.Error); + } + + [Fact] + public void Combine_BothResultsFail_ReturnsFirstError() + { + // Arrange + var error1 = new ResultError("First error"); + var error2 = new ResultError("Second error"); + var result1 = Result.Failure(error1); + var result2 = Result.Failure(error2); + + // Act + var combined = Result.Combine(result1, result2); + + // Assert + Assert.True(combined.IsFailure); + Assert.Equal(error1, combined.Error); + } + + [Fact] + public void Combine_ThreeSuccessResults_ReturnsCombinedSuccess() + { + // Arrange + var result1 = Result.Success(42); + var result2 = Result.Success("test"); + var result3 = Result.Success(3.14); + + // Act + var combined = Result.Combine(result1, result2, result3); + + // Assert + Assert.True(combined.IsSuccess); + Assert.Equal((42, "test", 3.14), combined.Value); + } + + [Fact] + public void Combine_ThirdResultFails_ReturnsThirdError() + { + // Arrange + var result1 = Result.Success(42); + var result2 = Result.Success("test"); + var error = new ResultError("Third error"); + var result3 = Result.Failure(error); + + // Act + var combined = Result.Combine(result1, result2, result3); + + // Assert + Assert.True(combined.IsFailure); + Assert.Equal(error, combined.Error); + } + + #endregion + + #region SuccessIf Tests + + [Fact] + public void SuccessIf_WhenConditionIsTrue_ReturnsSuccess() + { + // Act + var result = Result.SuccessIf(true, 42, "Should not see this"); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact] + public void SuccessIf_WhenConditionIsFalse_ReturnsFailure() + { + // Act + var result = Result.SuccessIf(false, 42, "Condition failed"); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Condition failed", result.Error.Message); + } + + [Fact] + public void SuccessIf_WithError_WhenConditionIsTrue_ReturnsSuccess() + { + // Arrange + var error = new ResultError("Should not see this"); + + // Act + var result = Result.SuccessIf(true, 42, error); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact] + public void SuccessIf_WithError_WhenConditionIsFalse_ReturnsFailure() + { + // Arrange + var error = new ResultError("Condition failed", "COND_FAIL"); + + // Act + var result = Result.SuccessIf(false, 42, error); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } + + #endregion + + #region FailureIf Tests + + [Fact] + public void FailureIf_WhenConditionIsTrue_ReturnsFailure() + { + // Act + var result = Result.FailureIf(true, 42, "Condition triggered failure"); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Condition triggered failure", result.Error.Message); + } + + [Fact] + public void FailureIf_WhenConditionIsFalse_ReturnsSuccess() + { + // Act + var result = Result.FailureIf(false, 42, "Should not see this"); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact] + public void FailureIf_WithError_WhenConditionIsTrue_ReturnsFailure() + { + // Arrange + var error = new ResultError("Condition triggered failure", "COND_FAIL"); + + // Act + var result = Result.FailureIf(true, 42, error); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } + + [Fact] + public void FailureIf_WithError_WhenConditionIsFalse_ReturnsSuccess() + { + // Arrange + var error = new ResultError("Should not see this"); + + // Act + var result = Result.FailureIf(false, 42, error); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + #endregion + + #region Real-World Scenario Tests + + [Fact] + public void RealWorldScenario_ValidationChain() + { + // Arrange + string? username = "john_doe"; + + // Act + var result = Result.Success(username) + .Ensure(u => !string.IsNullOrEmpty(u), "Username cannot be empty") + .Ensure(u => u!.Length >= 3, "Username must be at least 3 characters") + .Ensure(u => u!.Length <= 20, "Username must be at most 20 characters") + .Map(u => u!.ToUpperInvariant()); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal("JOHN_DOE", result.Value); + } + + [Fact] + public void RealWorldScenario_ValidationChainFails() + { + // Arrange + string? username = "ab"; + + // Act + var result = Result.Success(username) + .Ensure(u => !string.IsNullOrEmpty(u), "Username cannot be empty") + .Ensure(u => u!.Length >= 3, "Username must be at least 3 characters") + .Ensure(u => u!.Length <= 20, "Username must be at most 20 characters") + .Map(u => u!.ToUpperInvariant()); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Username must be at least 3 characters", result.Error.Message); + } + + [Fact] + public void RealWorldScenario_ChainedOperations() + { + // Arrange + int ParseNumber(string s) => int.Parse(s); + int Double(int n) => n * 2; + + // Act + var result = Result.Try(() => ParseNumber("21")) + .Map(Double) + .Map(n => $"Result: {n}"); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal("Result: 42", result.Value); + } + + [Fact] + public void RealWorldScenario_ChainedOperationsWithFailure() + { + // Arrange + int ParseNumber(string s) => int.Parse(s); + int Double(int n) => n * 2; + + // Act + var result = Result.Try(() => ParseNumber("not a number")) + .Map(Double) + .Map(n => $"Result: {n}"); + + // Assert + Assert.True(result.IsFailure); + Assert.NotNull(result.Error.Exception); + } + + [Fact] + public async Task RealWorldScenario_AsyncOperations() + { + // Arrange + async Task FetchDataAsync() + { + await Task.Delay(1); + return 42; + } + + // Act + var result = await Result.TryAsync(FetchDataAsync); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + #endregion + } +} diff --git a/src/Cortex.Tests/Types/Tests/ResultTests.cs b/src/Cortex.Tests/Types/Tests/ResultTests.cs new file mode 100644 index 0000000..c91543f --- /dev/null +++ b/src/Cortex.Tests/Types/Tests/ResultTests.cs @@ -0,0 +1,672 @@ +using Cortex.Types; + +namespace Cortex.Tests.Types.Tests +{ + public class ResultTests + { + #region Creation Tests + + [Fact] + public void Success_CreatesSuccessfulResult() + { + // Act + var result = Result.Success(42); + + // Assert + Assert.True(result.IsSuccess); + Assert.False(result.IsFailure); + Assert.Equal(42, result.Value); + } + + [Fact] + public void Failure_WithError_CreatesFailedResult() + { + // Arrange + var error = new ResultError("Test error"); + + // Act + var result = Result.Failure(error); + + // Assert + Assert.False(result.IsSuccess); + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } + + [Fact] + public void Failure_WithMessage_CreatesFailedResult() + { + // Act + var result = Result.Failure("Test error"); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Test error", result.Error.Message); + } + + [Fact] + public void Failure_WithException_CreatesFailedResult() + { + // Arrange + var exception = new InvalidOperationException("Test exception"); + + // Act + var result = Result.Failure(exception); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal("Test exception", result.Error.Message); + Assert.Same(exception, result.Error.Exception); + } + + [Fact] + public void Failure_WithNullError_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => Result.Failure((ResultError)null!)); + } + + #endregion + + #region Implicit Conversion Tests + + [Fact] + public void ImplicitConversion_FromValue_CreatesSuccessResult() + { + // Act + Result result = "test value"; + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal("test value", result.Value); + } + + [Fact] + public void ImplicitConversion_FromError_CreatesFailedResult() + { + // Arrange + var error = new ResultError("Test error"); + + // Act + Result result = error; + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(error, result.Error); + } + + #endregion + + #region Value Access Tests + + [Fact] + public void Value_OnSuccess_ReturnsValue() + { + // Arrange + var result = Result.Success(42); + + // Act & Assert + Assert.Equal(42, result.Value); + } + + [Fact] + public void Value_OnFailure_ThrowsInvalidOperationException() + { + // Arrange + var result = Result.Failure("Test error"); + + // Act & Assert + var exception = Assert.Throws(() => result.Value); + Assert.Contains("Cannot access Value", exception.Message); + } + + [Fact] + public void Error_OnFailure_ReturnsError() + { + // Arrange + var error = new ResultError("Test error"); + var result = Result.Failure(error); + + // Act & Assert + Assert.Equal(error, result.Error); + } + + [Fact] + public void Error_OnSuccess_ThrowsInvalidOperationException() + { + // Arrange + var result = Result.Success(42); + + // Act & Assert + var exception = Assert.Throws(() => result.Error); + Assert.Contains("Cannot access Error", exception.Message); + } + + #endregion + + #region TryGet Tests + + [Fact] + public void TryGetValue_OnSuccess_ReturnsTrueAndValue() + { + // Arrange + var result = Result.Success(42); + + // Act + var success = result.TryGetValue(out var value); + + // Assert + Assert.True(success); + Assert.Equal(42, value); + } + + [Fact] + public void TryGetValue_OnFailure_ReturnsFalse() + { + // Arrange + var result = Result.Failure("Test error"); + + // Act + var success = result.TryGetValue(out var value); + + // Assert + Assert.False(success); + Assert.Equal(default, value); + } + + [Fact] + public void TryGetError_OnFailure_ReturnsTrueAndError() + { + // Arrange + var error = new ResultError("Test error"); + var result = Result.Failure(error); + + // Act + var hasError = result.TryGetError(out var retrievedError); + + // Assert + Assert.True(hasError); + Assert.Equal(error, retrievedError); + } + + [Fact] + public void TryGetError_OnSuccess_ReturnsFalse() + { + // Arrange + var result = Result.Success(42); + + // Act + var hasError = result.TryGetError(out var error); + + // Assert + Assert.False(hasError); + Assert.Null(error); + } + + #endregion + + #region GetValueOrDefault Tests + + [Fact] + public void GetValueOrDefault_OnSuccess_ReturnsValue() + { + // Arrange + var result = Result.Success(42); + + // Act + var value = result.GetValueOrDefault(0); + + // Assert + Assert.Equal(42, value); + } + + [Fact] + public void GetValueOrDefault_OnFailure_ReturnsDefault() + { + // Arrange + var result = Result.Failure("Test error"); + + // Act + var value = result.GetValueOrDefault(99); + + // Assert + Assert.Equal(99, value); + } + + [Fact] + public void GetValueOrDefault_WithFactory_OnSuccess_ReturnsValue() + { + // Arrange + var result = Result.Success(42); + var factoryCalled = false; + + // Act + var value = result.GetValueOrDefault(() => { factoryCalled = true; return 99; }); + + // Assert + Assert.Equal(42, value); + Assert.False(factoryCalled); + } + + [Fact] + public void GetValueOrDefault_WithFactory_OnFailure_CallsFactory() + { + // Arrange + var result = Result.Failure("Test error"); + + // Act + var value = result.GetValueOrDefault(() => 99); + + // Assert + Assert.Equal(99, value); + } + + [Fact] + public void GetValueOrDefault_WithErrorHandler_OnFailure_PassesError() + { + // Arrange + var error = new ResultError("Test error"); + var result = Result.Failure(error); + ResultError? capturedError = null; + + // Act + var value = result.GetValueOrDefault(e => { capturedError = e; return "default"; }); + + // Assert + Assert.Equal("default", value); + Assert.Equal(error, capturedError); + } + + #endregion + + #region Match Tests + + [Fact] + public void Match_OnSuccess_ExecutesSuccessHandler() + { + // Arrange + var result = Result.Success(42); + + // Act + var output = result.Match( + onSuccess: v => $"Success: {v}", + onFailure: e => $"Failure: {e.Message}"); + + // Assert + Assert.Equal("Success: 42", output); + } + + [Fact] + public void Match_OnFailure_ExecutesFailureHandler() + { + // Arrange + var result = Result.Failure("Test error"); + + // Act + var output = result.Match( + onSuccess: v => $"Success: {v}", + onFailure: e => $"Failure: {e.Message}"); + + // Assert + Assert.Equal("Failure: Test error", output); + } + + #endregion + + #region Switch Tests + + [Fact] + public void Switch_OnSuccess_ExecutesSuccessAction() + { + // Arrange + var result = Result.Success(42); + int? capturedValue = null; + ResultError? capturedError = null; + + // Act + result.Switch( + onSuccess: v => capturedValue = v, + onFailure: e => capturedError = e); + + // Assert + Assert.Equal(42, capturedValue); + Assert.Null(capturedError); + } + + [Fact] + public void Switch_OnFailure_ExecutesFailureAction() + { + // Arrange + var error = new ResultError("Test error"); + var result = Result.Failure(error); + int? capturedValue = null; + ResultError? capturedError = null; + + // Act + result.Switch( + onSuccess: v => capturedValue = v, + onFailure: e => capturedError = e); + + // Assert + Assert.Null(capturedValue); + Assert.Equal(error, capturedError); + } + + #endregion + + #region Map Tests + + [Fact] + public void Map_OnSuccess_TransformsValue() + { + // Arrange + var result = Result.Success(42); + + // Act + var mapped = result.Map(v => v.ToString()); + + // Assert + Assert.True(mapped.IsSuccess); + Assert.Equal("42", mapped.Value); + } + + [Fact] + public void Map_OnFailure_PreservesError() + { + // Arrange + var error = new ResultError("Test error"); + var result = Result.Failure(error); + + // Act + var mapped = result.Map(v => v.ToString()); + + // Assert + Assert.True(mapped.IsFailure); + Assert.Equal(error, mapped.Error); + } + + [Fact] + public void MapError_OnFailure_TransformsError() + { + // Arrange + var result = Result.Failure("Original error"); + + // Act + var mapped = result.MapError(e => new ResultError($"Mapped: {e.Message}")); + + // Assert + Assert.True(mapped.IsFailure); + Assert.Equal("Mapped: Original error", mapped.Error.Message); + } + + [Fact] + public void MapError_OnSuccess_PreservesValue() + { + // Arrange + var result = Result.Success(42); + + // Act + var mapped = result.MapError(e => new ResultError("Should not happen")); + + // Assert + Assert.True(mapped.IsSuccess); + Assert.Equal(42, mapped.Value); + } + + #endregion + + #region Bind Tests + + [Fact] + public void Bind_OnSuccess_ChainsOperation() + { + // Arrange + var result = Result.Success(42); + + // Act + var bound = result.Bind(v => Result.Success($"Value: {v}")); + + // Assert + Assert.True(bound.IsSuccess); + Assert.Equal("Value: 42", bound.Value); + } + + [Fact] + public void Bind_OnSuccess_CanReturnFailure() + { + // Arrange + var result = Result.Success(42); + + // Act + var bound = result.Bind(v => Result.Failure("Validation failed")); + + // Assert + Assert.True(bound.IsFailure); + Assert.Equal("Validation failed", bound.Error.Message); + } + + [Fact] + public void Bind_OnFailure_SkipsOperation() + { + // Arrange + var error = new ResultError("Original error"); + var result = Result.Failure(error); + var operationCalled = false; + + // Act + var bound = result.Bind(v => { operationCalled = true; return Result.Success("test"); }); + + // Assert + Assert.True(bound.IsFailure); + Assert.Equal(error, bound.Error); + Assert.False(operationCalled); + } + + #endregion + + #region Tap Tests + + [Fact] + public void Tap_OnSuccess_ExecutesAction() + { + // Arrange + var result = Result.Success(42); + int? capturedValue = null; + + // Act + var tapped = result.Tap(v => capturedValue = v); + + // Assert + Assert.Equal(42, capturedValue); + Assert.Equal(result, tapped); + } + + [Fact] + public void Tap_OnFailure_SkipsAction() + { + // Arrange + var result = Result.Failure("Test error"); + var actionCalled = false; + + // Act + var tapped = result.Tap(v => actionCalled = true); + + // Assert + Assert.False(actionCalled); + Assert.Equal(result, tapped); + } + + [Fact] + public void TapError_OnFailure_ExecutesAction() + { + // Arrange + var error = new ResultError("Test error"); + var result = Result.Failure(error); + ResultError? capturedError = null; + + // Act + var tapped = result.TapError(e => capturedError = e); + + // Assert + Assert.Equal(error, capturedError); + Assert.Equal(result, tapped); + } + + [Fact] + public void TapError_OnSuccess_SkipsAction() + { + // Arrange + var result = Result.Success(42); + var actionCalled = false; + + // Act + var tapped = result.TapError(e => actionCalled = true); + + // Assert + Assert.False(actionCalled); + Assert.Equal(result, tapped); + } + + #endregion + + #region Ensure Tests + + [Fact] + public void Ensure_WhenPredicatePasses_ReturnsOriginalResult() + { + // Arrange + var result = Result.Success(42); + + // Act + var ensured = result.Ensure(v => v > 0, "Value must be positive"); + + // Assert + Assert.True(ensured.IsSuccess); + Assert.Equal(42, ensured.Value); + } + + [Fact] + public void Ensure_WhenPredicateFails_ReturnsFailure() + { + // Arrange + var result = Result.Success(-5); + + // Act + var ensured = result.Ensure(v => v > 0, "Value must be positive"); + + // Assert + Assert.True(ensured.IsFailure); + Assert.Equal("Value must be positive", ensured.Error.Message); + } + + [Fact] + public void Ensure_OnFailure_SkipsPredicate() + { + // Arrange + var result = Result.Failure("Original error"); + var predicateCalled = false; + + // Act + var ensured = result.Ensure(v => { predicateCalled = true; return v > 0; }, "Should not see this"); + + // Assert + Assert.True(ensured.IsFailure); + Assert.Equal("Original error", ensured.Error.Message); + Assert.False(predicateCalled); + } + + #endregion + + #region Equality Tests + + [Fact] + public void Equals_SuccessResultsWithSameValue_ReturnsTrue() + { + // Arrange + var result1 = Result.Success(42); + var result2 = Result.Success(42); + + // Act & Assert + Assert.Equal(result1, result2); + Assert.True(result1 == result2); + Assert.False(result1 != result2); + } + + [Fact] + public void Equals_SuccessResultsWithDifferentValues_ReturnsFalse() + { + // Arrange + var result1 = Result.Success(42); + var result2 = Result.Success(99); + + // Act & Assert + Assert.NotEqual(result1, result2); + } + + [Fact] + public void Equals_FailureResultsWithSameError_ReturnsTrue() + { + // Arrange + var result1 = Result.Failure("Test error"); + var result2 = Result.Failure("Test error"); + + // Act & Assert + Assert.Equal(result1, result2); + } + + [Fact] + public void Equals_SuccessAndFailure_ReturnsFalse() + { + // Arrange + var success = Result.Success(42); + var failure = Result.Failure("Test error"); + + // Act & Assert + Assert.NotEqual(success, failure); + } + + [Fact] + public void GetHashCode_SameResults_ReturnsSameHashCode() + { + // Arrange + var result1 = Result.Success(42); + var result2 = Result.Success(42); + + // Act & Assert + Assert.Equal(result1.GetHashCode(), result2.GetHashCode()); + } + + #endregion + + #region ToString Tests + + [Fact] + public void ToString_OnSuccess_ReturnsFormattedString() + { + // Arrange + var result = Result.Success(42); + + // Act + var str = result.ToString(); + + // Assert + Assert.Equal("Success(42)", str); + } + + [Fact] + public void ToString_OnFailure_ReturnsFormattedString() + { + // Arrange + var result = Result.Failure("Test error"); + + // Act + var str = result.ToString(); + + // Assert + Assert.Contains("Failure", str); + Assert.Contains("Test error", str); + } + + #endregion + } +} diff --git a/src/Cortex.Types/AnyOf/AnyOf5.cs b/src/Cortex.Types/AnyOf/AnyOf5.cs new file mode 100644 index 0000000..df4765b --- /dev/null +++ b/src/Cortex.Types/AnyOf/AnyOf5.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be any of five specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Fourth possible type + /// Fifth possible type + public readonly struct AnyOf : IEquatable>, IAnyOf + { + private readonly object _value; + private readonly HashSet _typeIndices; + + /// + public object Value => _value; + + /// + public IEnumerable TypeIndices => _typeIndices; + + private AnyOf(object value, HashSet typeIndices) => + (_value, _typeIndices) = (value, typeIndices); + + public static implicit operator AnyOf(T1 value) => new(value, new HashSet { 0 }); + public static implicit operator AnyOf(T2 value) => new(value, new HashSet { 1 }); + public static implicit operator AnyOf(T3 value) => new(value, new HashSet { 2 }); + public static implicit operator AnyOf(T4 value) => new(value, new HashSet { 3 }); + public static implicit operator AnyOf(T5 value) => new(value, new HashSet { 4 }); + + /// + /// Checks if the contained value is of or derived from type + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler, + Func t5Handler) + { + if (_typeIndices.Contains(0) && _value is T1 t1) return t1Handler(t1); + if (_typeIndices.Contains(1) && _value is T2 t2) return t2Handler(t2); + if (_typeIndices.Contains(2) && _value is T3 t3) return t3Handler(t3); + if (_typeIndices.Contains(3) && _value is T4 t4) return t4Handler(t4); + if (_typeIndices.Contains(4) && _value is T5 t5) return t5Handler(t5); + throw new InvalidOperationException("Invalid state"); + } + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action, + Action t5Action) + { + if (_typeIndices.Contains(0) && _value is T1 t1) { t1Action(t1); return; } + if (_typeIndices.Contains(1) && _value is T2 t2) { t2Action(t2); return; } + if (_typeIndices.Contains(2) && _value is T3 t3) { t3Action(t3); return; } + if (_typeIndices.Contains(3) && _value is T4 t4) { t4Action(t4); return; } + if (_typeIndices.Contains(4) && _value is T5 t5) { t5Action(t5); return; } + throw new InvalidOperationException("Invalid state"); + } + + /// + /// Returns all of the type parameters for which the stored value is assignable. + /// + public IEnumerable GetMatchingTypes() + { + if (_value is T1) yield return typeof(T1); + if (_value is T2) yield return typeof(T2); + if (_value is T3) yield return typeof(T3); + if (_value is T4) yield return typeof(T4); + if (_value is T5) yield return typeof(T5); + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(AnyOf other) => + _typeIndices.SetEquals(other._typeIndices) && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is AnyOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndices); + + public static bool operator ==(AnyOf left, AnyOf right) => + left.Equals(right); + + public static bool operator !=(AnyOf left, AnyOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/AnyOf/AnyOf6.cs b/src/Cortex.Types/AnyOf/AnyOf6.cs new file mode 100644 index 0000000..fb5fe16 --- /dev/null +++ b/src/Cortex.Types/AnyOf/AnyOf6.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be any of six specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Fourth possible type + /// Fifth possible type + /// Sixth possible type + public readonly struct AnyOf : IEquatable>, IAnyOf + { + private readonly object _value; + private readonly HashSet _typeIndices; + + /// + public object Value => _value; + + /// + public IEnumerable TypeIndices => _typeIndices; + + private AnyOf(object value, HashSet typeIndices) => + (_value, _typeIndices) = (value, typeIndices); + + public static implicit operator AnyOf(T1 value) => new(value, new HashSet { 0 }); + public static implicit operator AnyOf(T2 value) => new(value, new HashSet { 1 }); + public static implicit operator AnyOf(T3 value) => new(value, new HashSet { 2 }); + public static implicit operator AnyOf(T4 value) => new(value, new HashSet { 3 }); + public static implicit operator AnyOf(T5 value) => new(value, new HashSet { 4 }); + public static implicit operator AnyOf(T6 value) => new(value, new HashSet { 5 }); + + /// + /// Checks if the contained value is of or derived from type + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler, + Func t5Handler, + Func t6Handler) + { + if (_typeIndices.Contains(0) && _value is T1 t1) return t1Handler(t1); + if (_typeIndices.Contains(1) && _value is T2 t2) return t2Handler(t2); + if (_typeIndices.Contains(2) && _value is T3 t3) return t3Handler(t3); + if (_typeIndices.Contains(3) && _value is T4 t4) return t4Handler(t4); + if (_typeIndices.Contains(4) && _value is T5 t5) return t5Handler(t5); + if (_typeIndices.Contains(5) && _value is T6 t6) return t6Handler(t6); + throw new InvalidOperationException("Invalid state"); + } + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action, + Action t5Action, + Action t6Action) + { + if (_typeIndices.Contains(0) && _value is T1 t1) { t1Action(t1); return; } + if (_typeIndices.Contains(1) && _value is T2 t2) { t2Action(t2); return; } + if (_typeIndices.Contains(2) && _value is T3 t3) { t3Action(t3); return; } + if (_typeIndices.Contains(3) && _value is T4 t4) { t4Action(t4); return; } + if (_typeIndices.Contains(4) && _value is T5 t5) { t5Action(t5); return; } + if (_typeIndices.Contains(5) && _value is T6 t6) { t6Action(t6); return; } + throw new InvalidOperationException("Invalid state"); + } + + /// + /// Returns all of the type parameters for which the stored value is assignable. + /// + public IEnumerable GetMatchingTypes() + { + if (_value is T1) yield return typeof(T1); + if (_value is T2) yield return typeof(T2); + if (_value is T3) yield return typeof(T3); + if (_value is T4) yield return typeof(T4); + if (_value is T5) yield return typeof(T5); + if (_value is T6) yield return typeof(T6); + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(AnyOf other) => + _typeIndices.SetEquals(other._typeIndices) && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is AnyOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndices); + + public static bool operator ==(AnyOf left, AnyOf right) => + left.Equals(right); + + public static bool operator !=(AnyOf left, AnyOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/AnyOf/AnyOf7.cs b/src/Cortex.Types/AnyOf/AnyOf7.cs new file mode 100644 index 0000000..5298aa3 --- /dev/null +++ b/src/Cortex.Types/AnyOf/AnyOf7.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be any of seven specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Fourth possible type + /// Fifth possible type + /// Sixth possible type + /// Seventh possible type + public readonly struct AnyOf : IEquatable>, IAnyOf + { + private readonly object _value; + private readonly HashSet _typeIndices; + + /// + public object Value => _value; + + /// + public IEnumerable TypeIndices => _typeIndices; + + private AnyOf(object value, HashSet typeIndices) => + (_value, _typeIndices) = (value, typeIndices); + + public static implicit operator AnyOf(T1 value) => new(value, new HashSet { 0 }); + public static implicit operator AnyOf(T2 value) => new(value, new HashSet { 1 }); + public static implicit operator AnyOf(T3 value) => new(value, new HashSet { 2 }); + public static implicit operator AnyOf(T4 value) => new(value, new HashSet { 3 }); + public static implicit operator AnyOf(T5 value) => new(value, new HashSet { 4 }); + public static implicit operator AnyOf(T6 value) => new(value, new HashSet { 5 }); + public static implicit operator AnyOf(T7 value) => new(value, new HashSet { 6 }); + + /// + /// Checks if the contained value is of or derived from type + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler, + Func t5Handler, + Func t6Handler, + Func t7Handler) + { + if (_typeIndices.Contains(0) && _value is T1 t1) return t1Handler(t1); + if (_typeIndices.Contains(1) && _value is T2 t2) return t2Handler(t2); + if (_typeIndices.Contains(2) && _value is T3 t3) return t3Handler(t3); + if (_typeIndices.Contains(3) && _value is T4 t4) return t4Handler(t4); + if (_typeIndices.Contains(4) && _value is T5 t5) return t5Handler(t5); + if (_typeIndices.Contains(5) && _value is T6 t6) return t6Handler(t6); + if (_typeIndices.Contains(6) && _value is T7 t7) return t7Handler(t7); + throw new InvalidOperationException("Invalid state"); + } + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action, + Action t5Action, + Action t6Action, + Action t7Action) + { + if (_typeIndices.Contains(0) && _value is T1 t1) { t1Action(t1); return; } + if (_typeIndices.Contains(1) && _value is T2 t2) { t2Action(t2); return; } + if (_typeIndices.Contains(2) && _value is T3 t3) { t3Action(t3); return; } + if (_typeIndices.Contains(3) && _value is T4 t4) { t4Action(t4); return; } + if (_typeIndices.Contains(4) && _value is T5 t5) { t5Action(t5); return; } + if (_typeIndices.Contains(5) && _value is T6 t6) { t6Action(t6); return; } + if (_typeIndices.Contains(6) && _value is T7 t7) { t7Action(t7); return; } + throw new InvalidOperationException("Invalid state"); + } + + /// + /// Returns all of the type parameters for which the stored value is assignable. + /// + public IEnumerable GetMatchingTypes() + { + if (_value is T1) yield return typeof(T1); + if (_value is T2) yield return typeof(T2); + if (_value is T3) yield return typeof(T3); + if (_value is T4) yield return typeof(T4); + if (_value is T5) yield return typeof(T5); + if (_value is T6) yield return typeof(T6); + if (_value is T7) yield return typeof(T7); + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(AnyOf other) => + _typeIndices.SetEquals(other._typeIndices) && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is AnyOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndices); + + public static bool operator ==(AnyOf left, AnyOf right) => + left.Equals(right); + + public static bool operator !=(AnyOf left, AnyOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/AnyOf/AnyOf8.cs b/src/Cortex.Types/AnyOf/AnyOf8.cs new file mode 100644 index 0000000..400c62a --- /dev/null +++ b/src/Cortex.Types/AnyOf/AnyOf8.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be any of eight specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Fourth possible type + /// Fifth possible type + /// Sixth possible type + /// Seventh possible type + /// Eighth possible type + public readonly struct AnyOf : IEquatable>, IAnyOf + { + private readonly object _value; + private readonly HashSet _typeIndices; + + /// + public object Value => _value; + + /// + public IEnumerable TypeIndices => _typeIndices; + + private AnyOf(object value, HashSet typeIndices) => + (_value, _typeIndices) = (value, typeIndices); + + public static implicit operator AnyOf(T1 value) => new(value, new HashSet { 0 }); + public static implicit operator AnyOf(T2 value) => new(value, new HashSet { 1 }); + public static implicit operator AnyOf(T3 value) => new(value, new HashSet { 2 }); + public static implicit operator AnyOf(T4 value) => new(value, new HashSet { 3 }); + public static implicit operator AnyOf(T5 value) => new(value, new HashSet { 4 }); + public static implicit operator AnyOf(T6 value) => new(value, new HashSet { 5 }); + public static implicit operator AnyOf(T7 value) => new(value, new HashSet { 6 }); + public static implicit operator AnyOf(T8 value) => new(value, new HashSet { 7 }); + + /// + /// Checks if the contained value is of or derived from type + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler, + Func t5Handler, + Func t6Handler, + Func t7Handler, + Func t8Handler) + { + if (_typeIndices.Contains(0) && _value is T1 t1) return t1Handler(t1); + if (_typeIndices.Contains(1) && _value is T2 t2) return t2Handler(t2); + if (_typeIndices.Contains(2) && _value is T3 t3) return t3Handler(t3); + if (_typeIndices.Contains(3) && _value is T4 t4) return t4Handler(t4); + if (_typeIndices.Contains(4) && _value is T5 t5) return t5Handler(t5); + if (_typeIndices.Contains(5) && _value is T6 t6) return t6Handler(t6); + if (_typeIndices.Contains(6) && _value is T7 t7) return t7Handler(t7); + if (_typeIndices.Contains(7) && _value is T8 t8) return t8Handler(t8); + throw new InvalidOperationException("Invalid state"); + } + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action, + Action t5Action, + Action t6Action, + Action t7Action, + Action t8Action) + { + if (_typeIndices.Contains(0) && _value is T1 t1) { t1Action(t1); return; } + if (_typeIndices.Contains(1) && _value is T2 t2) { t2Action(t2); return; } + if (_typeIndices.Contains(2) && _value is T3 t3) { t3Action(t3); return; } + if (_typeIndices.Contains(3) && _value is T4 t4) { t4Action(t4); return; } + if (_typeIndices.Contains(4) && _value is T5 t5) { t5Action(t5); return; } + if (_typeIndices.Contains(5) && _value is T6 t6) { t6Action(t6); return; } + if (_typeIndices.Contains(6) && _value is T7 t7) { t7Action(t7); return; } + if (_typeIndices.Contains(7) && _value is T8 t8) { t8Action(t8); return; } + throw new InvalidOperationException("Invalid state"); + } + + /// + /// Returns all of the type parameters for which the stored value is assignable. + /// + public IEnumerable GetMatchingTypes() + { + if (_value is T1) yield return typeof(T1); + if (_value is T2) yield return typeof(T2); + if (_value is T3) yield return typeof(T3); + if (_value is T4) yield return typeof(T4); + if (_value is T5) yield return typeof(T5); + if (_value is T6) yield return typeof(T6); + if (_value is T7) yield return typeof(T7); + if (_value is T8) yield return typeof(T8); + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(AnyOf other) => + _typeIndices.SetEquals(other._typeIndices) && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is AnyOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndices); + + public static bool operator ==(AnyOf left, AnyOf right) => + left.Equals(right); + + public static bool operator !=(AnyOf left, AnyOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/OneOf/OneOf3.cs b/src/Cortex.Types/OneOf/OneOf3.cs index 16d67c3..1983de9 100644 --- a/src/Cortex.Types/OneOf/OneOf3.cs +++ b/src/Cortex.Types/OneOf/OneOf3.cs @@ -129,7 +129,7 @@ public bool Equals(OneOf other) => Equals(_value, other._value); public override bool Equals(object obj) => - obj is OneOf other && Equals(other); + obj is OneOf other && Equals(other); public override int GetHashCode() => HashCode.Combine(_value, _typeIndex); diff --git a/src/Cortex.Types/OneOf/OneOf4.cs b/src/Cortex.Types/OneOf/OneOf4.cs index e40c4b5..ad7aa3e 100644 --- a/src/Cortex.Types/OneOf/OneOf4.cs +++ b/src/Cortex.Types/OneOf/OneOf4.cs @@ -136,7 +136,7 @@ public bool Equals(OneOf other) => Equals(_value, other._value); public override bool Equals(object obj) => - obj is OneOf other && Equals(other); + obj is OneOf other && Equals(other); public override int GetHashCode() => HashCode.Combine(_value, _typeIndex); diff --git a/src/Cortex.Types/OneOf/OneOf5.cs b/src/Cortex.Types/OneOf/OneOf5.cs new file mode 100644 index 0000000..25fc9ac --- /dev/null +++ b/src/Cortex.Types/OneOf/OneOf5.cs @@ -0,0 +1,125 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be one of five specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Fourth possible type + /// Fifth possible type + public readonly struct OneOf : IEquatable>, IOneOf + { + private readonly object _value; + private readonly int _typeIndex; + + /// + public object Value => _value; + + /// + public int TypeIndex => _typeIndex; + + private OneOf(object value, int typeIndex) => + (_value, _typeIndex) = (value, typeIndex); + + public static implicit operator OneOf(T1 value) => new(value, 0); + public static implicit operator OneOf(T2 value) => new(value, 1); + public static implicit operator OneOf(T3 value) => new(value, 2); + public static implicit operator OneOf(T4 value) => new(value, 3); + public static implicit operator OneOf(T5 value) => new(value, 4); + + /// + /// Checks if the contained value is of or derived from type + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler, + Func t5Handler) => _typeIndex switch + { + 0 => t1Handler((T1)_value), + 1 => t2Handler((T2)_value), + 2 => t3Handler((T3)_value), + 3 => t4Handler((T4)_value), + 4 => t5Handler((T5)_value), + _ => throw new InvalidOperationException("Invalid state") + }; + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action, + Action t5Action) + { + switch (_typeIndex) + { + case 0: t1Action((T1)_value); break; + case 1: t2Action((T2)_value); break; + case 2: t3Action((T3)_value); break; + case 3: t4Action((T4)_value); break; + case 4: t5Action((T5)_value); break; + default: throw new InvalidOperationException("Invalid state"); + } + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(OneOf other) => + _typeIndex == other._typeIndex && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is OneOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndex); + + public static bool operator ==(OneOf left, OneOf right) => + left.Equals(right); + + public static bool operator !=(OneOf left, OneOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/OneOf/OneOf6.cs b/src/Cortex.Types/OneOf/OneOf6.cs new file mode 100644 index 0000000..b1dabaa --- /dev/null +++ b/src/Cortex.Types/OneOf/OneOf6.cs @@ -0,0 +1,131 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be one of six specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Fourth possible type + /// Fifth possible type + /// Sixth possible type + public readonly struct OneOf : IEquatable>, IOneOf + { + private readonly object _value; + private readonly int _typeIndex; + + /// + public object Value => _value; + + /// + public int TypeIndex => _typeIndex; + + private OneOf(object value, int typeIndex) => + (_value, _typeIndex) = (value, typeIndex); + + public static implicit operator OneOf(T1 value) => new(value, 0); + public static implicit operator OneOf(T2 value) => new(value, 1); + public static implicit operator OneOf(T3 value) => new(value, 2); + public static implicit operator OneOf(T4 value) => new(value, 3); + public static implicit operator OneOf(T5 value) => new(value, 4); + public static implicit operator OneOf(T6 value) => new(value, 5); + + /// + /// Checks if the contained value is of or derived from type + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler, + Func t5Handler, + Func t6Handler) => _typeIndex switch + { + 0 => t1Handler((T1)_value), + 1 => t2Handler((T2)_value), + 2 => t3Handler((T3)_value), + 3 => t4Handler((T4)_value), + 4 => t5Handler((T5)_value), + 5 => t6Handler((T6)_value), + _ => throw new InvalidOperationException("Invalid state") + }; + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action, + Action t5Action, + Action t6Action) + { + switch (_typeIndex) + { + case 0: t1Action((T1)_value); break; + case 1: t2Action((T2)_value); break; + case 2: t3Action((T3)_value); break; + case 3: t4Action((T4)_value); break; + case 4: t5Action((T5)_value); break; + case 5: t6Action((T6)_value); break; + default: throw new InvalidOperationException("Invalid state"); + } + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(OneOf other) => + _typeIndex == other._typeIndex && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is OneOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndex); + + public static bool operator ==(OneOf left, OneOf right) => + left.Equals(right); + + public static bool operator !=(OneOf left, OneOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/OneOf/OneOf7.cs b/src/Cortex.Types/OneOf/OneOf7.cs new file mode 100644 index 0000000..99981f7 --- /dev/null +++ b/src/Cortex.Types/OneOf/OneOf7.cs @@ -0,0 +1,137 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be one of seven specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Fourth possible type + /// Fifth possible type + /// Sixth possible type + /// Seventh possible type + public readonly struct OneOf : IEquatable>, IOneOf + { + private readonly object _value; + private readonly int _typeIndex; + + /// + public object Value => _value; + + /// + public int TypeIndex => _typeIndex; + + private OneOf(object value, int typeIndex) => + (_value, _typeIndex) = (value, typeIndex); + + public static implicit operator OneOf(T1 value) => new(value, 0); + public static implicit operator OneOf(T2 value) => new(value, 1); + public static implicit operator OneOf(T3 value) => new(value, 2); + public static implicit operator OneOf(T4 value) => new(value, 3); + public static implicit operator OneOf(T5 value) => new(value, 4); + public static implicit operator OneOf(T6 value) => new(value, 5); + public static implicit operator OneOf(T7 value) => new(value, 6); + + /// + /// Checks if the contained value is of or derived from type + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler, + Func t5Handler, + Func t6Handler, + Func t7Handler) => _typeIndex switch + { + 0 => t1Handler((T1)_value), + 1 => t2Handler((T2)_value), + 2 => t3Handler((T3)_value), + 3 => t4Handler((T4)_value), + 4 => t5Handler((T5)_value), + 5 => t6Handler((T6)_value), + 6 => t7Handler((T7)_value), + _ => throw new InvalidOperationException("Invalid state") + }; + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action, + Action t5Action, + Action t6Action, + Action t7Action) + { + switch (_typeIndex) + { + case 0: t1Action((T1)_value); break; + case 1: t2Action((T2)_value); break; + case 2: t3Action((T3)_value); break; + case 3: t4Action((T4)_value); break; + case 4: t5Action((T5)_value); break; + case 5: t6Action((T6)_value); break; + case 6: t7Action((T7)_value); break; + default: throw new InvalidOperationException("Invalid state"); + } + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(OneOf other) => + _typeIndex == other._typeIndex && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is OneOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndex); + + public static bool operator ==(OneOf left, OneOf right) => + left.Equals(right); + + public static bool operator !=(OneOf left, OneOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/OneOf/OneOf8.cs b/src/Cortex.Types/OneOf/OneOf8.cs new file mode 100644 index 0000000..74d741e --- /dev/null +++ b/src/Cortex.Types/OneOf/OneOf8.cs @@ -0,0 +1,143 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents a value that can be one of eight specified types + /// + /// First possible type + /// Second possible type + /// Third possible type + /// Fourth possible type + /// Fifth possible type + /// Sixth possible type + /// Seventh possible type + /// Eighth possible type + public readonly struct OneOf : IEquatable>, IOneOf + { + private readonly object _value; + private readonly int _typeIndex; + + /// + public object Value => _value; + + /// + public int TypeIndex => _typeIndex; + + private OneOf(object value, int typeIndex) => + (_value, _typeIndex) = (value, typeIndex); + + public static implicit operator OneOf(T1 value) => new(value, 0); + public static implicit operator OneOf(T2 value) => new(value, 1); + public static implicit operator OneOf(T3 value) => new(value, 2); + public static implicit operator OneOf(T4 value) => new(value, 3); + public static implicit operator OneOf(T5 value) => new(value, 4); + public static implicit operator OneOf(T6 value) => new(value, 5); + public static implicit operator OneOf(T7 value) => new(value, 6); + public static implicit operator OneOf(T8 value) => new(value, 7); + + /// + /// Checks if the contained value is of or derived from type + /// + public bool Is() => _value is T; + + /// + /// Returns the contained value as + /// + /// + /// Thrown when value is not compatible with + /// + public T As() => _value is T val + ? val + : throw new InvalidCastException(GetCastErrorMessage(typeof(T))); + + /// + /// Attempts to retrieve the value as + /// + public bool TryGet([NotNullWhen(true)] out T result) + { + if (_value is T val) + { + result = val; + return true; + } + + result = default!; + return false; + } + + /// + /// Type-safe pattern matching with exhaustive case handling + /// + public TResult Match( + Func t1Handler, + Func t2Handler, + Func t3Handler, + Func t4Handler, + Func t5Handler, + Func t6Handler, + Func t7Handler, + Func t8Handler) => _typeIndex switch + { + 0 => t1Handler((T1)_value), + 1 => t2Handler((T2)_value), + 2 => t3Handler((T3)_value), + 3 => t4Handler((T4)_value), + 4 => t5Handler((T5)_value), + 5 => t6Handler((T6)_value), + 6 => t7Handler((T7)_value), + 7 => t8Handler((T8)_value), + _ => throw new InvalidOperationException("Invalid state") + }; + + /// + /// Executes type-specific action with exhaustive case handling + /// + public void Switch( + Action t1Action, + Action t2Action, + Action t3Action, + Action t4Action, + Action t5Action, + Action t6Action, + Action t7Action, + Action t8Action) + { + switch (_typeIndex) + { + case 0: t1Action((T1)_value); break; + case 1: t2Action((T2)_value); break; + case 2: t3Action((T3)_value); break; + case 3: t4Action((T4)_value); break; + case 4: t5Action((T5)_value); break; + case 5: t6Action((T6)_value); break; + case 6: t7Action((T7)_value); break; + case 7: t8Action((T8)_value); break; + default: throw new InvalidOperationException("Invalid state"); + } + } + + private string GetCastErrorMessage(Type targetType) => + $"Cannot cast stored type {_value?.GetType().Name ?? "null"} to {targetType.Name}"; + + public bool Equals(OneOf other) => + _typeIndex == other._typeIndex && + Equals(_value, other._value); + + public override bool Equals(object obj) => + obj is OneOf other && Equals(other); + + public override int GetHashCode() => + HashCode.Combine(_value, _typeIndex); + + public static bool operator ==(OneOf left, OneOf right) => + left.Equals(right); + + public static bool operator !=(OneOf left, OneOf right) => + !left.Equals(right); + + public override string ToString() => + _value?.ToString() ?? string.Empty; + } +} diff --git a/src/Cortex.Types/Result/IResult.cs b/src/Cortex.Types/Result/IResult.cs new file mode 100644 index 0000000..bb2858b --- /dev/null +++ b/src/Cortex.Types/Result/IResult.cs @@ -0,0 +1,43 @@ +namespace Cortex.Types +{ + /// + /// Base interface for all Result types providing common functionality + /// + public interface IResult + { + /// + /// Gets whether the result represents a successful operation + /// + bool IsSuccess { get; } + + /// + /// Gets whether the result represents a failed operation + /// + bool IsFailure { get; } + } + + /// + /// Interface for Result types that carry a value + /// + /// Type of the success value + public interface IResult : IResult + { + /// + /// Gets the success value. Throws if the result is a failure. + /// + TValue Value { get; } + } + + /// + /// Interface for Result types that carry both value and error + /// + /// Type of the success value + /// Type of the error value + public interface IResult : IResult + { + /// + /// Gets the error value. Throws if the result is a success. + /// + TError Error { get; } + } +} diff --git a/src/Cortex.Types/Result/Result.cs b/src/Cortex.Types/Result/Result.cs new file mode 100644 index 0000000..b51acf1 --- /dev/null +++ b/src/Cortex.Types/Result/Result.cs @@ -0,0 +1,286 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents the result of an operation that can succeed with a value or fail with a built-in error + /// + /// Type of the success value + public readonly struct Result : IEquatable>, IResult + { + private readonly T _value; + private readonly ResultError _error; + private readonly bool _isSuccess; + + /// + public bool IsSuccess => _isSuccess; + + /// + public bool IsFailure => !_isSuccess; + + /// + /// Thrown when accessing Value on a failed result + public T Value => _isSuccess + ? _value + : throw new InvalidOperationException( + $"Cannot access Value on a failed Result. Error: {_error}"); + + /// + /// Thrown when accessing Error on a successful result + public ResultError Error => !_isSuccess + ? _error + : throw new InvalidOperationException( + "Cannot access Error on a successful Result"); + + private Result(T value, ResultError error, bool isSuccess) + { + _value = value; + _error = error; + _isSuccess = isSuccess; + } + + /// + /// Creates a successful result with the specified value + /// + /// The success value + /// A successful Result + public static Result Success(T value) => + new(value, default, true); + + /// + /// Creates a failed result with the specified error + /// + /// The error + /// A failed Result + public static Result Failure(ResultError error) => + new(default, error ?? throw new ArgumentNullException(nameof(error)), false); + + /// + /// Creates a failed result with the specified error message + /// + /// The error message + /// A failed Result + public static Result Failure(string errorMessage) => + new(default, new ResultError(errorMessage), false); + + /// + /// Creates a failed result from an exception + /// + /// The exception + /// A failed Result + public static Result Failure(Exception exception) => + new(default, ResultError.FromException(exception), false); + + /// + /// Implicit conversion from value to successful Result + /// + public static implicit operator Result(T value) => Success(value); + + /// + /// Implicit conversion from ResultError to failed Result + /// + public static implicit operator Result(ResultError error) => Failure(error); + + /// + /// Attempts to get the success value + /// + /// The success value if successful + /// True if successful, false otherwise +#if !NETSTANDARD2_0 + public bool TryGetValue([NotNullWhen(true)] out T value) +#else + public bool TryGetValue(out T value) +#endif + { + if (_isSuccess) + { + value = _value; + return true; + } + + value = default; + return false; + } + + /// + /// Attempts to get the error + /// + /// The error if failed + /// True if failed, false otherwise +#if !NETSTANDARD2_0 + public bool TryGetError([NotNullWhen(true)] out ResultError error) +#else + public bool TryGetError(out ResultError error) +#endif + { + if (!_isSuccess) + { + error = _error; + return true; + } + + error = default; + return false; + } + + /// + /// Gets the value if successful, otherwise returns the specified default value + /// + /// Default value to return on failure + /// The success value or default + public T GetValueOrDefault(T defaultValue = default) => + _isSuccess ? _value : defaultValue; + + /// + /// Gets the value if successful, otherwise returns the result of the factory function + /// + /// Factory function to create default value + /// The success value or factory result + public T GetValueOrDefault(Func defaultFactory) => + _isSuccess ? _value : defaultFactory(); + + /// + /// Gets the value if successful, otherwise returns the result of the error handler + /// + /// Handler that receives the error and returns a default value + /// The success value or handler result + public T GetValueOrDefault(Func errorHandler) => + _isSuccess ? _value : errorHandler(_error); + + /// + /// Pattern matches on the result, executing the appropriate handler + /// + /// Return type of handlers + /// Handler for success case + /// Handler for failure case + /// Result of the executed handler + public TResult Match( + Func onSuccess, + Func onFailure) => + _isSuccess ? onSuccess(_value) : onFailure(_error); + + /// + /// Executes the appropriate action based on success or failure + /// + /// Action for success case + /// Action for failure case + public void Switch( + Action onSuccess, + Action onFailure) + { + if (_isSuccess) + onSuccess(_value); + else + onFailure(_error); + } + + /// + /// Transforms the success value using the specified mapping function + /// + /// Type of the new value + /// Function to transform the value + /// A new Result with the transformed value or the original error + public Result Map(Func mapper) => + _isSuccess + ? Result.Success(mapper(_value)) + : Result.Failure(_error); + + /// + /// Transforms the error using the specified mapping function + /// + /// Function to transform the error + /// A new Result with the original value or transformed error + public Result MapError(Func mapper) => + _isSuccess + ? this + : Result.Failure(mapper(_error)); + + /// + /// Chains another operation that returns a Result + /// + /// Type of the new value + /// Function that returns a new Result + /// The new Result or the original error + public Result Bind(Func> binder) => + _isSuccess ? binder(_value) : Result.Failure(_error); + + /// + /// Executes an action on success, returning the original result + /// + /// Action to execute on success + /// The original Result + public Result Tap(Action action) + { + if (_isSuccess) + action(_value); + return this; + } + + /// + /// Executes an action on failure, returning the original result + /// + /// Action to execute on failure + /// The original Result + public Result TapError(Action action) + { + if (!_isSuccess) + action(_error); + return this; + } + + /// + /// Ensures a condition is met, converting to failure if not + /// + /// Condition to check + /// Error to use if condition fails + /// The original Result or a failed Result + public Result Ensure(Func predicate, ResultError error) => + _isSuccess && !predicate(_value) + ? Result.Failure(error) + : this; + + /// + /// Ensures a condition is met, converting to failure if not + /// + /// Condition to check + /// Error message to use if condition fails + /// The original Result or a failed Result + public Result Ensure(Func predicate, string errorMessage) => + Ensure(predicate, new ResultError(errorMessage)); + + public bool Equals(Result other) => + _isSuccess == other._isSuccess && + Equals(_value, other._value) && + Equals(_error, other._error); + + public override bool Equals(object obj) => + obj is Result other && Equals(other); + + public override int GetHashCode() + { +#if NETSTANDARD2_0 + unchecked + { + var hashCode = _isSuccess.GetHashCode(); + hashCode = (hashCode * 397) ^ (_value?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (_error?.GetHashCode() ?? 0); + return hashCode; + } +#else + return HashCode.Combine(_isSuccess, _value, _error); +#endif + } + + public static bool operator ==(Result left, Result right) => + left.Equals(right); + + public static bool operator !=(Result left, Result right) => + !left.Equals(right); + + public override string ToString() => + _isSuccess + ? $"Success({_value})" + : $"Failure({_error})"; + } +} diff --git a/src/Cortex.Types/Result/Result2.cs b/src/Cortex.Types/Result/Result2.cs new file mode 100644 index 0000000..8947b66 --- /dev/null +++ b/src/Cortex.Types/Result/Result2.cs @@ -0,0 +1,269 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Cortex.Types +{ + /// + /// Represents the result of an operation that can succeed with a value or fail with a custom error type + /// + /// Type of the success value + /// Type of the error value + public readonly struct Result : IEquatable>, IResult + { + private readonly TValue _value; + private readonly TError _error; + private readonly bool _isSuccess; + + /// + public bool IsSuccess => _isSuccess; + + /// + public bool IsFailure => !_isSuccess; + + /// + /// Thrown when accessing Value on a failed result + public TValue Value => _isSuccess + ? _value + : throw new InvalidOperationException( + $"Cannot access Value on a failed Result. Error: {_error}"); + + /// + /// Thrown when accessing Error on a successful result + public TError Error => !_isSuccess + ? _error + : throw new InvalidOperationException( + "Cannot access Error on a successful Result"); + + private Result(TValue value, TError error, bool isSuccess) + { + _value = value; + _error = error; + _isSuccess = isSuccess; + } + + /// + /// Creates a successful result with the specified value + /// + /// The success value + /// A successful Result + public static Result Success(TValue value) => + new(value, default, true); + + /// + /// Creates a failed result with the specified error + /// + /// The error + /// A failed Result + public static Result Failure(TError error) => + new(default, error, false); + + /// + /// Implicit conversion from value to successful Result + /// + public static implicit operator Result(TValue value) => + Success(value); + + /// + /// Attempts to get the success value + /// + /// The success value if successful + /// True if successful, false otherwise +#if !NETSTANDARD2_0 + public bool TryGetValue([NotNullWhen(true)] out TValue value) +#else + public bool TryGetValue(out TValue value) +#endif + { + if (_isSuccess) + { + value = _value; + return true; + } + + value = default; + return false; + } + + /// + /// Attempts to get the error + /// + /// The error if failed + /// True if failed, false otherwise +#if !NETSTANDARD2_0 + public bool TryGetError([NotNullWhen(true)] out TError error) +#else + public bool TryGetError(out TError error) +#endif + { + if (!_isSuccess) + { + error = _error; + return true; + } + + error = default; + return false; + } + + /// + /// Gets the value if successful, otherwise returns the specified default value + /// + /// Default value to return on failure + /// The success value or default + public TValue GetValueOrDefault(TValue defaultValue = default) => + _isSuccess ? _value : defaultValue; + + /// + /// Gets the value if successful, otherwise returns the result of the factory function + /// + /// Factory function to create default value + /// The success value or factory result + public TValue GetValueOrDefault(Func defaultFactory) => + _isSuccess ? _value : defaultFactory(); + + /// + /// Gets the value if successful, otherwise returns the result of the error handler + /// + /// Handler that receives the error and returns a default value + /// The success value or handler result + public TValue GetValueOrDefault(Func errorHandler) => + _isSuccess ? _value : errorHandler(_error); + + /// + /// Pattern matches on the result, executing the appropriate handler + /// + /// Return type of handlers + /// Handler for success case + /// Handler for failure case + /// Result of the executed handler + public TResult Match( + Func onSuccess, + Func onFailure) => + _isSuccess ? onSuccess(_value) : onFailure(_error); + + /// + /// Executes the appropriate action based on success or failure + /// + /// Action for success case + /// Action for failure case + public void Switch( + Action onSuccess, + Action onFailure) + { + if (_isSuccess) + onSuccess(_value); + else + onFailure(_error); + } + + /// + /// Transforms the success value using the specified mapping function + /// + /// Type of the new value + /// Function to transform the value + /// A new Result with the transformed value or the original error + public Result Map(Func mapper) => + _isSuccess + ? Result.Success(mapper(_value)) + : Result.Failure(_error); + + /// + /// Transforms the error using the specified mapping function + /// + /// Type of the new error + /// Function to transform the error + /// A new Result with the original value or transformed error + public Result MapError(Func mapper) => + _isSuccess + ? Result.Success(_value) + : Result.Failure(mapper(_error)); + + /// + /// Chains another operation that returns a Result + /// + /// Type of the new value + /// Function that returns a new Result + /// The new Result or the original error + public Result Bind(Func> binder) => + _isSuccess ? binder(_value) : Result.Failure(_error); + + /// + /// Executes an action on success, returning the original result + /// + /// Action to execute on success + /// The original Result + public Result Tap(Action action) + { + if (_isSuccess) + action(_value); + return this; + } + + /// + /// Executes an action on failure, returning the original result + /// + /// Action to execute on failure + /// The original Result + public Result TapError(Action action) + { + if (!_isSuccess) + action(_error); + return this; + } + + /// + /// Ensures a condition is met, converting to failure if not + /// + /// Condition to check + /// Error to use if condition fails + /// The original Result or a failed Result + public Result Ensure(Func predicate, TError error) => + _isSuccess && !predicate(_value) + ? Result.Failure(error) + : this; + + /// + /// Converts this Result to use the built-in ResultError type + /// + /// Function to convert the error to ResultError + /// A Result with ResultError + public Result ToResult(Func errorMapper) => + _isSuccess + ? Result.Success(_value) + : Result.Failure(errorMapper(_error)); + + public bool Equals(Result other) => + _isSuccess == other._isSuccess && + Equals(_value, other._value) && + Equals(_error, other._error); + + public override bool Equals(object obj) => + obj is Result other && Equals(other); + + public override int GetHashCode() + { +#if NETSTANDARD2_0 + unchecked + { + var hashCode = _isSuccess.GetHashCode(); + hashCode = (hashCode * 397) ^ (_value?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (_error?.GetHashCode() ?? 0); + return hashCode; + } +#else + return HashCode.Combine(_isSuccess, _value, _error); +#endif + } + + public static bool operator ==(Result left, Result right) => + left.Equals(right); + + public static bool operator !=(Result left, Result right) => + !left.Equals(right); + + public override string ToString() => + _isSuccess + ? $"Success({_value})" + : $"Failure({_error})"; + } +} diff --git a/src/Cortex.Types/Result/ResultError.cs b/src/Cortex.Types/Result/ResultError.cs new file mode 100644 index 0000000..65ff0d7 --- /dev/null +++ b/src/Cortex.Types/Result/ResultError.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Cortex.Types +{ + /// + /// Represents an error in a Result operation with a message and optional exception + /// + public sealed class ResultError : IEquatable + { + /// + /// Gets the error message + /// + public string Message { get; } + + /// + /// Gets the error code (optional) + /// + public string Code { get; } + + /// + /// Gets the inner exception if one was the cause of the error + /// + public Exception Exception { get; } + + /// + /// Gets additional metadata about the error + /// + public IReadOnlyDictionary Metadata { get; } + + /// + /// Creates a new ResultError with the specified message + /// + /// Error message + public ResultError(string message) + : this(message, null, null, null) + { + } + + /// + /// Creates a new ResultError with the specified message and code + /// + /// Error message + /// Error code + public ResultError(string message, string code) + : this(message, code, null, null) + { + } + + /// + /// Creates a new ResultError with the specified message and exception + /// + /// Error message + /// Underlying exception + public ResultError(string message, Exception exception) + : this(message, null, exception, null) + { + } + + /// + /// Creates a new ResultError with all properties + /// + /// Error message + /// Error code + /// Underlying exception + /// Additional metadata + public ResultError( + string message, + string code, + Exception exception, + IDictionary metadata) + { + Message = message ?? throw new ArgumentNullException(nameof(message)); + Code = code; + Exception = exception; + Metadata = metadata != null + ? new Dictionary(metadata) + : new Dictionary(); + } + + /// + /// Creates a ResultError from an exception + /// + /// The exception to convert + /// A new ResultError + public static ResultError FromException(Exception exception) + { + if (exception == null) + throw new ArgumentNullException(nameof(exception)); + + return new ResultError( + exception.Message, + exception.GetType().Name, + exception, + null); + } + + /// + /// Creates a composite error from multiple errors + /// + /// Collection of errors + /// A new ResultError representing all errors + public static ResultError Aggregate(IEnumerable errors) + { + if (errors == null) + throw new ArgumentNullException(nameof(errors)); + + var errorList = errors.ToList(); + if (errorList.Count == 0) + throw new ArgumentException("At least one error is required", nameof(errors)); + + if (errorList.Count == 1) + return errorList[0]; + + var messages = string.Join("; ", errorList.Select(e => e.Message)); + var metadata = new Dictionary + { + ["InnerErrors"] = errorList + }; + + return new ResultError( + $"Multiple errors occurred: {messages}", + "AGGREGATE_ERROR", + null, + metadata); + } + + public bool Equals(ResultError other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Message == other.Message && Code == other.Code; + } + + public override bool Equals(object obj) => + obj is ResultError other && Equals(other); + + public override int GetHashCode() + { +#if NETSTANDARD2_0 + unchecked + { + return ((Message?.GetHashCode() ?? 0) * 397) ^ (Code?.GetHashCode() ?? 0); + } +#else + return HashCode.Combine(Message, Code); +#endif + } + + public static bool operator ==(ResultError left, ResultError right) => + Equals(left, right); + + public static bool operator !=(ResultError left, ResultError right) => + !Equals(left, right); + + public override string ToString() => + string.IsNullOrEmpty(Code) + ? Message + : $"[{Code}] {Message}"; + } +} diff --git a/src/Cortex.Types/Result/ResultExtensions.cs b/src/Cortex.Types/Result/ResultExtensions.cs new file mode 100644 index 0000000..ba527bd --- /dev/null +++ b/src/Cortex.Types/Result/ResultExtensions.cs @@ -0,0 +1,199 @@ +using System; +using System.Threading.Tasks; + +namespace Cortex.Types +{ + /// + /// Provides static factory methods and utilities for creating Result instances + /// + public static class Result + { + /// + /// Creates a successful result with the specified value + /// + /// Type of the value + /// The success value + /// A successful Result + public static Result Success(T value) => + Result.Success(value); + + /// + /// Creates a successful result with the specified value and custom error type + /// + /// Type of the value + /// Type of the error + /// The success value + /// A successful Result + public static Result Success(TValue value) => + Result.Success(value); + + /// + /// Creates a failed result with the specified error + /// + /// Type of the value + /// The error + /// A failed Result + public static Result Failure(ResultError error) => + Result.Failure(error); + + /// + /// Creates a failed result with the specified error message + /// + /// Type of the value + /// The error message + /// A failed Result + public static Result Failure(string errorMessage) => + Result.Failure(errorMessage); + + /// + /// Creates a failed result from an exception + /// + /// Type of the value + /// The exception + /// A failed Result + public static Result Failure(Exception exception) => + Result.Failure(exception); + + /// + /// Creates a failed result with the specified error and custom error type + /// + /// Type of the value + /// Type of the error + /// The error + /// A failed Result + public static Result Failure(TError error) => + Result.Failure(error); + + /// + /// Executes the specified function and wraps any exception in a failed Result + /// + /// Type of the return value + /// Function to execute + /// A Result containing the function result or the caught exception + public static Result Try(Func func) + { + try + { + return Result.Success(func()); + } + catch (Exception ex) + { + return Result.Failure(ex); + } + } + + /// + /// Executes the specified function and wraps any exception in a failed Result + /// + /// Type of the return value + /// Function to execute + /// Handler to convert exception to error + /// A Result containing the function result or the handled exception + public static Result Try(Func func, Func exceptionHandler) + { + try + { + return Result.Success(func()); + } + catch (Exception ex) + { + return Result.Failure(exceptionHandler(ex)); + } + } + + /// + /// Executes the specified async function and wraps any exception in a failed Result + /// + /// Type of the return value + /// Async function to execute + /// A Task containing a Result with the function result or the caught exception + public static async Task> TryAsync(Func> func) + { + try + { + return Result.Success(await func().ConfigureAwait(false)); + } + catch (Exception ex) + { + return Result.Failure(ex); + } + } + + /// + /// Combines two results, returning failure if either fails + /// + public static Result<(T1, T2)> Combine( + Result result1, + Result result2) + { + if (result1.IsFailure) + return Result<(T1, T2)>.Failure(result1.Error); + if (result2.IsFailure) + return Result<(T1, T2)>.Failure(result2.Error); + + return Result<(T1, T2)>.Success((result1.Value, result2.Value)); + } + + /// + /// Combines three results, returning failure if any fails + /// + public static Result<(T1, T2, T3)> Combine( + Result result1, + Result result2, + Result result3) + { + if (result1.IsFailure) + return Result<(T1, T2, T3)>.Failure(result1.Error); + if (result2.IsFailure) + return Result<(T1, T2, T3)>.Failure(result2.Error); + if (result3.IsFailure) + return Result<(T1, T2, T3)>.Failure(result3.Error); + + return Result<(T1, T2, T3)>.Success((result1.Value, result2.Value, result3.Value)); + } + + /// + /// Creates a Result based on a condition + /// + /// Type of the value + /// Condition to evaluate + /// Value to use if condition is true + /// Error to use if condition is false + /// Success or Failure Result based on condition + public static Result SuccessIf(bool condition, T value, ResultError error) => + condition ? Result.Success(value) : Result.Failure(error); + + /// + /// Creates a Result based on a condition + /// + /// Type of the value + /// Condition to evaluate + /// Value to use if condition is true + /// Error message to use if condition is false + /// Success or Failure Result based on condition + public static Result SuccessIf(bool condition, T value, string errorMessage) => + condition ? Result.Success(value) : Result.Failure(errorMessage); + + /// + /// Creates a Result based on a condition (inverted) + /// + /// Type of the value + /// Condition to evaluate + /// Value to use if condition is false + /// Error to use if condition is true + /// Success or Failure Result based on condition + public static Result FailureIf(bool condition, T value, ResultError error) => + condition ? Result.Failure(error) : Result.Success(value); + + /// + /// Creates a Result based on a condition (inverted) + /// + /// Type of the value + /// Condition to evaluate + /// Value to use if condition is false + /// Error message to use if condition is true + /// Success or Failure Result based on condition + public static Result FailureIf(bool condition, T value, string errorMessage) => + condition ? Result.Failure(errorMessage) : Result.Success(value); + } +}