diff --git a/.editorconfig b/.editorconfig index 336df47..ad2181c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,12 +1,12 @@ root = true [*] -indent_style = space charset = utf-8 +end_of_line = lf +indent_style = space indent_size = 4 -insert_final_newline = true trim_trailing_whitespace = true -end_of_line = lf +insert_final_newline = true # XML project files [*.{csproj,fsproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] @@ -17,24 +17,26 @@ indent_size = 2 indent_size = 2 [*.{cs,csx}] -# Organize usings +max_line_length = 120 dotnet_sort_system_directives_first = true # this. preferences -dotnet_style_qualification_for_field = false:silent -dotnet_style_qualification_for_property = false:silent -dotnet_style_qualification_for_method = false:silent -dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:error dotnet_style_predefined_type_for_member_access = true:error # Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion + # Modifier preferences dotnet_style_require_accessibility_modifiers = omit_if_default:warning dotnet_style_readonly_field = true:suggestion + # Expression-level preferences dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion @@ -50,33 +52,31 @@ dotnet_style_prefer_conditional_expression_over_return = true:silent # Style Definitions dotnet_naming_style.pascal_case_style.capitalization = pascal_case -dotnet_naming_style.camel_case_style.capitalization = camel_case - +dotnet_naming_style.camel_case_style.capitalization = camel_case # constant case preferences -dotnet_naming_symbols.constant_fields.applicable_kinds = field -dotnet_naming_symbols.constant_fields.applicable_accessibilities = * -dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style # fields case preferences dotnet_naming_symbols.private_field_symbols.applicable_kinds = field dotnet_naming_symbols.private_field_symbols.applicable_accessibilities = private + dotnet_naming_rule.private_field_symbols_should_be_camel_case.symbols = private_field_symbols dotnet_naming_rule.private_field_symbols_should_be_camel_case.severity = warning dotnet_naming_rule.private_field_symbols_should_be_camel_case.style = camel_case_style -# protected fields -dotnet_naming_symbols.protected_field_symbols.applicable_kinds = field +dotnet_naming_symbols.protected_field_symbols.applicable_kinds = field, property dotnet_naming_symbols.protected_field_symbols.applicable_accessibilities = protected dotnet_naming_rule.protected_field_symbols_should_be_camel_case.symbols = protected_field_symbols -dotnet_naming_rule.protected_field_symbols_should_be_camel_case.severity = warning +dotnet_naming_rule.protected_field_symbols_should_be_camel_case.severity = suggestion dotnet_naming_rule.protected_field_symbols_should_be_camel_case.style = camel_case_style -[*.cs] # var preferences csharp_style_var_for_built_in_types = true:silent csharp_style_var_when_type_is_apparent = true:silent @@ -95,13 +95,16 @@ csharp_style_expression_bodied_local_functions = true:suggestion # Pattern matching preferences csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + # Null-checking preferences csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion + # Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion + # Expression-level preferences -csharp_prefer_braces = when_multiline:suggestion +csharp_prefer_braces = false csharp_style_deconstructed_variable_declaration = true:suggestion csharp_prefer_simple_default_expression = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion @@ -119,6 +122,7 @@ csharp_new_line_between_query_expression_clauses = true csharp_indent_case_contents = true csharp_indent_switch_labels = true csharp_indent_labels = flush_left + # Space preferences csharp_space_after_cast = false csharp_space_after_keywords_in_control_flow_statements = true @@ -135,27 +139,41 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true +# Analysers +## dotnet +dotnet_diagnostic.IDE0011.severity = suggestion # require braces +dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary. +dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary. +dotnet_diagnostic.CA1860.severity = silent # Prefer comparing 'Length' to 0 rather than using 'Any()' + +## Sonar +dotnet_diagnostic.S1481.severity = warning # variable never used +dotnet_diagnostic.S1144.severity = suggestion # Remove the unused private constructor 'AddAuthorizationHeaderOperationFilter'. +dotnet_diagnostic.S1118.severity = suggestion # Add a 'protected' constructor or the 'static' keyword to the class declaration. +dotnet_diagnostic.S1075.severity = suggestion # hardcoded URIs +dotnet_diagnostic.S3459.severity = suggestion # Unassigned members should be removed +dotnet_diagnostic.S2699.severity = suggestion # add assert to test +dotnet_diagnostic.S3237.severity = silent # Use the 'value' contextual keyword in this property set accessor declaration. +dotnet_diagnostic.S1210.severity = silent # When implementing IComparable , you should also override , =, >, and >=. +dotnet_diagnostic.S2219.severity = silent # runtime type checking should be simplified +dotnet_diagnostic.S2094.severity = silent # empty class (false positive) + + +## AsyncFixer +dotnet_diagnostic.AsyncFixer01.severity = suggestion # The method dont needs async -# Roslynator -dotnet_analyzer_diagnostic.category-roslynator.severity = default -roslynator_analyzers.enabled_by_default = true -roslynator_refactorings.enabled = true -roslynator_compiler_diagnostic_fixes.enabled = true +# Nunit +dotnet_diagnostic.NUnit1027.severity = suggestion # parameter error nunit -dotnet_diagnostic.RCS1214.severity = warning -dotnet_diagnostic.RCS1036.severity = warning -dotnet_diagnostic.S3881.severity = suggestion -dotnet_diagnostic.IDE0011.severity = silent -dotnet_diagnostic.RCS1001.severity = silent -dotnet_diagnostic.RCS1194.severity = silent -dotnet_diagnostic.RCS1163.severity = silent -dotnet_diagnostic.RCS1118.severity = silent -dotnet_diagnostic.CA1050.severity = silent -dotnet_diagnostic.S3925.severity = silent +# Resharper +resharper_space_within_single_line_array_initializer_braces = true +resharper_keep_existing_attribute_arrangement = true +resharper_max_array_initializer_elements_on_line = 10 +resharper_max_initializer_elements_on_line = 1 +resharper_trailing_comma_in_multiline_lists = true +resharper_wrap_array_initializer_style = chop_if_long +resharper_empty_block_style = together_same_line +[Usings.cs] +dotnet_diagnostic.IDE0005.severity = silent # Using directive is unnecessary. -# Nunit -dotnet_diagnostic.NUnit1027.severity = silent -dotnet_diagnostic.S3903.severity = silent -dotnet_diagnostic.AsyncFixer05.severity = silent -dotnet_diagnostic.S3973.severity = silent diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index f24e754..9343d1f 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -91,8 +91,7 @@ "Lint", "Report", "Restore", - "Test", - "TestCoverage" + "Test" ] } }, @@ -115,8 +114,7 @@ "Lint", "Report", "Restore", - "Test", - "TestCoverage" + "Test" ] } }, diff --git a/build/Build.cs b/build/Build.cs index f7720a4..e2e02d5 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -1,7 +1,8 @@ class BuildProject : NukeBuild { [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] - readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + readonly Configuration Configuration = + IsLocalBuild ? Configuration.Debug : Configuration.Release; [Parameter(List = false)] readonly bool DotnetRunningInContainer; [GlobalJson] readonly GlobalJson GlobalJson; @@ -39,18 +40,6 @@ class BuildProject : NukeBuild .EnableNoRestore())); Target Test => _ => _ - .Description("Run all tests") - .DependsOn(Build) - .Executes(() => Solution - .GetProjects("*.Tests") - .ForEach(project => - DotNetTest(s => s - .EnableNoBuild() - .EnableNoRestore() - .SetConfiguration(Configuration) - .SetProjectFile(project)))); - - Target TestCoverage => _ => _ .Description("Run tests with coverage") .DependsOn(Build) .Executes(() => DotNetTest(s => s @@ -74,7 +63,8 @@ class BuildProject : NukeBuild Target Lint => _ => _ .Description("Check for codebase formatting and analysers") .DependsOn(Build) - .Executes(() => DotNet($"format -v normal --no-restore --verify-no-changes \"{Solution.Path}\"")); + .Executes(() => + DotNet($"format -v normal --no-restore --verify-no-changes \"{Solution.Path}\"")); Target Format => _ => _ .Description("Try fix codebase formatting and analysers") @@ -83,12 +73,12 @@ class BuildProject : NukeBuild Target Report => _ => _ .Description("Run tests and generate coverage report") - .DependsOn(TestCoverage) + .DependsOn(Test) .Triggers(GenerateReport, BrowseReport); Target GenerateReport => _ => _ .Description("Generate test coverage report") - .After(TestCoverage) + .After(Test) .OnlyWhenDynamic(() => CoverageFiles.GlobFiles().Any()) .Executes(() => ReportGenerator(r => r @@ -124,7 +114,7 @@ class BuildProject : NukeBuild Target GenerateBadges => _ => _ .Description("Generate cool badges for readme") - .After(TestCoverage) + .After(Test) .Requires(() => CoverageFiles.GlobFiles().Any()) .Executes(() => { diff --git a/src/Json/ResultJsonTypeConverter.cs b/src/Json/ResultJsonTypeConverter.cs index 02ada2b..5456ee8 100644 --- a/src/Json/ResultJsonTypeConverter.cs +++ b/src/Json/ResultJsonTypeConverter.cs @@ -70,11 +70,11 @@ public override Result Read( Utf8JsonReader valueReader = new(JsonSerializer.SerializeToUtf8Bytes(value)); valueReader.Read(); if (status.GetString() == statusOk) - return new Result( + return new( okConverter.Read(ref valueReader, okType, options)!); if (status.GetString() == statusError) - return new Result( + return new( errorConverter.Read(ref valueReader, errorType, options)!); throw new JsonException($"Invalid status {status}"); diff --git a/src/Result.cs b/src/Result.cs index f2cffca..9fd4f3e 100644 --- a/src/Result.cs +++ b/src/Result.cs @@ -4,6 +4,11 @@ namespace Resulteles; +/// +/// Represents an successful operation +/// +public readonly record struct Success; + /// /// Helper type for errorValue handling without exceptions. /// @@ -40,8 +45,8 @@ namespace Resulteles; public Result(TOk okValue) { IsOk = true; - this.OkValue = okValue; - this.ErrorValue = default; + OkValue = okValue; + ErrorValue = default; } /// @@ -50,8 +55,8 @@ public Result(TOk okValue) public Result(TError error) { IsOk = false; - this.OkValue = default; - this.ErrorValue = error; + OkValue = default; + ErrorValue = error; } /// @@ -125,7 +130,7 @@ public bool Equals(Result other) => /// Match the result to obtain the value /// public T Match(Func ok, Func error) => - IsOk ? ok(this.OkValue) : error(this.ErrorValue); + IsOk ? ok(OkValue) : error(ErrorValue); /// /// Switch the result to process value @@ -133,8 +138,8 @@ public T Match(Func ok, Func error) => public void Switch(Action ok, Action error) { if (IsOk) - ok(this.OkValue); + ok(OkValue); else - error(this.ErrorValue); + error(ErrorValue); } } diff --git a/src/ResultAsyncExtensions.cs b/src/ResultAsyncExtensions.cs index 35c5263..8be61da 100644 --- a/src/ResultAsyncExtensions.cs +++ b/src/ResultAsyncExtensions.cs @@ -47,13 +47,6 @@ public static async Task> Tap( return result; } - /// - /// Match the result to obtain the value - /// - public static async Task Match(this Result result, - Func> ok, Func> error) => - result.IsOk ? await ok(result.OkValue) : await error(result.ErrorValue); - /// /// Match the result to obtain the value /// @@ -71,6 +64,45 @@ public static async Task Func> error) => result.IsOk ? ok(result.OkValue) : await error(result.ErrorValue); + /// + /// Switch the result to process value + /// + public static async Task SwitchAsync( + this Result result, + Func ok, + Func error) + { + if (result.IsOk) + await ok(result.OkValue); + else + await error(result.ErrorValue); + } + + /// + /// Switch the result to process value + /// + public static async Task SwitchAsync(this Result result, + Func ok, Action error) + { + if (result.IsOk) + await ok(result.OkValue); + else + error(result.ErrorValue); + } + + /// + /// Switch the result to process value + /// + public static async Task SwitchAsync(this Result result, + Action ok, + Func error) + { + if (result.IsOk) + ok(result.OkValue); + else + await error(result.ErrorValue); + } + /// /// Projects ok result value into a new form. /// e @@ -145,43 +177,5 @@ public static Task> SelectManyAsync( public static async Task> SelectManyAsync( this Result result, Func>> bind) => - result.IsError ? new Result(result.ErrorValue) : await bind(result.OkValue); - - /// - /// Switch the result to process value - /// - public static async Task Switch(this Result result, - Func ok, - Func error) - { - if (result.IsOk) - await ok(result.OkValue); - else - await error(result.ErrorValue); - } - - /// - /// Switch the result to process value - /// - public static async Task Switch(this Result result, - Func ok, Action error) - { - if (result.IsOk) - await ok(result.OkValue); - else - error(result.ErrorValue); - } - - /// - /// Switch the result to process value - /// - public static async Task Switch(this Result result, - Action ok, - Func error) - { - if (result.IsOk) - ok(result.OkValue); - else - await error(result.ErrorValue); - } + result.IsError ? new(result.ErrorValue) : await bind(result.OkValue); } diff --git a/src/ResultException.cs b/src/ResultException.cs index 5eedfc2..c4c021c 100644 --- a/src/ResultException.cs +++ b/src/ResultException.cs @@ -1,14 +1,12 @@ using System.Runtime.Serialization; -#pragma warning disable CS0628 - namespace Resulteles; /// /// The exception that is thrown when a invalid explicit cast is made on a result. /// [Serializable] -public sealed class ResultInvalidCastException : Exception +public class ResultInvalidCastException : Exception { /// internal ResultInvalidCastException(string message) : base(message) { } @@ -22,12 +20,12 @@ protected ResultInvalidCastException(SerializationInfo info, StreamingContext co /// The exception that is thrown for invalid result values. /// [Serializable] -public sealed class ResultInvalidException : Exception +public class ResultException : Exception { /// - internal ResultInvalidException(string message) : base(message) { } + internal ResultException(string message) : base(message) { } /// - protected ResultInvalidException(SerializationInfo info, StreamingContext context) + protected ResultException(SerializationInfo info, StreamingContext context) : base(info, context) { } } diff --git a/src/ResultLinqExtensions.cs b/src/ResultLinqExtensions.cs index 5a9fe1e..7f85715 100644 --- a/src/ResultLinqExtensions.cs +++ b/src/ResultLinqExtensions.cs @@ -100,7 +100,14 @@ public static Result Zip( /// /// Return new collection with ok values only /// - public static IEnumerable Choose( + public static IEnumerable GetErrorValues( + this IEnumerable> results) => + from result in results where result.IsError select result.ErrorValue; + + /// + /// Return new collection with ok values only + /// + public static IEnumerable GetOkValues( this IEnumerable> results ) => from result in results where result.IsOk select result.OkValue; @@ -108,7 +115,64 @@ this IEnumerable> results /// /// Return new collection with ok values only /// - public static IEnumerable ChooseErrors( - this IEnumerable> results) => - from result in results where result.IsError select result.ErrorValue; + public static IEnumerable ChooseResult( + this IEnumerable results, + Func> selector + ) => results.Select(selector).GetOkValues(); + + /// + /// Return one result with all ok values or first error + /// + public static Result, TError> ToSingleResult( + this IEnumerable> results + ) + { + var values = new List(); + foreach (var result in results) + { + if (result.IsError) return result.ErrorValue; + values.Add(result.OkValue); + } + + return values.AsReadOnly(); + } + + /// + /// Return one result with all ok values or first error + /// + public static Result, TError> ToSingleResult( + this IEnumerable results, + Func> selector + ) => results.Select(selector).ToSingleResult(); + + /// + /// Return one result with all ok or all errors + /// + public static Result, IReadOnlyList> ToSingleResultWithAllErrors( + this IEnumerable> results + ) + { + var values = new List(); + var errors = new List(); + + foreach (var result in results) + { + if (result.IsOk) + values.Add(result.OkValue); + else + errors.Add(result.ErrorValue); + } + + if (errors.Any()) return errors.AsReadOnly(); + return values.AsReadOnly(); + } + + /// + /// Return one result with all ok values or first error + /// + public static Result, IReadOnlyList> ToSingleResultWithAllErrors( + this IEnumerable results, + Func> selector + ) => results.Select(selector).ToSingleResultWithAllErrors(); + } diff --git a/src/ResultStatic.cs b/src/ResultStatic.cs index 223a1ce..ab81814 100644 --- a/src/ResultStatic.cs +++ b/src/ResultStatic.cs @@ -25,6 +25,12 @@ public static Result Ok(TOk result) => public static Result Error(TError error) => Result.Error(error); + /// + /// Represents an Error or a Failure. The code failed with a value of 'TError representing what went wrong. + /// + public static Result Error(TError error) => + Result.Error(error); + /// /// Try run function, catching exceptions as a result error value /// diff --git a/src/ResultValueExtensions.cs b/src/ResultValueExtensions.cs index 5fcfd52..2c03353 100644 --- a/src/ResultValueExtensions.cs +++ b/src/ResultValueExtensions.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; namespace Resulteles; @@ -63,7 +62,7 @@ public static TOk GetValueOrThrow(this Result result) if (result.ErrorValue is Exception exception) throw exception; else - throw new ResultInvalidException($"{result.ErrorValue}"); + throw new ResultException($"{result.ErrorValue}"); return result.OkValue; } @@ -76,7 +75,7 @@ public static TOk GetValueOrThrow( Func formatMessage) { if (result.IsError) - throw new ResultInvalidException(formatMessage(result.ErrorValue)); + throw new ResultException(formatMessage(result.ErrorValue)); return result.OkValue; } @@ -105,7 +104,7 @@ public static void ThrowIfError(this Result result) if (result.ErrorValue is Exception exception) throw exception; - throw new ResultInvalidException($"{result.ErrorValue}"); + throw new ResultException($"{result.ErrorValue}"); } /// @@ -115,7 +114,7 @@ public static void ThrowIfError(this Result result, Func formatMessage) { if (result.IsError) - throw new ResultInvalidException(formatMessage(result.ErrorValue)); + throw new ResultException(formatMessage(result.ErrorValue)); } /// @@ -196,24 +195,6 @@ public static void Deconstruct( where TOk : struct => result.Select(x => (TOk?)x); - /// - /// Convert result of task into task of result - /// - public static Result, TError> ToResult( - this IEnumerable> results) - { - List okResults = new(); - foreach (var result in results) - { - if (result.IsOk) - okResults.Add(result.OkValue); - else - return new(result.ErrorValue); - } - - return new(new ReadOnlyCollection(okResults)); - } - /// /// If a result is successful, returns it, otherwise . /// @@ -222,4 +203,14 @@ public static Result, TError> ToResult( where T : struct => valueResult.IsOk ? valueResult.OkValue : null; + + /// + /// Run side effect when result is OK + /// + public static Result Tap( + this Result result, Action action) + { + if (result.IsOk) action(result.OkValue); + return result; + } } diff --git a/tests/Resulteles.Tests/ResultAsyncTests.cs b/tests/Resulteles.Tests/ResultAsyncTests.cs new file mode 100644 index 0000000..51b602f --- /dev/null +++ b/tests/Resulteles.Tests/ResultAsyncTests.cs @@ -0,0 +1,134 @@ +#pragma warning disable CS1998 +namespace Resulteles.Tests; + +public class ResultAsyncTests +{ + [Test] + public async Task ShouldMapAsync() + { + var result = await Result.Ok(42).SelectAsync(Task.FromResult); + result.Should().Be(Result.Ok(42)); + } + + [Test] + public async Task ShouldMapOkAndErrorsOnOkAsync() + { + var result = await Result.Ok(42).SelectAsync(Task.FromResult, Task.FromResult); + result.Should().Be(Result.Ok(42)); + } + + [Test] + public async Task ShouldMapOkAndErrorsOnErrorAsync() + { + var result = await Result.Error("Err").SelectAsync(Task.FromResult, Task.FromResult); + result.Should().Be(Result.Error("Err")); + } + + + [Test] + public async Task ShouldBindAsync() + { + var result = await Result.Ok(42) + .SelectManyAsync(x => Task.FromResult(Result.Ok(x + 10))); + + result.Should().Be(Result.Ok(52)); + } + + [Test] + public async Task ShouldTapValue() + { + var tappedValue = 0; + _ = await new Result(42).Tap(async v => tappedValue = v); + tappedValue.Should().Be(42); + } + + [Test] + public async Task ShouldMatchSuccessValueAsync() + { + var result = Result.Ok(42); + + var value = await result.Match( + async x => x.ToString(), + _ => "" + ); + + value.Should().Be("42"); + } + + [Test] + public async Task ShouldMatchErrorValue() + { + var result = Result.Error("Err"); + + var value = await result.Match( + x => x.ToString(), + async error => $"{error}!" + ); + + value.Should().Be("Err!"); + } + + [Test] + public async Task ShouldMatchSuccessValueBothAsync() + { + var result = Result.Ok(42); + + var value = await result.Match( + async x => x.ToString(), + async _ => "" + ); + + value.Should().Be("42"); + } + + [Test] + public async Task ShouldMatchErrorBothValue() + { + var result = Result.Error("Err"); + + var value = await result.Match( + async x => x.ToString(), + async error => $"{error}!" + ); + + value.Should().Be("Err!"); + } + + [Test] + public async Task ShouldSwitchOnSuccessValueAsync() + { + var result = Result.Ok(42); + + await result.SwitchAsync( + async value => + { + value.Should().Be(42); + Assert.Pass(); + }, + async _ => + { + Assert.Fail(); + }); + + Assert.Fail(); + } + + [Test] + public async Task ShouldSwitchOnErrorValueAsync() + { + var result = Result.Error("Err"); + + await result.SwitchAsync( + async _ => + { + Assert.Fail(); + }, + async error => + { + error.Should().Be("Err"); + Assert.Pass(); + }); + + Assert.Fail(); + } +} diff --git a/tests/Resulteles.Tests/ResultLinqTests.cs b/tests/Resulteles.Tests/ResultLinqTests.cs new file mode 100644 index 0000000..e2fd582 --- /dev/null +++ b/tests/Resulteles.Tests/ResultLinqTests.cs @@ -0,0 +1,272 @@ +namespace Resulteles.Tests; + +public class ResultLinqTests +{ + static T Identity(T value) => value; + + static Result IsEven(int n) => n % 2 == 0 ? n : $"Invalid {n}"; + + + [Test] + public void ShouldCombineOkResults() + { + var result = + from ok1 in Result.Ok(42) + from ok2 in Result.Ok(100) + select ok1 + ok2; + + result.Should().Be(Result.Ok(142)); + } + + [Test] + public void ShouldShorCircuitErrorResult() + { + var result = + from ok1 in Result.Ok(42) + from ok2 in Result.Error("FAIL") + from ok3 in Result.Ok(100) + select ok1 + ok2 + ok3; + result.Should().Be(Result.Error("FAIL")); + } + + [Test] + public void ShouldAsEnumerableHaveOneItemOnOk() + { + var result = Result.Ok(42); + result.AsEnumerable().Should().BeEquivalentTo(new[] { 42 }); + } + + [Test] + public void ShouldAsEnumerableBeEmptyOnError() + { + var result = Result.Error("fail"); + result.AsEnumerable().Should().BeEmpty(); + } + + [Test] + public void ShouldAToArrayHaveOneItemOnOk() + { + var result = Result.Ok(42); + result.ToArray().Should().BeEquivalentTo(new[] { 42 }); + } + + [Test] + public void ShouldArrayBeEmptyOnError() + { + var result = Result.Error("fail"); + result.ToArray().Should().BeEmpty(); + } + + [Test] + public void ShouldMapOkValue() => + Result.Ok(42) + .Select(x => x.ToString()) + .Should().Be(Result.Ok("42")); + + + [Test] + public void ShouldMapErrorValue() => + Result.Error(-1) + .SelectError(x => x.ToString()) + .Should().Be(Result.Error("-1")); + + [Test] + public void ShouldMapOkAndErrorValuesOnOk() => + Result.Ok(42) + .Select(x => x.ToString(), y => y.ToString()) + .Should().Be(Result.Ok("42")); + + [Test] + public void ShouldMapOkAndErrorValuesOnError() => + Result.Error(-1) + .Select(x => x.ToString(), y => y.ToString()) + .Should().Be(Result.Error("-1")); + + [PropertyTest] + public bool ShouldRespectFunctorIdentityLaw(Result result) + { + var mapped = result.Select(Identity); + return result == mapped; + } + + [PropertyTest] + public bool ShouldRespectFunctorCompositionLaw(Result result, Func f, Func g) + { + var composed = result.Select(v => g(f(v))); + var mapped = result.Select(f).Select(g); + + return composed == mapped; + } + + [PropertyTest] + public bool ShouldRespectMonadLeftIdentityLaw(int value, Func> h) + { + var mapped = Result.Ok(value).SelectMany(h); + return mapped == h(value); + } + + [PropertyTest] + public bool ShouldRespectMonadRightIdentityLaw(Result m) + { + var mapped = m.SelectMany(Result.Ok); + return mapped == m; + } + + [PropertyTest] + public bool ShouldRespectMonadAssociativityIdentityLaw( + Result m, + Func> g, + Func> h + ) + { + var left = m.SelectMany(g).SelectMany(h); + var right = m.SelectMany(x => g(x).SelectMany(h)); + + return left == right; + } + + [PropertyTest] + public bool ShouldPropagateOkToString(int value) => + Result.Ok(value).ToString() == value.ToString(); + + [PropertyTest] + public bool ShouldPropagateErrorToString(int value) => + Result.Error(value).ToString() == value.ToString(); + + [Test] + public void ShouldZipValuesIntoTupleOnOk() + { + var r1 = Result.Ok(42); + var r2 = Result.Ok(1); + + var result = r1.Zip(r2); + result.Should().Be(Result<(int, int), string>.Ok((42, 1))); + } + + [Test] + public void ShouldZipValuesWithFunction() + { + var r1 = Result.Ok(42); + var r2 = Result.Ok(1); + + var result = r1.Zip(r2, (a, b) => (a + b).ToString()); + result.Should().Be(Result.Ok("43")); + } + + [Test] + public void ShouldZipValuesIntoTupleReturnErrorWhenFirstResultIsError() + { + var r1 = Result.Error("Err1"); + var r2 = Result.Ok(1); + + var result = r1.Zip(r2); + result.Should().Be(Result<(int, int), string>.Error("Err1")); + } + + [Test] + public void ShouldZipValuesIntoTupleReturnErrorWhenSecondResultIsError() + { + var r1 = Result.Ok(1); + var r2 = Result.Error("Err2"); + + var result = r1.Zip(r2); + result.Should().Be(Result<(int, int), string>.Error("Err2")); + } + + [Test] + public void ShouldGetOkValuesFromCollections() + { + var results = new Result[] { "Err1", 42, "Err2", 99 }; + + results.GetOkValues().Should().BeEquivalentTo( + new[] { 42, 99, } + ); + } + + [Test] + public void ShouldGetErrorValuesFromCollections() + { + var results = new Result[] { "Err1", 42, "Err2", 99 }; + results.GetErrorValues().Should().BeEquivalentTo("Err1", "Err2"); + } + + [Test] + public void ShouldChooseOnlyOkValuesFromEnumerable() + { + var results = new[] { 1, 2, 3, 4 }.ChooseResult(IsEven); + + results.Should().BeEquivalentTo(new[] { 2, 4 }); + } + + [Test] + public void ShouldTraverseResultIntoSingleErrorResult() + { + var results = new Result[] { "Err1", 42, "Err2", 99 }; + results.ToSingleResult().Should().Be( + Result.Error, string>("Err1") + ); + } + + [Test] + public void ShouldTraverseResultIntoErrorListResult() + { + var results = new Result[] { "Err1", 42, "Err2", 99 }; + results.ToSingleResultWithAllErrors().Should() + .BeOfType, IReadOnlyList>>() + .And.BeEquivalentTo(new + { + IsError = true, + Value = new[] { "Err1", "Err2" } + }); + } + + [Test] + public void ShouldTraverseResultIntoOkListResult() + { + var results = new Result[] { 42, 99 }; + results.ToSingleResult().Should() + .BeOfType, string>>() + .And + .BeEquivalentTo(new + { + IsOk = true, + Value = new[] { 42, 99 } + }); + } + + [Test] + public void ShouldTraverseEnumerableIntoSingleErrorResult() + { + var results = new[] { 2, 3, 4, 5 }.ToSingleResult(IsEven); + results.Should().Be( + Result.Error, string>("Invalid 3") + ); + } + + [Test] + public void ShouldEnumerableResultIntoErrorListResult() + { + var results = new[] { 2, 3, 4, 5 }.ToSingleResultWithAllErrors(IsEven); + results.Should() + .BeOfType, IReadOnlyList>>() + .And.BeEquivalentTo(new + { + IsError = true, + Value = new[] { "Invalid 3", "Invalid 5" }, + }); + } + + [Test] + public void ShouldEnumerableResultIntoOkListResult() + { + var results = new[] { 2, 4, 6 }.ToSingleResult(IsEven); + results.Should() + .BeOfType, string>>() + .And + .BeEquivalentTo(new + { + IsOk = true, + Value = new[] { 2, 4, 6 }, + }); + } +} diff --git a/tests/Resulteles.Tests/ResultTests.cs b/tests/Resulteles.Tests/ResultTests.cs index c53e41e..5613f1b 100644 --- a/tests/Resulteles.Tests/ResultTests.cs +++ b/tests/Resulteles.Tests/ResultTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Resulteles; namespace Resulteles.Tests; @@ -21,6 +20,22 @@ public void ShouldCompareEqualBeFalse() (ok1 == ok2).Should().BeFalse(); } + [Test] + public void ShouldCompareNotEqualBeFalse() + { + var ok1 = Result.Ok(42); + var ok2 = Result.Ok(42); + (ok1 != ok2).Should().BeFalse(); + } + + [Test] + public void ShouldCompareNotEqualBeTrue() + { + var ok1 = Result.Ok(42); + var ok2 = Result.Ok(99); + (ok1 != ok2).Should().BeTrue(); + } + [Test] public void ShouldSerializeOkResult() { @@ -40,101 +55,224 @@ public void ShouldSerializeErrorResult() } [Test] - public void ShouldCombineOkResults() + public void ShouldPatternMatchPropertyOk() { - var result = - from ok1 in Result.Ok(42) - from ok2 in Result.Ok(100) - select ok1 + ok2; + if (Result.Ok(42) is { Value: 42 }) + Assert.Pass(); - result.Should().Be(Result.Ok(142)); + Assert.Fail("unexpected!"); } [Test] - public void ShouldShorCircuitErrorResult() + public void ShouldPatternMatchTupleOk() { - var result = - from ok1 in Result.Ok(42) - from ok2 in Result.Error("FAIL") - from ok3 in Result.Ok(100) - select ok1 + ok2 + ok3; - result.Should().Be(Result.Error("FAIL")); + if (Result.Ok(42) is (true, 42, null)) + Assert.Pass(); + + Assert.Fail("unexpected!"); } [Test] - public void ShouldBeEnumerable() + public void ShouldTryOk() { - foreach (var value in Result.Ok(42).AsEnumerable()) - if (value == 42) - Assert.Pass(); + var result = Result.Ok(42); - Assert.Fail("unexpected!"); + if (result.TryOk(out var value) && value == 42) + Assert.Pass(); + + Assert.Fail(); } [Test] - public void ShouldMatchPropertyOk() + public void ShouldTryError() { - if (Result.Ok(42) is { Value: 42 }) + var result = Result.Error("Failure"); + + if (result.TryError(out var value) && value == "Failure") Assert.Pass(); - Assert.Fail("unexpected!"); + Assert.Fail(); } [Test] - public void ShouldMatchTupleOk() + public void ShouldTryGetOrError() { - if (Result.Ok(42) is (true, 42, null)) + var result = Result.Error("BAD"); + + if (!result.AsNullable().TryGet(out var value, out var error)) + { + Assert.IsNull(value); + Assert.IsNotNull(error); Assert.Pass(); + } - Assert.Fail("unexpected!"); + Assert.Fail(); } [Test] - public void ShouldMapValue() => - Result.Ok(42) - .Select(x => x.ToString()) - .Should().Be(Result.Ok("42")); + public void ShouldMatchSuccessValue() + { + var result = Result.Ok(42); + + var value = result.Match( + x => x.ToString(), + _ => "NOPE" + ); + + value.Should().Be("42"); + } [Test] - public void ShouldTryGet() + public void ShouldMatchErrorValue() + { + var result = Result.Error("Err"); + + var value = result.Match( + x => x.ToString(), + error => $"{error}!" + ); + + value.Should().Be("Err!"); + } + + + [Test] + public void ShouldSwitchOnSuccessValue() { var result = Result.Ok(42); - if (result.TryOk(out var value) && value == 42) - Assert.Pass(); + result.Switch( + value => + { + value.Should().Be(42); + Assert.Pass(); + }, + _ => + { + Assert.Fail(); + }); Assert.Fail(); } [Test] - public void ShouldTryGetOrError() + public void ShouldSwitchOnErrorValue() { - var result = Result.Error("BAD"); + var result = Result.Error("Err"); - if (!result.AsNullable().TryGet(out var value, out var error)) - { - Assert.IsNull(value); - Assert.IsNotNull(error); - Assert.Pass(); - } + result.Switch( + _ => + { + Assert.Fail(); + }, + error => + { + error.Should().Be("Err"); + Assert.Pass(); + }); Assert.Fail(); } + [PropertyTest] + public void ShouldValueBeSameAsOkValue(int okValue) + { + var result = Result.Ok(okValue); + result.Value.Should().Be(okValue); + } + + [PropertyTest] + public void ShouldValueBeSameAsErrorValue(string errorValue) + { + var result = Result.Error(errorValue); + result.Value.Should().Be(errorValue); + } + + [PropertyTest] + public bool ShouldExplicitCastOnOk(int okValue) + { + var result = Result.Ok(okValue); + var casted = (int)result; + + return casted == okValue; + } + + [PropertyTest] + public bool ShouldExplicitCastOnError(string errorValue) + { + var result = Result.Error(errorValue); + var casted = (string)result; + + return casted == errorValue; + } + [Test] - public async Task ShouldMapAsync() + public void ShouldThrowOnBadExplicitErrorCast() { - var result = await Result.Ok(42).SelectAsync(Task.FromResult); - result.Should().Be(Result.Ok(42)); + var result = Result.Error("NOPE"); + var action = () => (int)result; + action.Should().Throw(); } [Test] - public async Task ShouldBindAsync() + public void ShouldThrowOnExplicitCastOnError() { - var result = await Result.Ok(42) - .SelectManyAsync(x => Task.FromResult(Result.Ok(x + 10))); + var result = Result.Ok(42); + var action = () => (string)result; + action.Should().Throw(); + } + + [Test] + public void ShouldReturnOkValueImplicitly() + { + Result Stuff() => 42; + Stuff().Should().Be(Result.Ok(42)); + } + + [Test] + public void ShouldReturnErrorValueImplicitly() + { + Result Stuff() => "Err"; + Stuff().Should().Be(Result.Error("Err")); + } + + + [Test] + public void ShouldTryResultWithSuccess() + { + var result = Result.Try(() => 42); + result.Should().Be(Result.Ok(42)); + } + + [Test] + public void ShouldTryResultWithError() + { + var result = Result.Try(() => + { + throw new InvalidOperationException("NOPE"); + }); + + result.Should().BeOfType>(); + result.Value.Should().BeOfType().And.BeEquivalentTo(new { Message = "NOPE" }); + } + + [Test] + public async Task ShouldTryAsyncResultWithSuccess() + { + var result = await Result.TryAsync(() => Task.FromResult(42)); + result.Should().Be(Result.Ok(42)); + } + + [Test] + public async Task ShouldTryAsyncResultWithError() + { + var result = await Result.TryAsync(() => + { + throw new InvalidOperationException("NOPE"); + }); - result.Should().Be(Result.Ok(52)); + result.Should().BeOfType>(); + result.Value.Should().BeOfType().And.BeEquivalentTo(new { Message = "NOPE" }); } } diff --git a/tests/Resulteles.Tests/ResultValueTests.cs b/tests/Resulteles.Tests/ResultValueTests.cs new file mode 100644 index 0000000..81860e6 --- /dev/null +++ b/tests/Resulteles.Tests/ResultValueTests.cs @@ -0,0 +1,115 @@ +namespace Resulteles.Tests; + +public class ResultValueTests +{ + public class TestException : Exception + { + public TestException(string message) : base(message) { } + } + + [Test] + public void ShouldGetValueOrThrowReturnValueWhenIsOk() + { + var result = Result.Ok(42); + var value = result.GetValueOrThrow(); + value.Should().Be(42); + } + + [Test] + public void ShouldGetValueOrThrowRaiseAnExceptionWhenIsError() + { + var result = Result.Error("Err"); + var action = () => result.GetValueOrThrow(); + action.Should().Throw().WithMessage("Err"); + } + + [Test] + public void ShouldGetValueOrThrowRaiseAFormattedExceptionWhenIsError() + { + var result = Result.Error("Err"); + var action = () => result.GetValueOrThrow(e => e.ToUpper()); + action.Should().Throw().WithMessage("ERR"); + } + + [Test] + public void ShouldGetValueOrThrowRaiseACustomExceptionWhenIsError() + { + var result = Result.Error("Err"); + var action = () => result.GetValueOrThrow(e => new TestException(e)); + action.Should().Throw().WithMessage("Err"); + } + + [Test] + public void ShouldThrowIfErrorRaiseExceptionOnError() + { + var result = Result.Error("Err"); + var action = () => result.ThrowIfError(); + action.Should().Throw().WithMessage("Err"); + } + + [Test] + public void ShouldThrowIfErrorRaiseFormattedExceptionOnError() + { + var result = Result.Error("Err"); + var action = () => result.ThrowIfError(e => e.ToUpper()); + action.Should().Throw().WithMessage("ERR"); + } + + [Test] + public void ShouldThrowIfErrorRaiseCustomExceptionOnError() + { + var result = Result.Error("Err"); + var action = () => result.ThrowIfError(e => new TestException(e)); + action.Should().Throw().WithMessage("ERR"); + } + + [Test] + public void ShouldReturnOkValueWhenDefaultValueOnOkResult() + { + var result = Result.Ok(42).DefaultValue(1); + result.Should().Be(42); + } + + [Test] + public void ShouldReturnDefaultValueWhenDefaultValueOnErrorResult() + { + var result = Result.Error("Error").DefaultValue(1); + result.Should().Be(1); + } + + [Test] + public void ShouldReturnOkValueWhenDefautlWithOnOkResult() + { + var result = Result.Ok(42).DefaultWith(_ => 1); + result.Should().Be(42); + } + + [Test] + public void ShouldReturnDefaultValueWhenDefaultWithOnErrorResult() + { + var result = Result.Error("1").DefaultWith(int.Parse); + result.Should().Be(1); + } + + [Test] + public void ShouldReturnNullableValueTypeOkToNullable() + { + var result = Result.Ok(42).ToNullable(); + result.Should().Be(new int?(42)); + } + + [Test] + public void ShouldReturnNullableValueTypeErrorToNullable() + { + var result = Result.Error("").ToNullable(); + result.Should().Be(null); + } + + [Test] + public void ShouldTapValue() + { + var tappedValue = 0; + Result.Ok(42).Tap(v => tappedValue = v); + tappedValue.Should().Be(42); + } +} diff --git a/tests/Resulteles.Tests/Setup.cs b/tests/Resulteles.Tests/Setup.cs new file mode 100644 index 0000000..afcf19e --- /dev/null +++ b/tests/Resulteles.Tests/Setup.cs @@ -0,0 +1,39 @@ +using FsCheck; + +namespace Resulteles.Tests; + +public record OkResult(Result Item); + +public record ErrorResult(Result Item); + +public class Generators +{ + protected Generators() { } + + public static Arbitrary> GenerateResult() => + Gen.OneOf( + Arb.From().Generator.Select(Result.Ok), + Arb.From().Generator.Select(Result.Error) + ) + .ToArbitrary(); + + public static Arbitrary> GenerateOkResult() => + Arb.From().Generator.Select(v => new OkResult(Result.Ok(v))).ToArbitrary(); + + public static Arbitrary> GenerateErrorResult() => + Arb.From().Generator.Select(v => new ErrorResult(Result.Error(v))) + .ToArbitrary(); +} + +[Serializable, AttributeUsage(AttributeTargets.Method)] +public sealed class PropertyTestAttribute : FsCheck.NUnit.PropertyAttribute +{ + public PropertyTestAttribute() => QuietOnSuccess = true; +} + +[SetUpFixture] +public class SetupFixture +{ + [OneTimeSetUp] + public void OneTimeSetup() => FsCheck.Arb.Register(); +}