commit ca9f290d44a0036de6fec7ffb98762ad5cf123be Author: Louis Seubert Date: Sun Apr 14 17:42:13 2024 +0200 feat: initial project commit 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