From ca9f290d44a0036de6fec7ffb98762ad5cf123be Mon Sep 17 00:00:00 2001 From: Louis Seubert Date: Sun, 14 Apr 2024 17:42:13 +0200 Subject: [PATCH] feat: initial project commit --- .editorconfig | 443 ++++++++++ .forgejo/workflows/default.yml | 29 + .gitignore | 478 +++++++++++ Directory.Build.props | 24 + Directory.Build.targets | 3 + Directory.Packages.props | 13 + Geekeey.Common.Results.sln | 30 + global.json | 9 + nuget.config | 19 + src/Results.Tests/ErrorTests.cs | 30 + .../ExtensionsEnumerableTests.cs | 54 ++ src/Results.Tests/Fixtures/CustomTestError.cs | 7 + .../Fixtures/CustomTestException.cs | 5 + src/Results.Tests/PreludeTests.cs | 119 +++ src/Results.Tests/ResultConversionTests.cs | 66 ++ src/Results.Tests/ResultEqualityTests.cs | 173 ++++ src/Results.Tests/ResultMatchingTests.cs | 183 +++++ src/Results.Tests/ResultTests.cs | 63 ++ src/Results.Tests/ResultTransformTests.cs | 756 ++++++++++++++++++ src/Results.Tests/ResultUnboxTests.cs | 113 +++ src/Results.Tests/Results.Tests.csproj | 25 + src/Results/Errors/AggregateError.cs | 16 + src/Results/Errors/Error.cs | 33 + src/Results/Errors/ExceptionError.cs | 18 + src/Results/Errors/InvalidOperationError.cs | 11 + src/Results/Errors/NotFoundError.cs | 17 + src/Results/Errors/StringError.cs | 11 + src/Results/Exceptions/UnwrapException.cs | 18 + .../Extensions/Extensions.Enumerable.cs | 34 + src/Results/Extensions/Extensions.Task.cs | 48 ++ src/Results/Prelude.cs | 92 +++ src/Results/Project.props | 5 + src/Results/Result.Conversion.cs | 36 + src/Results/Result.Equality.cs | 115 +++ src/Results/Result.Matching.cs | 91 +++ src/Results/Result.Transform.cs | 335 ++++++++ src/Results/Result.Unbox.cs | 63 ++ src/Results/Result.cs | 76 ++ src/Results/Results.csproj | 14 + src/Results/package-readme.md | 2 + 40 files changed, 3677 insertions(+) create mode 100644 .editorconfig create mode 100644 .forgejo/workflows/default.yml create mode 100644 .gitignore create mode 100644 Directory.Build.props create mode 100644 Directory.Build.targets create mode 100644 Directory.Packages.props create mode 100644 Geekeey.Common.Results.sln create mode 100644 global.json create mode 100644 nuget.config create mode 100644 src/Results.Tests/ErrorTests.cs create mode 100644 src/Results.Tests/ExtensionsEnumerableTests.cs create mode 100644 src/Results.Tests/Fixtures/CustomTestError.cs create mode 100644 src/Results.Tests/Fixtures/CustomTestException.cs create mode 100644 src/Results.Tests/PreludeTests.cs create mode 100644 src/Results.Tests/ResultConversionTests.cs create mode 100644 src/Results.Tests/ResultEqualityTests.cs create mode 100644 src/Results.Tests/ResultMatchingTests.cs create mode 100644 src/Results.Tests/ResultTests.cs create mode 100644 src/Results.Tests/ResultTransformTests.cs create mode 100644 src/Results.Tests/ResultUnboxTests.cs create mode 100644 src/Results.Tests/Results.Tests.csproj create mode 100644 src/Results/Errors/AggregateError.cs create mode 100644 src/Results/Errors/Error.cs create mode 100644 src/Results/Errors/ExceptionError.cs create mode 100644 src/Results/Errors/InvalidOperationError.cs create mode 100644 src/Results/Errors/NotFoundError.cs create mode 100644 src/Results/Errors/StringError.cs create mode 100644 src/Results/Exceptions/UnwrapException.cs create mode 100644 src/Results/Extensions/Extensions.Enumerable.cs create mode 100644 src/Results/Extensions/Extensions.Task.cs create mode 100644 src/Results/Prelude.cs create mode 100644 src/Results/Project.props create mode 100644 src/Results/Result.Conversion.cs create mode 100644 src/Results/Result.Equality.cs create mode 100644 src/Results/Result.Matching.cs create mode 100644 src/Results/Result.Transform.cs create mode 100644 src/Results/Result.Unbox.cs create mode 100644 src/Results/Result.cs create mode 100644 src/Results/Results.csproj create mode 100644 src/Results/package-readme.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b6b8e92 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,443 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 +tab_width = 4 +end_of_line = lf +insert_final_newline = false +trim_trailing_whitespace = true + +[.forgejo/workflows/*.yml] +indent_size = 2 +indent_style = space + +[*.{xml,csproj,props,targets}] +indent_size = 2 +indent_style = space + +[nuget.config] +indent_size = 2 +indent_style = space + +[*.{cs,vb}] #### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = true +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_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_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +[*.cs] #### C# Coding Conventions #### + +# var preferences +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:silent + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# 'namespace' preferences +csharp_style_namespace_declarations = file_scoped:warning + +[*.cs] #### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +[*.{cs,vb}] #### .NET Naming styles #### + +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types_and_namespaces.required_modifiers = + + +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion +dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interfaces.required_modifiers = + + +dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums +dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.enums.applicable_kinds = enum +dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enums.required_modifiers = + + +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion +dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase + +dotnet_naming_symbols.type_parameters.applicable_kinds = namespace +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters.required_modifiers = + + +dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods +dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.methods.required_modifiers = + + +dotnet_naming_rule.async_methods_end_in_async.severity = suggestion +dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods +dotnet_naming_rule.async_methods_end_in_async.style = end_in_async + +dotnet_naming_symbols.any_async_methods.applicable_kinds = method +dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * +dotnet_naming_symbols.any_async_methods.required_modifiers = async + + +dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion +dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters +dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase + +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.parameters.applicable_accessibilities = * +dotnet_naming_symbols.parameters.required_modifiers = + + +dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties +dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.properties.required_modifiers = + + +dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.events_should_be_pascalcase.symbols = events +dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.events.applicable_kinds = event +dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.events.required_modifiers = + +# local + +dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables +dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase + +dotnet_naming_symbols.local_variables.applicable_kinds = local +dotnet_naming_symbols.local_variables.applicable_accessibilities = local +dotnet_naming_symbols.local_variables.required_modifiers = + + +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function +dotnet_naming_symbols.local_functions.applicable_accessibilities = * +dotnet_naming_symbols.local_functions.required_modifiers = + + +dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.applicable_accessibilities = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +# private + +dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + + +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static + + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static + + +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.private_constant_fields.applicable_kinds = field +dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_constant_fields.required_modifiers = const + +# public + +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_fields.required_modifiers = + + +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static + + +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.public_constant_fields.applicable_kinds = field +dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_constant_fields.required_modifiers = const + +# others + +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + + +# Naming styles + +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case + +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case + +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_style.s_camelcase.required_prefix = s_ +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.capitalization = camel_case + +dotnet_naming_style.end_in_async.required_prefix = +dotnet_naming_style.end_in_async.required_suffix = Async +dotnet_naming_style.end_in_async.capitalization = pascal_case +dotnet_naming_style.end_in_async.word_separator = + +[*.{cs.vb}] +dotnet_diagnostic.IDE0055.severity = error +# IDE0051: Remove unused private member +dotnet_diagnostic.IDE0051.severity = error +# IDE0052: Remove unread private member +dotnet_diagnostic.IDE0052.severity = error +# IDE0064: Make struct fields writable +dotnet_diagnostic.IDE0064.severity = error + +dotnet_analyzer_diagnostic.severity = error +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = suggestion +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = error +# CA1304: Specify CultureInfo +dotnet_diagnostic.CA1304.severity = warning +# CA1802: Use literals where appropriate +dotnet_diagnostic.CA1802.severity = warning +# CA1813: Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = error +# CA1815: Override equals and operator equals on value types +dotnet_diagnostic.CA1815.severity = warning +# CA1820: Test for empty strings using string length +dotnet_diagnostic.CA1820.severity = warning +# CA1821: Remove empty finalizers +dotnet_diagnostic.CA1821.severity = warning +# CA1822: Mark members as static +dotnet_diagnostic.CA1822.severity = suggestion +# CA1823: Avoid unused private fields +dotnet_diagnostic.CA1823.severity = warning +dotnet_code_quality.CA1822.api_surface = private, internal +# CA1825: Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = warning +# CA1826: Use property instead of Linq Enumerable method +dotnet_diagnostic.CA1826.severity = suggestion +# CA1827: Do not use Count/LongCount when Any can be used +dotnet_diagnostic.CA1827.severity = warning +# CA1828: Do not use CountAsync/LongCountAsync when AnyAsync can be used +dotnet_diagnostic.CA1828.severity = warning +# CA1829: Use Length/Count property instead of Enumerable.Count method +dotnet_diagnostic.CA1829.severity = warning +#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters +dotnet_diagnostic.CA1847.severity = warning +#CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method +dotnet_diagnostic.CA1854.severity = warning +#CA2211:Non-constant fields should not be visible +dotnet_diagnostic.CA2211.severity = error diff --git a/.forgejo/workflows/default.yml b/.forgejo/workflows/default.yml new file mode 100644 index 0000000..359a76b --- /dev/null +++ b/.forgejo/workflows/default.yml @@ -0,0 +1,29 @@ +name: dotnet publish +on: + push: + tags: [ '[0-9]+.[0-9]+.[0-9]+' ] + branches: [ 'main', 'develop' ] + +jobs: + package: + runs-on: debian-latest + strategy: + matrix: + dotnet-version: [ '8.0' ] + container: mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet-version }} + steps: + - uses: https://git.geekeey.de/actions/checkout@1 + - name: NuGet login + shell: bash + run: | + # This token is readonly and can only be used for restore + dotnet nuget update source geekeey --store-password-in-clear-text \ + --username "${{ github.actor }}" --password "${{ github.token }}" + - name: .NET pack + shell: bash + run: | + dotnet pack -p:ContinuousIntegrationBuild=true + - name: .NET test + shell: bash + run: | + dotnet test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ee88cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,478 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +!**/src/packages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..e471787 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,24 @@ + + + + true + + + + net8.0 + enable + enable + + + + The Geekeey Team + A simple and lightweight result type implementation for C#. + geekeey utility result + package-readme.md + + + + Geekeey.Common.$(MSBuildProjectName) + Geekeey.Common.$(MSBuildProjectName) + + \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..0c98d16 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..8edaec5 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,13 @@ + + + + true + + + + + + + + + \ No newline at end of file diff --git a/Geekeey.Common.Results.sln b/Geekeey.Common.Results.sln new file mode 100644 index 0000000..7e0a70f --- /dev/null +++ b/Geekeey.Common.Results.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Results", "src\Results\Results.csproj", "{5F1B824C-659D-4A1A-B538-862788681437}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Results.Tests", "src\Results.Tests\Results.Tests.csproj", "{1DC64E48-B1AF-4426-A336-65F89AD10591}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5F1B824C-659D-4A1A-B538-862788681437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F1B824C-659D-4A1A-B538-862788681437}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F1B824C-659D-4A1A-B538-862788681437}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F1B824C-659D-4A1A-B538-862788681437}.Release|Any CPU.Build.0 = Release|Any CPU + {1DC64E48-B1AF-4426-A336-65F89AD10591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DC64E48-B1AF-4426-A336-65F89AD10591}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DC64E48-B1AF-4426-A336-65F89AD10591}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DC64E48-B1AF-4426-A336-65F89AD10591}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000..6a12de5 --- /dev/null +++ b/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + }, + "msbuild-sdks": { + } +} \ No newline at end of file diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..8ceffa3 --- /dev/null +++ b/nuget.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Results.Tests/ErrorTests.cs b/src/Results.Tests/ErrorTests.cs new file mode 100644 index 0000000..098699b --- /dev/null +++ b/src/Results.Tests/ErrorTests.cs @@ -0,0 +1,30 @@ +namespace Geekeey.Common.Results.Tests; + +[TestFixture] +internal sealed class ErrorTests +{ + [Test] + public void ImplicitConversion_FromString_ReturnsStringError() + { + Error error = "error"; + + Assert.Multiple(() => + { + Assert.That(error, Is.InstanceOf()); + Assert.That(error.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void ImplicitConversion_FromException_ReturnsExceptionError() + { + Error error = new CustomTestException(); + + Assert.Multiple(() => + { + Assert.That(error, Is.InstanceOf()); + var instance = error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } +} \ No newline at end of file diff --git a/src/Results.Tests/ExtensionsEnumerableTests.cs b/src/Results.Tests/ExtensionsEnumerableTests.cs new file mode 100644 index 0000000..d5c597a --- /dev/null +++ b/src/Results.Tests/ExtensionsEnumerableTests.cs @@ -0,0 +1,54 @@ +namespace Geekeey.Common.Results.Tests; + +[TestFixture] +internal sealed class ExtensionsEnumerableTests +{ + [Test] + public void Join_ReturnsAllSuccess_ForSequenceContainingAllSuccess() + { + IEnumerable> xs = [1, 2, 3, 4, 5]; + + var result = xs.Join(); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EquivalentTo(new[] { 1, 2, 3, 4, 5 })); + }); + } + + [Test] + public void Join_ReturnsFirstFailure_ForSequenceContainingFailure() + { + IEnumerable> xs = + [ + Success(1), + Success(2), + Failure("error 1"), + Success(4), + Failure("error 2") + ]; + + var result = xs.Join(); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error 1")); + }); + } + + [Test] + public void Join_ReturnsSuccess_ForEmptySequence() + { + IEnumerable> xs = []; + + var result = xs.Join(); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.Empty); + }); + } +} \ No newline at end of file diff --git a/src/Results.Tests/Fixtures/CustomTestError.cs b/src/Results.Tests/Fixtures/CustomTestError.cs new file mode 100644 index 0000000..0138bef --- /dev/null +++ b/src/Results.Tests/Fixtures/CustomTestError.cs @@ -0,0 +1,7 @@ +namespace Geekeey.Common.Results.Tests; + +internal sealed class CustomTestError : Error +{ + internal const string DefaultMessage = "This is a custom error for test"; + public override string Message => DefaultMessage; +} \ No newline at end of file diff --git a/src/Results.Tests/Fixtures/CustomTestException.cs b/src/Results.Tests/Fixtures/CustomTestException.cs new file mode 100644 index 0000000..0adcff3 --- /dev/null +++ b/src/Results.Tests/Fixtures/CustomTestException.cs @@ -0,0 +1,5 @@ +namespace Geekeey.Common.Results.Tests; + +internal sealed class CustomTestException : Exception +{ +} \ No newline at end of file diff --git a/src/Results.Tests/PreludeTests.cs b/src/Results.Tests/PreludeTests.cs new file mode 100644 index 0000000..96c8f43 --- /dev/null +++ b/src/Results.Tests/PreludeTests.cs @@ -0,0 +1,119 @@ +namespace Geekeey.Common.Results.Tests; + +[TestFixture] +internal sealed class PreludeTests +{ + [Test] + public void Try_ReturnsSuccess_WithoutThrowing() + { + var result = Try(() => 2); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo(2)); + }); + } + + [Test] + public void Try_ReturnsFailure_WithThrowing() + { + var result = Try(() => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task TryAsync_ReturnsSuccess_WithoutThrowing_Task() + { + var result = await TryAsync(() => Task.FromResult(2)); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo(2)); + }); + } + + [Test] + public async Task TryAsync_ReturnsFailure_WithThrowing_Task() + { + var result = await TryAsync(Task () => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task TryAsync_ReturnsFailure_WithAwaitThrowing_Task() + { + var result = await TryAsync(async Task () => + { + await Task.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task TryAsync_ReturnsSuccess_WithoutThrowing_ValueTask() + { + var result = await TryAsync(() => ValueTask.FromResult(2)); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo(2)); + }); + } + + [Test] + public async Task TryAsync_ReturnsFailure_WithThrowing_ValueTask() + { + var result = await TryAsync(ValueTask () => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task TryAsync_ReturnsFailure_WithAwaitThrowing_ValueTask() + { + var result = await TryAsync(async ValueTask () => + { + await Task.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } +} \ No newline at end of file diff --git a/src/Results.Tests/ResultConversionTests.cs b/src/Results.Tests/ResultConversionTests.cs new file mode 100644 index 0000000..7c3fe49 --- /dev/null +++ b/src/Results.Tests/ResultConversionTests.cs @@ -0,0 +1,66 @@ +namespace Geekeey.Common.Results.Tests; + +[TestFixture] +internal sealed class ResultConversionTests +{ + [Test] + public void ImplicitConversion_FromValue_IsSuccess() + { + var result = Success(2); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsFailure, Is.False); + Assert.That(result.Value, Is.EqualTo(2)); + }); + } + + [Test] + public void ImplicitConversion_FromError_IsFailure() + { + var error = new CustomTestError(); + var result = Failure(error); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.IsFailure, Is.True); + Assert.That(result.Error, Is.InstanceOf()); + }); + } + + [Test] + public void Unwrap_ReturnsValue_ForSuccess() + { + var result = Success(2); + var value = result.Unwrap(); + + Assert.That(value, Is.EqualTo(2)); + } + + [Test] + public void Unwrap_Throws_ForFailure() + { + var result = Failure("error"); + + Assert.That(() => result.Unwrap(), Throws.InstanceOf()); + } + + [Test] + public void ExplicitConversion_ReturnsValue_ForSuccess() + { + var result = Success(2); + var value = (int)result; + + Assert.That(value, Is.EqualTo(2)); + } + + [Test] + public void ExplicitConversion_Throws_ForFailure() + { + var result = Failure("error"); + + Assert.That(() => (int)result, Throws.InstanceOf()); + } +} \ No newline at end of file diff --git a/src/Results.Tests/ResultEqualityTests.cs b/src/Results.Tests/ResultEqualityTests.cs new file mode 100644 index 0000000..ac35c29 --- /dev/null +++ b/src/Results.Tests/ResultEqualityTests.cs @@ -0,0 +1,173 @@ +namespace Geekeey.Common.Results.Tests; + +[TestFixture] +internal sealed class ResultEqualityTests +{ + [Test] + public void Equals_T_ReturnsTrue_ForSuccessAndEqualValue() + { + var a = Success(2); + var b = 2; + + Assert.That(a.Equals(b), Is.True); + } + + [Test] + public void Equals_T_ReturnsFalse_ForSuccessAndUnequalValue() + { + var a = Success(2); + var b = 3; + + Assert.That(a.Equals(b), Is.False); + } + + [Test] + public void Equals_T_ReturnsFalse_ForFailure() + { + var a = Failure("error"); + var b = 2; + + Assert.That(a.Equals(b), Is.False); + } + + [Test] + public void Equals_Result_ReturnsTrue_ForSuccessAndSuccessAndEqualValue() + { + var a = Success(2); + var b = Success(2); + + Assert.That(a.Equals(b), Is.True); + } + + [Test] + public void Equals_Result_ReturnsFalse_ForSuccessAndSuccessAndUnequalValue() + { + var a = Success(2); + var b = Success(3); + + Assert.That(a.Equals(b), Is.False); + } + + [Test] + public void Equals_Result_ReturnsFalse_ForSuccessAndFailure() + { + var a = Success(2); + var b = Failure("error 1"); + + Assert.That(a.Equals(b), Is.False); + } + + [Test] + public void Equals_Result_ReturnsFalse_ForFailureAndSuccess() + { + var a = Failure("error"); + var b = Success(2); + + Assert.That(a.Equals(b), Is.False); + } + + [Test] + public void Equals_Result_ReturnsTrue_ForFailureAndFailure() + { + var a = Failure("error 1"); + var b = Failure("error 2"); + + Assert.That(a.Equals(b), Is.True); + } + + [Test] + public void Equals_T_ReturnsTrue_ForSuccessAndEqualValue_WithComparer() + { + var a = Success(2); + var b = 2; + + Assert.That(a.Equals(b, EqualityComparer.Default), Is.True); + } + + [Test] + public void Equals_T_ReturnsFalse_ForSuccessAndUnequalValue_WithComparer() + { + var a = Success(2); + var b = 3; + + Assert.That(a.Equals(b, EqualityComparer.Default), Is.False); + } + + [Test] + public void Equals_T_ReturnsFalse_ForFailure_WithComparer() + { + var a = Failure("error"); + var b = 2; + + Assert.That(a.Equals(b, EqualityComparer.Default), Is.False); + } + + [Test] + public void Equals_Result_ReturnsTrue_ForSuccessAndSuccessAndEqualValue_WithComparer() + { + var a = Success(2); + var b = Success(2); + + Assert.That(a.Equals(b, EqualityComparer.Default), Is.True); + } + + [Test] + public void Equals_Result_ReturnsFalse_ForSuccessAndSuccessAndUnequalValue_WithComparer() + { + var a = Success(2); + var b = Success(3); + + Assert.That(a.Equals(b, EqualityComparer.Default), Is.False); + } + + [Test] + public void Equals_Result_ReturnsFalse_ForSuccessAndFailure_WithComparer() + { + var a = Success(2); + var b = Failure("error 1"); + + Assert.That(a.Equals(b, EqualityComparer.Default), Is.False); + } + + [Test] + public void Equals_Result_ReturnsFalse_ForFailureAndSuccess_WithComparer() + { + var a = Failure("error"); + var b = Success(2); + + Assert.That(a.Equals(b, EqualityComparer.Default), Is.False); + } + + [Test] + public void Equals_Result_ReturnsTrue_ForFailureAndFailure_WithComparer() + { + var a = Failure("error 1"); + var b = Failure("error 2"); + + Assert.That(a.Equals(b, EqualityComparer.Default), Is.True); + } + + [Test] + public void GetHashCode_ReturnsHashCode_ForSuccess() + { + var result = Success(2); + + Assert.That(result.GetHashCode(), Is.EqualTo(2.GetHashCode())); + } + + [Test] + public void GetHashCode_Returns_Zero_ForNull() + { + var result = Success(null); + + Assert.That(result.GetHashCode(), Is.Zero); + } + + [Test] + public void GetHashCode_Returns_Zero_ForFailure() + { + var result = Failure("error"); + + Assert.That(result.GetHashCode(), Is.Zero); + } +} \ No newline at end of file diff --git a/src/Results.Tests/ResultMatchingTests.cs b/src/Results.Tests/ResultMatchingTests.cs new file mode 100644 index 0000000..db93a0c --- /dev/null +++ b/src/Results.Tests/ResultMatchingTests.cs @@ -0,0 +1,183 @@ +namespace Geekeey.Common.Results.Tests; + +[TestFixture] +internal sealed class ResultMatchingTests +{ + [Test] + public void Match_CallsSuccessFunc_ForSuccess() + { + var result = Success(2); + var match = result.Match( + v => v, + _ => throw new InvalidOperationException()); + + Assert.That(match, Is.EqualTo(2)); + } + + [Test] + public void Match_CallsFailureFunc_ForFailure() + { + var result = Failure("error"); + var match = result.Match( + _ => throw new InvalidOperationException(), + e => e); + + Assert.That(match.Message, Is.EqualTo("error")); + } + + [Test] + public void Switch_CallsSuccessAction_ForSuccess() + { + var called = false; + + var result = Success(2); + result.Switch( + v => + { + Assert.That(v, Is.EqualTo(2)); + called = true; + }, + _ => throw new InvalidOperationException() + ); + + Assert.That(called, Is.True); + } + + [Test] + public void Switch_CallsFailureAction_ForFailure() + { + var called = false; + + var result = Failure("error"); + result.Switch( + _ => throw new InvalidOperationException(), + e => + { + called = true; + Assert.That(e.Message, Is.EqualTo("error")); + } + ); + + Assert.That(called, Is.True); + } + + [Test] + public async Task MatchAsync_CallsSuccessFunc_ForSuccess_Task() + { + var result = Success(2); + var match = await result.MatchAsync( + Task.FromResult, + _ => throw new InvalidOperationException()); + + Assert.That(match, Is.EqualTo(2)); + } + + [Test] + public async Task MatchAsync_CallsFailureFunc_ForFailure_Task() + { + var result = Failure("error"); + var match = await result.MatchAsync( + _ => throw new InvalidOperationException(), + Task.FromResult); + + Assert.That(match.Message, Is.EqualTo("error")); + } + + [Test] + public async Task SwitchAsync_CallsSuccessAction_ForSuccess_Task() + { + var called = false; + + var result = Success(2); + await result.SwitchAsync( + v => + { + Assert.That(v, Is.EqualTo(2)); + called = true; + return Task.CompletedTask; + }, + _ => throw new InvalidOperationException() + ); + + Assert.That(called, Is.True); + } + + [Test] + public async Task SwitchAsync_CallsFailureAction_ForFailure_Task() + { + var called = false; + + var result = Failure("error"); + await result.SwitchAsync( + _ => throw new InvalidOperationException(), + e => + { + called = true; + Assert.That(e.Message, Is.EqualTo("error")); + return Task.CompletedTask; + } + ); + + Assert.That(called, Is.True); + } + + [Test] + public async Task MatchAsync_CallsSuccessFunc_ForSuccess_ValueTask() + { + var result = Success(2); + var match = await result.MatchAsync( + ValueTask.FromResult, + _ => throw new InvalidOperationException()); + + Assert.That(match, Is.EqualTo(2)); + } + + [Test] + public async Task MatchAsync_CallsFailureFunc_ForFailure_ValueTask() + { + var result = Failure("error"); + var match = await result.MatchAsync( + _ => throw new InvalidOperationException(), + ValueTask.FromResult); + + Assert.That(match.Message, Is.EqualTo("error")); + } + + [Test] + public async Task SwitchAsync_CallsSuccessAction_ForSuccess_ValueTask() + { + var called = false; + + var result = Success(2); + await result.SwitchAsync( + v => + { + Assert.That(v, Is.EqualTo(2)); + called = true; + return ValueTask.CompletedTask; + }, + _ => throw new InvalidOperationException() + ); + + Assert.That(called, Is.True); + } + + [Test] + public async Task SwitchAsync_CallsFailureAction_ForFailure_ValueTask() + { + var called = false; + + var result = Failure("error"); + await result.SwitchAsync( + _ => throw new InvalidOperationException(), + e => + { + called = true; + Assert.That(e.Message, Is.EqualTo("error")); + return ValueTask.CompletedTask; + } + ); + + Assert.That(called, Is.True); + } +} \ No newline at end of file diff --git a/src/Results.Tests/ResultTests.cs b/src/Results.Tests/ResultTests.cs new file mode 100644 index 0000000..bdca827 --- /dev/null +++ b/src/Results.Tests/ResultTests.cs @@ -0,0 +1,63 @@ +namespace Geekeey.Common.Results.Tests; + +[TestFixture] +internal sealed class ResultTests +{ + [Test] + public void New_T_HasValue() + { + var result = new Result(1); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsFailure, Is.False); + Assert.That(result.Value, Is.Not.EqualTo(default(int))); + Assert.That(result.Error, Is.Null); + }); + } + + [Test] + public void New_Error_HasError() + { + var result = new Result(new CustomTestError()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.IsFailure, Is.True); + Assert.That(result.Value, Is.EqualTo(default(int))); + Assert.That(result.Error, Is.InstanceOf()); + }); + } + + [Test] + public void Default_IsDefault() + { + var result = default(Result); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.IsFailure, Is.True); + Assert.That(result.Value, Is.EqualTo(default(int))); + Assert.That(result.Error, Is.EqualTo(Error.DefaultValueError)); + }); + } + + [Test] + public void ToString_ReturnsSuccessString() + { + Result result = 2; + + Assert.That(result.ToString(), Is.EqualTo("Success { 2 }")); + } + + [Test] + public void ToString_ReturnsFailureString() + { + Result result = new StringError("error"); + + Assert.That(result.ToString(), Is.EqualTo("Failure { error }")); + } +} \ No newline at end of file diff --git a/src/Results.Tests/ResultTransformTests.cs b/src/Results.Tests/ResultTransformTests.cs new file mode 100644 index 0000000..ce38eb7 --- /dev/null +++ b/src/Results.Tests/ResultTransformTests.cs @@ -0,0 +1,756 @@ +namespace Geekeey.Common.Results.Tests; + +[TestFixture] +internal sealed class ResultTransformTests +{ + [Test] + public void Map_ReturnsSuccess_ForSuccess() + { + var start = Success(2); + var result = start.Map(value => value.ToString()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public void Map_ReturnsFailure_ForFailure() + { + var start = Failure("error"); + var result = start.Map(value => value.ToString()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void Then_ReturnsSuccess_ForSuccessAndMappingReturningSuccess() + { + var start = Success(2); + var result = start.Then(value => Success(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public void Then_ReturnsFailure_ForSuccessAndMappingReturningFailure() + { + var start = Success(2); + var result = start.Then(_ => Failure("error")); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void Then_ReturnsFailure_ForFailureAndMappingReturningSuccess() + { + var start = Failure("error"); + var result = start.Then(value => Success(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void Then_ReturnsFailure_ForFailureAndMappingReturningFailure() + { + var start = Failure("error"); + var result = start.Then(_ => Failure("error 2")); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void TryMap_ReturnsSuccess_ForSuccessWithoutThrowing() + { + var start = Success(2); + var result = start.TryMap(value => value.ToString()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public void TryMap_ReturnsFailure_ForFailureWithoutThrowing() + { + var start = Failure("error"); + var result = start.TryMap(value => value.ToString()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void TryMap_ReturnsFailure_ForSuccessWithThrowing() + { + var start = Success(2); + var result = start.TryMap(_ => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public void TryMap_ReturnsFailure_ForFailureWithThrowing() + { + var start = Failure("error"); + var result = start.TryMap(_ => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void ThenTry_ReturnsSuccess_ForSuccessAndMappingReturningSuccess() + { + var start = Success(2); + var result = start.ThenTry(value => Success(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public void ThenTry_ReturnsFailure_ForSuccessAndMappingReturningFailure() + { + var start = Success(2); + var result = start.ThenTry(_ => Failure("error")); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void ThenTry_ReturnsFailure_ForFailureAndMappingReturningFailure() + { + var start = Failure("error"); + var result = start.ThenTry(x => Success(x.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void ThenTry_ReturnsFailure_ForSuccessAndMappingThrowing() + { + var start = Success(2); + var result = start.ThenTry(_ => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public void ThenTry_ReturnsFailure_ForFailureAndMappingThrowing() + { + var start = Failure("error"); + var result = start.ThenTry(_ => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task MapAsync_ReturnsSuccess_ForSuccess_Task() + { + var start = Success(2); + var result = await start.MapAsync(value => Task.FromResult(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public async Task MapAsync_ReturnsFailure_ForFailure_Task() + { + var start = Failure("error"); + var result = await start.MapAsync(value => Task.FromResult(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenAsync_ReturnsSuccess_ForSuccessAndMappingReturningSuccess_Task() + { + var start = Success(2); + var result = await start.ThenAsync(value => Task.FromResult(Success(value.ToString()))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public async Task ThenAsync_ReturnsFailure_ForSuccessAndMappingReturningFailure_Task() + { + var start = Success(2); + var result = await start.ThenAsync(_ => Task.FromResult(Failure("error"))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenAsync_ReturnsFailure_ForFailureAndMappingReturningSuccess_Task() + { + var start = Failure("error"); + var result = await start.ThenAsync(value => Task.FromResult(Success(value.ToString()))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenAsync_ReturnsFailure_ForFailureAndMappingReturningFailure_Task() + { + var start = Failure("error"); + var result = await start.ThenAsync(_ => Task.FromResult(Failure("error 2"))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsSuccess_ForSuccessWithoutThrowing_Task() + { + var start = Success(2); + var result = await start.TryMapAsync(value => Task.FromResult(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForFailureWithoutThrowing_Task() + { + var start = Failure("error"); + var result = await start.TryMapAsync(value => Task.FromResult(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForSuccessWithThrowing_Task() + { + var start = Success(2); + var result = await start.TryMapAsync(Task (_) => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForSuccessWithAwaitThrowing_Task() + { + var start = Success(2); + var result = await start.TryMapAsync(async Task (_) => + { + await Task.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForFailureWithThrowing_Task() + { + var start = Failure("error"); + var result = await start.TryMapAsync(Task (_) => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForFailureWithAwaitThrowing_Task() + { + var start = Failure("error"); + var result = await start.TryMapAsync(async Task (_) => + { + await Task.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsSuccess_ForSuccessAndMappingReturningSuccess_Task() + { + var start = Success(2); + var result = await start.ThenTryAsync(value => Task.FromResult(Success(value.ToString()))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingReturningFailure_Task() + { + var start = Success(2); + var result = await start.ThenTryAsync(_ => Task.FromResult(Failure("error"))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingReturningFailure_Task() + { + var start = Failure("error"); + var result = await start.ThenTryAsync(x => Task.FromResult(Success(x.ToString()))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingThrowing_Task() + { + var start = Success(2); + var result = await start.ThenTryAsync(Task> (_) => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingAwaitThrowing_Task() + { + var start = Success(2); + var result = await start.ThenTryAsync(async Task> (_) => + { + await Task.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingThrowing_Task() + { + var start = Failure("error"); + var result = await start.ThenTryAsync(Task> (_) => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingAwaitThrowing_Task() + { + var start = Failure("error"); + var result = await start.ThenTryAsync(async Task> (_) => + { + await Task.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task MapAsync_ReturnsSuccess_ForSuccess_ValueTask() + { + var start = Success(2); + var result = await start.MapAsync(value => ValueTask.FromResult(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public async Task MapAsync_ReturnsFailure_ForFailure_ValueTask() + { + var start = Failure("error"); + var result = await start.MapAsync(value => ValueTask.FromResult(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenAsync_ReturnsSuccess_ForSuccessAndMappingReturningSuccess_ValueTask() + { + var start = Success(2); + var result = await start.ThenAsync(value => ValueTask.FromResult(Success(value.ToString()))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public async Task ThenAsync_ReturnsFailure_ForSuccessAndMappingReturningFailure_ValueTask() + { + var start = Success(2); + var result = await start.ThenAsync(_ => ValueTask.FromResult(Failure("error"))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenAsync_ReturnsFailure_ForFailureAndMappingReturningSuccess_ValueTask() + { + var start = Failure("error"); + var result = await start.ThenAsync(value => ValueTask.FromResult(Success(value.ToString()))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenAsync_ReturnsFailure_ForFailureAndMappingReturningFailure_ValueTask() + { + var start = Failure("error"); + var result = await start.ThenAsync(_ => ValueTask.FromResult(Failure("error 2"))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsSuccess_ForSuccessWithoutThrowing_ValueTask() + { + var start = Success(2); + var result = await start.TryMapAsync(value => ValueTask.FromResult(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForFailureWithoutThrowing_ValueTask() + { + var start = Failure("error"); + var result = await start.TryMapAsync(value => ValueTask.FromResult(value.ToString())); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForSuccessWithThrowing_ValueTask() + { + var start = Success(2); + var result = await start.TryMapAsync(ValueTask (_) => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForSuccessWithAwaitThrowing_ValueTask() + { + var start = Success(2); + var result = await start.TryMapAsync(async ValueTask (_) => + { + await ValueTask.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForFailureWithThrowing_ValueTask() + { + var start = Failure("error"); + var result = await start.TryMapAsync(ValueTask (_) => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task TryMapAsync_ReturnsFailure_ForFailureWithAwaitThrowing_ValueTask() + { + var start = Failure("error"); + var result = await start.TryMapAsync(async ValueTask (_) => + { + await ValueTask.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsSuccess_ForSuccessAndMappingReturningSuccess_ValueTask() + { + var start = Success(2); + var result = await start.ThenTryAsync(value => ValueTask.FromResult(Success(value.ToString()))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo("2")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingReturningFailure_ValueTask() + { + var start = Success(2); + var result = await start.ThenTryAsync(_ => ValueTask.FromResult(Failure("error"))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingReturningFailure_ValueTask() + { + var start = Failure("error"); + var result = await start.ThenTryAsync(x => ValueTask.FromResult(Success(x.ToString()))); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingThrowing_ValueTask() + { + var start = Success(2); + var result = await start.ThenTryAsync(ValueTask> (_) => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingAwaitThrowing_ValueTask() + { + var start = Success(2); + var result = await start.ThenTryAsync(async ValueTask> (_) => + { + await ValueTask.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + var instance = result.Error as ExceptionError; + Assert.That(instance?.Exception, Is.InstanceOf()); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingThrowing_ValueTask() + { + var start = Failure("error"); + var result = await start.ThenTryAsync(ValueTask> (_) => throw new CustomTestException()); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingAwaitThrowing_ValueTask() + { + var start = Failure("error"); + var result = await start.ThenTryAsync(async ValueTask> (_) => + { + await ValueTask.CompletedTask; + throw new CustomTestException(); + }); + + Assert.Multiple(() => + { + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error?.Message, Is.EqualTo("error")); + }); + } +} \ No newline at end of file diff --git a/src/Results.Tests/ResultUnboxTests.cs b/src/Results.Tests/ResultUnboxTests.cs new file mode 100644 index 0000000..ebb0281 --- /dev/null +++ b/src/Results.Tests/ResultUnboxTests.cs @@ -0,0 +1,113 @@ +namespace Geekeey.Common.Results.Tests; + +[TestFixture] +internal sealed class ResultUnboxTests +{ + [Test] + public void TryGetValue_1_ReturnsTrueAndSetsValue_ForSuccess() + { + var result = Success(2); + var ok = result.TryGetValue(out var value); + + Assert.Multiple(() => + { + Assert.That(ok, Is.True); + Assert.That(value, Is.EqualTo(2)); + }); + } + + [Test] + public void TryGetValue_1_ReturnsFalse_ForFailure() + { + var result = Failure("error"); + var ok = result.TryGetValue(out var value); + + Assert.Multiple(() => + { + Assert.That(ok, Is.False); + Assert.That(value, Is.EqualTo(default(int))); + }); + } + + [Test] + public void TryGetValue_2_ReturnsTrueAndSetsValue_ForSuccess() + { + var result = Success(2); + var ok = result.TryGetValue(out var value, out var error); + + Assert.Multiple(() => + { + Assert.That(ok, Is.True); + Assert.That(value, Is.EqualTo(2)); + Assert.That(error, Is.EqualTo(default(Error))); + }); + } + + [Test] + public void TryGetValue_2_ReturnsFalseAndSetsError_ForFailure() + { + var result = Failure("error"); + var ok = result.TryGetValue(out var value, out var error); + + Assert.Multiple(() => + { + Assert.That(ok, Is.False); + Assert.That(value, Is.EqualTo(default(int))); + Assert.That(error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void TryGetError_1_ReturnsTrueAndSetsError_ForFailure() + { + var result = Failure("error"); + var ok = result.TryGetError(out var error); + + Assert.Multiple(() => + { + Assert.That(ok, Is.True); + Assert.That(error?.Message, Is.EqualTo("error")); + }); + } + + [Test] + public void TryGetError_1_ReturnsFalse_ForSuccess() + { + var result = Success(2); + var ok = result.TryGetError(out var error); + + Assert.Multiple(() => + { + Assert.That(ok, Is.False); + Assert.That(error, Is.EqualTo(default(Error))); + }); + } + + [Test] + public void TryGetError_2_ReturnsTrueAndSetsError_ForFailure() + { + var r = Failure("error"); + var ok = r.TryGetError(out var error, out var value); + + Assert.Multiple(() => + { + Assert.That(ok, Is.True); + Assert.That(error?.Message, Is.EqualTo("error")); + Assert.That(value, Is.EqualTo(default(int))); + }); + } + + [Test] + public void TryGetError_2_ReturnsFalseAndSetsValue_ForSuccess() + { + var r = Success(2); + var ok = r.TryGetError(out var error, out var value); + + Assert.Multiple(() => + { + Assert.That(ok, Is.False); + Assert.That(error, Is.EqualTo(default(Error))); + Assert.That(value, Is.EqualTo(2)); + }); + } +} \ No newline at end of file diff --git a/src/Results.Tests/Results.Tests.csproj b/src/Results.Tests/Results.Tests.csproj new file mode 100644 index 0000000..699975e --- /dev/null +++ b/src/Results.Tests/Results.Tests.csproj @@ -0,0 +1,25 @@ + + + + false + true + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Results/Errors/AggregateError.cs b/src/Results/Errors/AggregateError.cs new file mode 100644 index 0000000..c06bf8f --- /dev/null +++ b/src/Results/Errors/AggregateError.cs @@ -0,0 +1,16 @@ +namespace Geekeey.Common.Results; + +/// +/// An error which is a combination of other errors. +/// +/// The errors the error consists of. +public sealed class AggregateError(IEnumerable errors) : Error +{ + /// + /// The errors the error consists of. + /// + public IReadOnlyCollection Errors { get; } = errors.ToList(); + + /// + public override string Message => string.Join(Environment.NewLine, Errors.Select(error => error.Message)); +} \ No newline at end of file diff --git a/src/Results/Errors/Error.cs b/src/Results/Errors/Error.cs new file mode 100644 index 0000000..7bc8cda --- /dev/null +++ b/src/Results/Errors/Error.cs @@ -0,0 +1,33 @@ +namespace Geekeey.Common.Results; + +public abstract class Error +{ + /// + /// A statically accessible default "Result has no value." error. + /// + internal static Error DefaultValueError { get; } = new StringError("The result has no value."); + + /// + /// The message used to display the error. + /// + public abstract string Message { get; } + + /// + /// Gets a string representation of the error. Returns by default. + /// + public override string ToString() => Message; + + /// + /// Implicitly converts a string into a . + /// + /// The message of the error. + public static implicit operator Error(string message) + => new StringError(message); + + /// + /// Implicitly converts an exception into an . + /// + /// The exception to convert. + public static implicit operator Error(Exception exception) + => new ExceptionError(exception); +} \ No newline at end of file diff --git a/src/Results/Errors/ExceptionError.cs b/src/Results/Errors/ExceptionError.cs new file mode 100644 index 0000000..2c987a9 --- /dev/null +++ b/src/Results/Errors/ExceptionError.cs @@ -0,0 +1,18 @@ +namespace Geekeey.Common.Results; + +/// +/// An error which is constructed from an exception. +/// +/// The exception in the error. +public sealed class ExceptionError(Exception exception) : Error +{ + /// + /// The exception in the error. + /// + public Exception Exception { get; } = exception; + + /// + /// The exception in the error. + /// + public override string Message => Exception.Message; +} \ No newline at end of file diff --git a/src/Results/Errors/InvalidOperationError.cs b/src/Results/Errors/InvalidOperationError.cs new file mode 100644 index 0000000..a955972 --- /dev/null +++ b/src/Results/Errors/InvalidOperationError.cs @@ -0,0 +1,11 @@ +namespace Geekeey.Common.Results; + +/// +/// An error which represents an invalid operation. +/// +/// An optional message describing the operation and why it is invalid. +public sealed class InvalidOperationError(string? message = null) : Error +{ + /// + public override string Message => message ?? "Invalid operation."; +} \ No newline at end of file diff --git a/src/Results/Errors/NotFoundError.cs b/src/Results/Errors/NotFoundError.cs new file mode 100644 index 0000000..4db04e1 --- /dev/null +++ b/src/Results/Errors/NotFoundError.cs @@ -0,0 +1,17 @@ +namespace Geekeey.Common.Results; + +/// +/// An error which represents something which wasn't found. +/// +/// The key corresponding to the thing which wasn't found. +/// A message which describes the thing which wasn't found. +public sealed class NotFoundError(object? key, string message) : Error +{ + /// + /// The key corresponding to the thing which wasn't found. + /// + public object? Key { get; } = key; + + /// + public override string Message => message; +} \ No newline at end of file diff --git a/src/Results/Errors/StringError.cs b/src/Results/Errors/StringError.cs new file mode 100644 index 0000000..db9d10c --- /dev/null +++ b/src/Results/Errors/StringError.cs @@ -0,0 +1,11 @@ +namespace Geekeey.Common.Results; + +/// +/// An error which displays a simple string. +/// +/// The message to display. +public sealed class StringError(string message) : Error +{ + /// + public override string Message => message; +} \ No newline at end of file diff --git a/src/Results/Exceptions/UnwrapException.cs b/src/Results/Exceptions/UnwrapException.cs new file mode 100644 index 0000000..866730f --- /dev/null +++ b/src/Results/Exceptions/UnwrapException.cs @@ -0,0 +1,18 @@ +namespace Geekeey.Common.Results; + +/// +/// The exception is thrown when an is attempted to be unwrapped contains only a failure value. +/// +public sealed class UnwrapException : Exception +{ + /// + /// Creates a new . + /// + public UnwrapException() : base("Cannot unwrap result because it does not have a value.") { } + + /// + /// Creates a new . + /// + /// An error message. + public UnwrapException(string error) : base(error) { } +} \ No newline at end of file diff --git a/src/Results/Extensions/Extensions.Enumerable.cs b/src/Results/Extensions/Extensions.Enumerable.cs new file mode 100644 index 0000000..9bc62db --- /dev/null +++ b/src/Results/Extensions/Extensions.Enumerable.cs @@ -0,0 +1,34 @@ +namespace Geekeey.Common.Results; + +public static partial class Extensions +{ + /// + /// Turns a sequence of results into a single result containing the success values in the results only if all the + /// results have success values. + /// + /// The results to turn into a single sequence. + /// The type of the success values in the results. + /// A single result containing a sequence of all the success values from the original sequence of results, + /// or the first failure value encountered within the sequence. + /// + /// This method completely enumerates the input sequence before returning and is not lazy. As a consequence of this, + /// the sequence within the returned result is an . + /// + public static Result> Join(this IEnumerable> results) + { + _ = results.TryGetNonEnumeratedCount(out var count); + var list = new List(count); + + foreach (var result in results) + { + if (!result.TryGetValue(out var value, out var error)) + { + return new Result>(error); + } + + list.Add(value); + } + + return list; + } +} \ No newline at end of file diff --git a/src/Results/Extensions/Extensions.Task.cs b/src/Results/Extensions/Extensions.Task.cs new file mode 100644 index 0000000..1834158 --- /dev/null +++ b/src/Results/Extensions/Extensions.Task.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Geekeey.Common.Results; + +[ExcludeFromCodeCoverage] +public static partial class Extensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task> Map(this ValueTask> result, + Func func) + => (await result).Map(func); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async ValueTask> Map(this Task> result, + Func func) + => (await result).Map(func); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async ValueTask> Map(this ValueTask> result, + Func> func) + => await (await result).MapAsync(func); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task> Map(this Task> result, + Func> func) + => await (await result).MapAsync(func); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async ValueTask> Then(this ValueTask> result, + Func> func) + => (await result).Then(func); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task> Then(this Task> result, + Func> func) + => (await result).Then(func); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async ValueTask> Then(this ValueTask> result, + Func>> func) + => await (await result).ThenAsync(func); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task> Then(this Task> result, + Func>> func) + => await (await result).ThenAsync(func); +} \ No newline at end of file diff --git a/src/Results/Prelude.cs b/src/Results/Prelude.cs new file mode 100644 index 0000000..e3461c1 --- /dev/null +++ b/src/Results/Prelude.cs @@ -0,0 +1,92 @@ +using System.Diagnostics.Contracts; + +namespace Geekeey.Common.Results; + +/// +/// A class containing various utility methods, a 'prelude' to the rest of the library. +/// +/// +/// This class is meant to be imported statically, e.g. using static Geekeey.Common.Results.Prelude;. +/// Recommended to be imported globally via a global using statement. +/// +public static class Prelude +{ + /// + /// Creates a result containing an success value. + /// + /// The type of the success value. + /// The success value to create the result from. + [Pure] + public static Result Success(T value) => new(value); + + /// + /// Creates a result containing a failure value. + /// + /// The type of an success value in the result. + /// The failure value to create the result from. + [Pure] + public static Result Failure(Error error) => new(error); + + /// + /// Tries to execute a function and return the result. If the function throws an exception, the exception will be + /// returned wrapped in an . + /// + /// The type the function returns. + /// The function to try execute. + /// A result containing the return value of the function or an containing the + /// exception thrown by the function. + [Pure] + public static Result Try(Func function) + { + try + { + return new Result(function()); + } + catch (Exception exception) + { + return new Result(new ExceptionError(exception)); + } + } + + /// + /// Tries to execute an asynchronous function and return the result. If the function throws an exception, the + /// exception will be returned wrapped in an . + /// + /// The type the function returns. + /// The function to try execute. + /// A result containing the return value of the function or an containing the + /// exception thrown by the function. + [Pure] + public static async ValueTask> TryAsync(Func> function) + { + try + { + return new Result(await function()); + } + catch (Exception exception) + { + return new Result(new ExceptionError(exception)); + } + } + + /// + /// Tries to execute an asynchronous function and return the result. If the function throws an exception, the + /// exception will be returned wrapped in an . + /// + /// The type the function returns. + /// The function to try execute. + /// A result containing the return value of the function or an containing the + /// exception thrown by the function. + [Pure] + public static async Task> TryAsync(Func> function) + { + try + { + return new Result(await function()); + } + catch (Exception exception) + { + return new Result(new ExceptionError(exception)); + } + } +} \ No newline at end of file diff --git a/src/Results/Project.props b/src/Results/Project.props new file mode 100644 index 0000000..6942754 --- /dev/null +++ b/src/Results/Project.props @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Results/Result.Conversion.cs b/src/Results/Result.Conversion.cs new file mode 100644 index 0000000..2eceeb3 --- /dev/null +++ b/src/Results/Result.Conversion.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.Contracts; + +namespace Geekeey.Common.Results; + +public readonly partial struct Result +{ + /// + /// Implicitly constructs a result from a success value. + /// + /// The value to construct the result from. + [Pure] + public static implicit operator Result(T value) => new(value); + + /// + /// Implicitly constructs a result from a failure value. + /// + /// The error to construct the result from. + [Pure] + public static implicit operator Result(Error error) => new(error); + + /// + /// Unwraps the success value of the result. Throws an if the result is a failure. + /// + /// + /// This call is unsafe in the sense that it might intentionally throw an exception. Please only use this + /// call if the caller knows that this operation is safe, or that an exception is acceptable to be thrown. + /// + /// The success value of the result. + /// The result is not a success. + [Pure] + public T Unwrap() => IsSuccess ? Value! : throw new UnwrapException(); + + /// + [Pure] + public static explicit operator T(Result result) => result.Unwrap(); +} \ No newline at end of file diff --git a/src/Results/Result.Equality.cs b/src/Results/Result.Equality.cs new file mode 100644 index 0000000..847183d --- /dev/null +++ b/src/Results/Result.Equality.cs @@ -0,0 +1,115 @@ +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Numerics; + +namespace Geekeey.Common.Results; + +public readonly partial struct Result : IEquatable>, IEquatable, + IEqualityOperators, Result, bool>, IEqualityOperators, T, bool> +{ + /// + /// Checks whether the result is equal to another result. Results are equal if both results are success values and + /// the success values are equal, or if both results are failures. + /// + /// The result to check for equality with the current result. + [Pure] + public bool Equals(Result other) + => Equals(this, other, EqualityComparer.Default); + + /// + /// Checks whether the result is equal to another result. Results are equal if both results are success values and + /// the success values are equal, or if both results are failures. + /// + /// The result to check for equality with the current result. + /// The equality comparer to use for comparing values. + [Pure] + public bool Equals(Result other, IEqualityComparer comparer) + => Equals(this, other, comparer); + + /// + /// Checks whether the result is a success value and the success value is equal to another value. + /// + /// The value to check for equality with the success value of the result. + [Pure] + public bool Equals(T? other) + => Equals(this, other, EqualityComparer.Default); + + /// + /// Checks whether the result is a success value and the success value is equal to another value using a specified + /// equality comparer. + /// + /// The value to check for equality with the success value of the result. + /// The equality comparer to use for comparing values. + [Pure] + public bool Equals(T? other, IEqualityComparer comparer) + => Equals(this, other, comparer); + + /// + [Pure] + public override bool Equals(object? other) + => other is T x && Equals(x) || other is Result r && Equals(r); + + /// + [Pure] + public override int GetHashCode() + => GetHashCode(this, EqualityComparer.Default); + + internal static bool Equals(Result a, Result b, IEqualityComparer comparer) + { + if (!a._success || !b._success) return !a._success && !b._success; + if (a.Value is null || b.Value is null) return a.Value is null && b.Value is null; + return comparer.Equals(a.Value, b.Value); + } + + internal static bool Equals(Result a, T? b, IEqualityComparer comparer) + { + if (!a._success) return false; + if (a.Value is null || b is null) return a.Value is null && b is null; + return comparer.Equals(a.Value, b); + } + + internal static int GetHashCode(Result result, IEqualityComparer comparer) + { + if (result is { _success: true, Value: not null }) + return comparer.GetHashCode(result.Value); + return 0; + } + + /// + /// Checks whether two results are equal. Results are equal if both results are success values and the success + /// values are equal, or if both results are failures. + /// + /// The first result to compare. + /// The second result to compare. + [Pure] + [ExcludeFromCodeCoverage] + public static bool operator ==(Result a, Result b) => a.Equals(b); + + /// + /// Checks whether two results are not equal. Results are equal if both results are success values and the success + /// values are equal, or if both results are failures. + /// + /// The first result to compare. + /// The second result to compare. + [Pure] + [ExcludeFromCodeCoverage] + public static bool operator !=(Result a, Result b) => !a.Equals(b); + + /// + /// Checks whether a result is a success value and the success value is equal to another value. + /// + /// The result to compare. + /// The value to check for equality with the success value in the result. + [Pure] + [ExcludeFromCodeCoverage] + public static bool operator ==(Result a, T? b) => a.Equals(b); + + /// + /// Checks whether a result either does not have a value, or the value is not equal to another value. + /// + /// The result to compare. + /// The value to check for inequality with the success value in the result. + [Pure] + [ExcludeFromCodeCoverage] + public static bool operator !=(Result a, T? b) => !a.Equals(b); +} \ No newline at end of file diff --git a/src/Results/Result.Matching.cs b/src/Results/Result.Matching.cs new file mode 100644 index 0000000..47f310d --- /dev/null +++ b/src/Results/Result.Matching.cs @@ -0,0 +1,91 @@ +using System.Diagnostics.Contracts; + +namespace Geekeey.Common.Results; + +public readonly partial struct Result +{ + /// + /// Matches over the success value or failure value of the result and returns another value. Can be conceptualized + /// as an exhaustive switch expression matching all possible values of the type. + /// + /// The type to return from the match. + /// The function to apply to the success value of the result if the result is a success. + /// The function to apply to the failure value of the result if the result is a failure. + /// The result of applying either or on the success + /// value or failure value of the result. + [Pure] + public TResult Match(Func success, Func failure) + => _success ? success(Value!) : failure(Error!); + + /// + /// Matches over the success value or failure value of the result and invokes an effectful action onto the success + /// value or failure value. Can be conceptualized as an exhaustive switch statement matching all possible + /// values of the type. + /// + /// The function to call with the success value of the result if the result is a success. + /// The function to call with the failure value of the result if the result is a failure. + public void Switch(Action success, Action failure) + { + if (_success) success(Value!); + else failure(Error!); + } +} + +public readonly partial struct Result +{ + /// + /// Asynchronously matches over the success value or failure value of the result and returns another value. Can be + /// conceptualized as an exhaustive switch expression matching all possible values of the type. + /// + /// The type to return from the match. + /// The function to apply to the success value of the result if the result is a success. + /// The function to apply to the failure value of the result if the result is a failure. + /// A task completing with the result of applying either or + /// on the success value or failure value of the result. + [Pure] + public async ValueTask MatchAsync(Func> success, Func> failure) + => _success ? await success(Value!) : await failure(Error!); + + /// + /// Asynchronously matches over the success value or failure value of the result and invokes an effectful action + /// onto the success value or the failure value. Can be conceptualized as an exhaustive switch statement + /// matching all possible values of the type. + /// + /// The function to call with the success value of the result if the result is a success. + /// The function to call with the failure value of the result if the result is a failure. + public async ValueTask SwitchAsync(Func success, Func failure) + { + if (_success) await success(Value!); + else await failure(Error!); + } +} + +public readonly partial struct Result +{ + /// + /// Asynchronously matches over the success value or failure value of the result and returns another value. Can be + /// conceptualized as an exhaustive switch expression matching all possible values of the type. + /// + /// The type to return from the match. + /// The function to apply to the success value of the result if the result is a success. + /// The function to apply to the failure value of the result if the result is a failure. + /// A task completing with the result of applying either or + /// on the success value or failure value of the result. + [Pure] + public async Task MatchAsync(Func> success, Func> failure) + => _success ? await success(Value!) : await failure(Error!); + + /// + /// Asynchronously matches over the success value or failure value of the result and invokes an effectful action + /// onto the success value or failure value. Can be conceptualized as an exhaustive switch statement matching + /// all possible values of + /// the type. + /// + /// The function to call with the success value of the result if the result is a success. + /// The function to call with the failure value of the result if the result is a failure. + public async Task SwitchAsync(Func success, Func failure) + { + if (_success) await success(Value!); + else await failure(Error!); + } +} \ No newline at end of file diff --git a/src/Results/Result.Transform.cs b/src/Results/Result.Transform.cs new file mode 100644 index 0000000..b5b22c3 --- /dev/null +++ b/src/Results/Result.Transform.cs @@ -0,0 +1,335 @@ +using System.Diagnostics.Contracts; + +namespace Geekeey.Common.Results; + +public readonly partial struct Result +{ + /// + /// Maps the success value of the result using a mapping function, or does nothing if the result is a failure. + /// + /// The function used to map the success value. + /// The type of the new value. + /// A new result containing either the mapped success value or the failure value of the original + /// result. + [Pure] + public Result Map(Func func) + => _success ? new Result(func(Value!)) : new Result(Error!); + + /// + /// Tries to map the success value of the result using a mapping function, or does nothing if the result is a + /// failure. If the mapping function throws an exception, the exception will be returned wrapped in an + /// . + /// + /// The function used to map the success value. + /// The type of the new value. + /// A new result containing either the mapped value, the exception thrown by + /// wrapped in an , or the failure value of the original result. + [Pure] + public Result TryMap(Func func) + { + try + { + return Map(func); + } + catch (Exception exception) + { + return new Result(new ExceptionError(exception)); + } + } + + /// + /// Maps the success value of the result to a new result using a mapping function, or does nothing if the result is + /// a failure. + /// + /// The function used to map the success value to a new result. + /// The type of the new value. + /// A result which is either the mapped result or a new result containing the failure value of the original + /// result. + [Pure] + public Result Then(Func> func) + => _success ? func(Value!) : new Result(Error!); + + /// + /// Tries to map the success value of the result to a new result using a mapping function, or does nothing if the result + /// is a failure. If the mapping function throws an exception, the exception will be returned wrapped in an + /// . + /// + /// The function used to map the success value to a new result. + /// The type of the new value. + /// A result which is either the mapped result, the exception thrown by wrapped in + /// an , or a new result containing the failure value of the original result. + [Pure] + public Result ThenTry(Func> func) + { + try + { + return Then(func); + } + catch (Exception exception) + { + return new Result(new ExceptionError(exception)); + } + } +} + +public readonly partial struct Result +{ + /// + /// Maps the success value of the result using an asynchronous mapping function, or does nothing if the result is + /// a failure. + /// + /// The function used to map the success value. + /// The type of the new value. + /// A which either completes asynchronously by invoking the mapping function on + /// the success value of the result and constructing a new result containing the mapped value, or completes + /// synchronously by returning a new result containing the failure value of the original result. + [Pure] + public ValueTask> MapAsync(Func> func) + { + if (!_success) return ValueTask.FromResult(new Result(Error!)); + + var task = func(Value!); + return CreateResult(task); + + static async ValueTask> CreateResult(ValueTask task) + { + var value = await task; + return new Result(value); + } + } + + /// + /// Maps the success value of the result using an asynchronous mapping function, or does nothing if the result is a + /// failure. If the mapping function throws an exception, the exception will be returned wrapped in an + /// . + /// + /// The function used to map the success value. + /// The type of the new value. + /// A which either completes asynchronously by invoking the mapping function on + /// the success value of the result and constructing a new result containing the mapped value, returning any exception + /// thrown by wrapped in an or completes synchronously by + /// returning a new result containing the failure value of the original result. + [Pure] + public ValueTask> TryMapAsync(Func> func) + { + if (!_success) return ValueTask.FromResult(new Result(Error!)); + + try + { + var task = func(Value!); + return CreateResult(task); + } + catch (Exception exception) + { + return ValueTask.FromResult(new Result(new ExceptionError(exception))); + } + + static async ValueTask> CreateResult(ValueTask task) + { + try + { + var value = await task; + return new Result(value); + } + catch (Exception exception) + { + return new Result(new ExceptionError(exception)); + } + } + } + + /// + /// Maps the success value of the result to a new result using an asynchronous mapping function, or does nothing if + /// the result is a failure. + /// + /// The function used to map the success value to a new result. + /// The type of the new value. + /// A which either completes asynchronously by invoking the mapping function on + /// the success value of the result, or completes synchronously by returning a new result containing the failure + /// value of the original result. + [Pure] + public ValueTask> ThenAsync(Func>> func) + { + if (!_success) return ValueTask.FromResult(new Result(Error!)); + + var task = func(Value!); + return CreateResult(task); + + static async ValueTask> CreateResult(ValueTask> task) + { + var result = await task; + return result; + } + } + + /// + /// Maps the success value of the result to a new result using an asynchronous mapping function, or does nothing if + /// the result is a failure. If the mapping function throws an exception, the exception will be returned wrapped in + /// an . + /// + /// The function used to map the success value to a new result. + /// The type of the new value. + /// A which either completes asynchronously by invoking the mapping function on + /// the success value of the result, returning any exception thrown by wrapped in an + /// , or completes synchronously by returning a new result containing the failure value + /// of the original result. + [Pure] + public ValueTask> ThenTryAsync(Func>> func) + { + if (!_success) return ValueTask.FromResult(new Result(Error!)); + + try + { + var task = func(Value!); + return CreateResult(task); + } + catch (Exception exception) + { + return ValueTask.FromResult(new Result(new ExceptionError(exception))); + } + + static async ValueTask> CreateResult(ValueTask> task) + { + try + { + var value = await task; + return value; + } + catch (Exception exception) + { + return new Result(new ExceptionError(exception)); + } + } + } +} + +public readonly partial struct Result +{ + /// + /// Maps the success value of the result using an asynchronous mapping function, or does nothing if the result is + /// a failure. + /// + /// The function used to map the success value. + /// The type of the new value. + /// A which either completes asynchronously by invoking the mapping function on + /// the success value of the result and constructing a new result containing the mapped value, or completes + /// synchronously by returning a new result containing the failure value of the original result. + [Pure] + public Task> MapAsync(Func> func) + { + if (!_success) return Task.FromResult(new Result(Error!)); + + var task = func(Value!); + return CreateResult(task); + + static async Task> CreateResult(Task task) + { + var value = await task; + return new Result(value); + } + } + + /// + /// Maps the success value of the result using an asynchronous mapping function, or does nothing if the result is a + /// failure. If the mapping function throws an exception, the exception will be returned wrapped in an + /// . + /// + /// The function used to map the success value. + /// The type of the new value. + /// A which either completes asynchronously by invoking the mapping function on + /// the success value of the result and constructing a new result containing the mapped value, returning any exception + /// thrown by wrapped in an or completes synchronously by + /// returning a new result containing the failure value of the original result. + [Pure] + public Task> TryMapAsync(Func> func) + { + if (!_success) return Task.FromResult(new Result(Error!)); + + try + { + var task = func(Value!); + return CreateResult(task); + } + catch (Exception exception) + { + return Task.FromResult(new Result(new ExceptionError(exception))); + } + + static async Task> CreateResult(Task task) + { + try + { + var value = await task; + return new Result(value); + } + catch (Exception exception) + { + return new Result(new ExceptionError(exception)); + } + } + } + + /// + /// Maps the success value of the result to a new result using an asynchronous mapping function, or does nothing if + /// the result is a failure. + /// + /// The function used to map the success value to a new result. + /// The type of the new value. + /// A which either completes asynchronously by invoking the mapping function on + /// the success value of the result, or completes synchronously by returning a new result containing the failure + /// value of the original result. + [Pure] + public Task> ThenAsync(Func>> func) + { + if (!_success) return Task.FromResult(new Result(Error!)); + + var task = func(Value!); + return CreateResult(task); + + static async Task> CreateResult(Task> task) + { + var result = await task; + return result; + } + } + + /// + /// Maps the success value of the result to a new result using an asynchronous mapping function, or does nothing if + /// the result is a failure. If the mapping function throws an exception, the exception will be returned wrapped in + /// an . + /// + /// The function used to map the success value to a new result. + /// The type of the new value. + /// A which either completes asynchronously by invoking the mapping function on + /// the success value of the result, returning any exception thrown by wrapped in an + /// , or completes synchronously by returning a new result containing the failure value + /// of the original result. + [Pure] + public Task> ThenTryAsync(Func>> func) + { + if (!_success) return Task.FromResult(new Result(Error!)); + + try + { + var task = func(Value!); + return CreateResult(task); + } + catch (Exception exception) + { + return Task.FromResult(new Result(new ExceptionError(exception))); + } + + static async Task> CreateResult(Task> task) + { + try + { + var value = await task; + return value; + } + catch (Exception exception) + { + return new Result(new ExceptionError(exception)); + } + } + } +} \ No newline at end of file diff --git a/src/Results/Result.Unbox.cs b/src/Results/Result.Unbox.cs new file mode 100644 index 0000000..42e186d --- /dev/null +++ b/src/Results/Result.Unbox.cs @@ -0,0 +1,63 @@ +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; + +namespace Geekeey.Common.Results; + +public readonly partial struct Result +{ + /// + /// Tries to get the success value from the result. + /// + /// The success value of the result. + /// Whether the result has success value. + [Pure] + public bool TryGetValue([MaybeNullWhen(false)] out T value) + { + value = Value!; + + return _success; + } + + /// + /// Tries to get the success value from the result. + /// + /// The success value of the result. + /// The failure value of the result. + /// Whether the result has a success value. + [Pure] + public bool TryGetValue([MaybeNullWhen(false)] out T value, [MaybeNullWhen(true)] out Error error) + { + value = Value!; + error = !_success ? Error : null!; + + return _success; + } + + /// + /// Tries to get the failure value from the result. + /// + /// The failure value of the result. + /// Whether the result has a failure value. + [Pure] + public bool TryGetError([MaybeNullWhen(false)] out Error error) + { + error = !_success ? Error : null!; + + return !_success; + } + + /// + /// Tries to get the failure value from the result. + /// + /// The failure value of the result. + /// The success value of the result. + /// Whether the result a failure value. + [Pure] + public bool TryGetError([MaybeNullWhen(false)] out Error error, [MaybeNullWhen(true)] out T value) + { + error = !_success ? Error : null!; + value = Value!; + + return !_success; + } +} \ No newline at end of file diff --git a/src/Results/Result.cs b/src/Results/Result.cs new file mode 100644 index 0000000..e9b7652 --- /dev/null +++ b/src/Results/Result.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; + +namespace Geekeey.Common.Results; + +/// +/// A type which contains either a success value or a failure value, which is represented by an . +/// +/// The type of the success value. +[DebuggerTypeProxy(typeof(Result<>.ResultDebugProxy))] +public readonly partial struct Result +{ + private readonly bool _success; + + private readonly T? _value; + private readonly Error? _error; + + /// + /// Creates a new result with an success value. + /// + /// The success value. + public Result(T value) + { + _success = true; + _value = value; + _error = default; + } + + /// + /// Creates a new result with an failure value. + /// + /// The error of the result. + public Result(Error error) + { + _success = false; + _value = default; + _error = error; + } + + internal T? Value => _value; + + internal Error? Error => _success ? null : (_error ?? Error.DefaultValueError); + + /// + /// Whether the result is a success. + /// + /// + /// This is always the inverse of but is more specific about intent. + /// + [MemberNotNullWhen(true, nameof(Value))] + public bool IsSuccess => _success; + + /// + /// Whether the result is a failure. + /// + /// + /// This is always the inverse of but is more specific about intent. + /// + [MemberNotNullWhen(true, nameof(Error))] + public bool IsFailure => !_success; + + /// + /// Gets a string representation of the result. + /// + [Pure] + public override string ToString() + => _success ? $"Success {{ {Value} }}" : $"Failure {{ {Error} }}"; + + private sealed class ResultDebugProxy(Result result) + { + public bool IsSuccess => result.IsSuccess; + + public object? Value => result.IsSuccess ? result.Value : result.Error; + } +} \ No newline at end of file diff --git a/src/Results/Results.csproj b/src/Results/Results.csproj new file mode 100644 index 0000000..3a353e1 --- /dev/null +++ b/src/Results/Results.csproj @@ -0,0 +1,14 @@ + + + true + + + + + + + + + + + \ No newline at end of file diff --git a/src/Results/package-readme.md b/src/Results/package-readme.md new file mode 100644 index 0000000..d6faa53 --- /dev/null +++ b/src/Results/package-readme.md @@ -0,0 +1,2 @@ +Result is a simple yet powerful [result type](https://doc.rust-lang.org/std/result/) implementation for C#, containing a +variety of utilities and standard functions for working with result types and integrating them into the rest of C#. \ No newline at end of file