diff --git a/LICENSE b/LICENSE index ded1dcf..cf771b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023-2025 JustFixMe +Copyright (c) 2023-2026 JustFixMe Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Railway.SourceGenerator/ResultAppendExecutor.cs b/Railway.SourceGenerator/ResultAppendExecutor.cs index 41679ea..792d151 100644 --- a/Railway.SourceGenerator/ResultAppendExecutor.cs +++ b/Railway.SourceGenerator/ResultAppendExecutor.cs @@ -88,27 +88,27 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor } """); - GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); if (argCount < Constants.MaxResultTupleSize) { GenerateExpandedMethods(sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateExpandedAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateExpandedAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateExpandedAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateExpandedAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); } sb.AppendLine("#endregion"); } - private void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + private void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { string methodTemplateDecl = GenerateTemplateDecl(templateArgNames); sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultTypeDef}}> Append{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func nextFunc) + public static async {{taskType}}<{{resultTypeDef}}> Append{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func nextFunc) { var result = await resultTask.ConfigureAwait(false); if (result.State == ResultState.Bottom) @@ -137,7 +137,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultTypeDef}}> Append{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func<{{taskType}}> nextFunc) + public static async {{taskType}}<{{resultTypeDef}}> Append{{methodSuffix}}{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func<{{taskType}}> nextFunc) { if (result.State == ResultState.Bottom) { @@ -165,7 +165,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultTypeDef}}> Append{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<{{taskType}}> nextFunc) + public static async {{taskType}}<{{resultTypeDef}}> Append{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<{{taskType}}> nextFunc) { var result = await resultTask.ConfigureAwait(false); if (result.State == ResultState.Bottom) @@ -192,7 +192,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor """); } - private void GenerateExpandedAsyncMethods(string taskType, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + private void GenerateExpandedAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { var expandedTemplateArgNames = templateArgNames.Add("TNext"); string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames); @@ -201,7 +201,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, TNext next) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodSuffix}}{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, TNext next) { var result = await resultTask.ConfigureAwait(false); return result.State switch @@ -216,7 +216,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Result next) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodSuffix}}{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Result next) { var result = await resultTask.ConfigureAwait(false); if ((result.State & next.State) == ResultState.Bottom) @@ -243,7 +243,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func nextFunc) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodSuffix}}{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func nextFunc) { var result = await resultTask.ConfigureAwait(false); return result.State switch @@ -258,7 +258,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{resultTypeDef}} result, Func<{{taskType}}> nextFunc) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodSuffix}}{{methodExpandedTemplateDecl}}(this {{resultTypeDef}} result, Func<{{taskType}}> nextFunc) { return result.State switch { @@ -272,7 +272,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<{{taskType}}> nextFunc) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodSuffix}}{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<{{taskType}}> nextFunc) { var result = await resultTask.ConfigureAwait(false); return result.State switch @@ -288,7 +288,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func> nextFunc) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodSuffix}}{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func> nextFunc) { var result = await resultTask.ConfigureAwait(false); if (result.State == ResultState.Bottom) @@ -317,7 +317,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{resultTypeDef}} result, Func<{{taskType}}>> nextFunc) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodSuffix}}{{methodExpandedTemplateDecl}}(this {{resultTypeDef}} result, Func<{{taskType}}>> nextFunc) { if (result.State == ResultState.Bottom) { @@ -345,7 +345,7 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultAppendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<{{taskType}}>> nextFunc) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Append{{methodSuffix}}{{methodExpandedTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<{{taskType}}>> nextFunc) { var result = await resultTask.ConfigureAwait(false); if (result.State == ResultState.Bottom) diff --git a/Railway.SourceGenerator/ResultBindExecutor.cs b/Railway.SourceGenerator/ResultBindExecutor.cs index 0f0e7d2..c38d8cc 100644 --- a/Railway.SourceGenerator/ResultBindExecutor.cs +++ b/Railway.SourceGenerator/ResultBindExecutor.cs @@ -36,13 +36,13 @@ internal sealed class ResultBindExecutor : ResultExtensionsExecutor } """); - GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { string methodTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("R")); string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result")); @@ -51,7 +51,7 @@ internal sealed class ResultBindExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultBindExecutor)}}", "1.0.0.0")] - public static async {{taskType}}> Bind{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{bindTemplateDecl}} binding) + public static async {{taskType}}> Bind{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{bindTemplateDecl}} binding) { var result = await resultTask.ConfigureAwait(false); return result.State switch @@ -66,7 +66,7 @@ internal sealed class ResultBindExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultBindExecutor)}}", "1.0.0.0")] - public static {{taskType}}> Bind{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} binding) + public static {{taskType}}> Bind{{methodSuffix}}{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} binding) { return result.State switch { @@ -80,7 +80,7 @@ internal sealed class ResultBindExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultBindExecutor)}}", "1.0.0.0")] - public static async {{taskType}}> Bind{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} binding) + public static async {{taskType}}> Bind{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} binding) { var result = await resultTask.ConfigureAwait(false); return result.State switch diff --git a/Railway.SourceGenerator/ResultExtendExecutor.cs b/Railway.SourceGenerator/ResultExtendExecutor.cs index 55ee7ed..bee095e 100644 --- a/Railway.SourceGenerator/ResultExtendExecutor.cs +++ b/Railway.SourceGenerator/ResultExtendExecutor.cs @@ -57,13 +57,13 @@ internal sealed class ResultExtendExecutor : ResultExtensionsExecutor } """); - GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { var expandedTemplateArgNames = templateArgNames.Add("R"); string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames); @@ -74,7 +74,7 @@ internal sealed class ResultExtendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{bindTemplateDecl}} extensionFunc) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{bindTemplateDecl}} extensionFunc) { var result = await resultTask.ConfigureAwait(false); if (result.State == ResultState.Bottom) @@ -103,7 +103,7 @@ internal sealed class ResultExtendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} extensionFunc) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodSuffix}}{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} extensionFunc) { if (result.State == ResultState.Bottom) { @@ -131,7 +131,7 @@ internal sealed class ResultExtendExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} extensionFunc) + public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} extensionFunc) { var result = await resultTask.ConfigureAwait(false); if (result.State == ResultState.Bottom) diff --git a/Railway.SourceGenerator/ResultMapExecutor.cs b/Railway.SourceGenerator/ResultMapExecutor.cs index 65d62ea..abd50be 100644 --- a/Railway.SourceGenerator/ResultMapExecutor.cs +++ b/Railway.SourceGenerator/ResultMapExecutor.cs @@ -34,13 +34,13 @@ internal sealed class ResultMapExecutor : ResultExtensionsExecutor } """); - GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { var methodTemplateArgNames = templateArgNames.Add("R"); string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames); @@ -49,7 +49,7 @@ internal sealed class ResultMapExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultMapExecutor)}}", "1.0.0.0")] - public static async {{taskType}}> Map{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{methodTemplateDecl}} mapping) + public static async {{taskType}}> Map{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{methodTemplateDecl}} mapping) { var result = await resultTask.ConfigureAwait(false); return result.State switch @@ -64,7 +64,7 @@ internal sealed class ResultMapExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultMapExecutor)}}", "1.0.0.0")] - public static async {{taskType}}> Map{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} mapping) + public static async {{taskType}}> Map{{methodSuffix}}{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} mapping) { return result.State switch { @@ -78,7 +78,7 @@ internal sealed class ResultMapExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultMapExecutor)}}", "1.0.0.0")] - public static async {{taskType}}> Map{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} mapping) + public static async {{taskType}}> Map{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} mapping) { var result = await resultTask.ConfigureAwait(false); return result.State switch diff --git a/Railway.SourceGenerator/ResultMatchExecutor.cs b/Railway.SourceGenerator/ResultMatchExecutor.cs index 2bfb33c..bbe26fd 100644 --- a/Railway.SourceGenerator/ResultMatchExecutor.cs +++ b/Railway.SourceGenerator/ResultMatchExecutor.cs @@ -35,13 +35,13 @@ internal sealed class ResultMatchExecutor : ResultExtensionsExecutor } """); - GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { var methodTemplateArgNames = templateArgNames.Add("R"); string methodTemplateDecl = GenerateTemplateDecl(methodTemplateArgNames); @@ -50,7 +50,7 @@ internal sealed class ResultMatchExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultMatchExecutor)}}", "1.0.0.0")] - public static async {{taskType}} Match{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{methodTemplateDecl}} onSuccess, Func onFailure) + public static async {{taskType}} Match{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{methodTemplateDecl}} onSuccess, Func onFailure) { var result = await resultTask.ConfigureAwait(false); return result.State switch @@ -65,7 +65,7 @@ internal sealed class ResultMatchExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultMatchExecutor)}}", "1.0.0.0")] - public static {{taskType}} Match{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} onSuccess, Func> onFailure) + public static {{taskType}} Match{{methodSuffix}}{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} onSuccess, Func> onFailure) { return result.State switch { @@ -79,7 +79,7 @@ internal sealed class ResultMatchExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultMatchExecutor)}}", "1.0.0.0")] - public static async {{taskType}} Match{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} onSuccess, Func> onFailure) + public static async {{taskType}} Match{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} onSuccess, Func> onFailure) { var result = await resultTask.ConfigureAwait(false); var matchTask = result.State switch diff --git a/Railway.SourceGenerator/ResultTapExecutor.cs b/Railway.SourceGenerator/ResultTapExecutor.cs index 23cd335..cdb5139 100644 --- a/Railway.SourceGenerator/ResultTapExecutor.cs +++ b/Railway.SourceGenerator/ResultTapExecutor.cs @@ -41,13 +41,13 @@ internal sealed class ResultTapExecutor : ResultExtensionsExecutor } """); - GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion); - GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, resultValueExpansion); + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) + private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string resultValueExpansion) { string methodTemplateDecl = GenerateTemplateDecl(templateArgNames); string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add(taskType)); @@ -55,7 +55,7 @@ internal sealed class ResultTapExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultTapExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultTypeDef}}> Tap{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Action{{methodTemplateDecl}}? onSuccess = null, Action? onFailure = null) + public static async {{taskType}}<{{resultTypeDef}}> Tap{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Action{{methodTemplateDecl}}? onSuccess = null, Action? onFailure = null) { var result = await resultTask.ConfigureAwait(false); switch (result.State) @@ -76,7 +76,7 @@ internal sealed class ResultTapExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultTapExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultTypeDef}}> Tap{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}}? onSuccess = null, Func? onFailure = null) + public static async {{taskType}}<{{resultTypeDef}}> Tap{{methodSuffix}}{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}}? onSuccess = null, Func? onFailure = null) { switch (result.State) { @@ -98,7 +98,7 @@ internal sealed class ResultTapExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultTapExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultTypeDef}}> Tap{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}}? onSuccess = null, Func? onFailure = null) + public static async {{taskType}}<{{resultTypeDef}}> Tap{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}}? onSuccess = null, Func? onFailure = null) { var result = await resultTask.ConfigureAwait(false); switch (result.State) diff --git a/Railway.SourceGenerator/ResultTryRecoverExecutor.cs b/Railway.SourceGenerator/ResultTryRecoverExecutor.cs index ebcf11a..6db3ae2 100644 --- a/Railway.SourceGenerator/ResultTryRecoverExecutor.cs +++ b/Railway.SourceGenerator/ResultTryRecoverExecutor.cs @@ -33,17 +33,17 @@ internal sealed class ResultTryRecoverExecutor : ResultExtensionsExecutor } """); - GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, methodTemplateDecl); - GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, methodTemplateDecl); + GenerateAsyncMethods("Task", string.Empty, sb, templateArgNames, resultTypeDef, methodTemplateDecl); + GenerateAsyncMethods("ValueTask", "ValueTask", sb, templateArgNames, resultTypeDef, methodTemplateDecl); sb.AppendLine("#endregion"); } - private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string methodTemplateDecl) + private static void GenerateAsyncMethods(string taskType, string methodSuffix, StringBuilder sb, ImmutableArray templateArgNames, string resultTypeDef, string methodTemplateDecl) { sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func recover) + public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func recover) { var result = await resultTask.ConfigureAwait(false); return result.State switch @@ -57,7 +57,7 @@ internal sealed class ResultTryRecoverExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func> recover) + public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodSuffix}}{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func> recover) { return result.State switch { @@ -70,7 +70,7 @@ internal sealed class ResultTryRecoverExecutor : ResultExtensionsExecutor sb.AppendLine($$""" [PureAttribute] [GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")] - public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func> recover) + public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodSuffix}}{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func> recover) { var result = await resultTask.ConfigureAwait(false); return result.State switch diff --git a/Railway/Ensure.cs b/Railway/Ensure.cs index 6110367..8f64fe8 100644 --- a/Railway/Ensure.cs +++ b/Railway/Ensure.cs @@ -2,6 +2,7 @@ namespace Just.Railway; public static partial class Ensure { + [return: NotNull] public delegate Error ErrorFactory(string valueExpression); public const string DefaultErrorType = "EnsureFailed"; diff --git a/Railway/Error.cs b/Railway/Error.cs index 3159678..7c59dc9 100644 --- a/Railway/Error.cs +++ b/Railway/Error.cs @@ -14,7 +14,13 @@ public abstract class Error : IEquatable, IComparable /// /// Exception [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Error New(Exception thisException) => new ExceptionalError(thisException); + public static Error New(Exception thisException) => thisException switch + { + AggregateException { InnerExceptions.Count: > 1 } aggegateException => new ManyErrors(aggegateException.InnerExceptions.Select(x => Error.New(x))), + AggregateException { InnerExceptions.Count: 1 } aggegateException => new ExceptionalError(aggegateException.InnerExceptions[0]), + _ => new ExceptionalError(thisException), + }; + /// /// Create a with an overriden detail. This can be useful for sanitising the display message /// when internally we're carrying the exception. @@ -85,7 +91,7 @@ public abstract class Error : IEquatable, IComparable [Pure] public abstract int Count { get; } [Pure] public abstract bool IsEmpty { get; } [Pure] public abstract bool IsExpected { get; } - [Pure] public abstract bool IsExeptional { get; } + [Pure] public abstract bool IsExceptional { get; } [Pure] public Error Append(Error? next) { @@ -166,7 +172,7 @@ public sealed class ExpectedError : Error [Pure] public override int Count => 1; [Pure] public override bool IsEmpty => false; [Pure] public override bool IsExpected => true; - [Pure] public override bool IsExeptional => false; + [Pure] public override bool IsExceptional => false; [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public override IEnumerable ToEnumerable() @@ -208,7 +214,7 @@ public sealed class ExceptionalError : Error [Pure] public override int Count => 1; [Pure] public override bool IsEmpty => false; [Pure] public override bool IsExpected => false; - [Pure] public override bool IsExeptional => true; + [Pure] public override bool IsExceptional => true; [Pure] public override Exception ToException() => Exception ?? base.ToException(); @@ -220,7 +226,7 @@ public sealed class ExceptionalError : Error private static ImmutableDictionary ExtractExtensionData(Exception exception) { - if (!(exception.Data?.Count > 0)) + if (exception.Data is null || exception.Data.Count == 0) return ImmutableDictionary.Empty; var values = GetGenericExtData(exception); @@ -276,7 +282,7 @@ public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList { var unpackedErrors = ImmutableArray.CreateBuilder(); - foreach (var err in errors) + foreach (var err in errors.Where(x => x is not null)) { if (err.IsEmpty) continue; @@ -309,7 +315,7 @@ public sealed class ManyErrors : Error, IEnumerable, IReadOnlyList [Pure] public override int Count => _errors.Length; [Pure] public override bool IsEmpty => _errors.IsEmpty; [Pure] public override bool IsExpected => _errors.All(static x => x.IsExpected); - [Pure] public override bool IsExeptional => _errors.Any(static x => x.IsExeptional); + [Pure] public override bool IsExceptional => _errors.Any(static x => x.IsExceptional); [Pure] public Error this[int index] => _errors[index]; diff --git a/Railway/ErrorJsonConverter.cs b/Railway/ErrorJsonConverter.cs index 83d6441..cd697ad 100644 --- a/Railway/ErrorJsonConverter.cs +++ b/Railway/ErrorJsonConverter.cs @@ -8,7 +8,7 @@ public sealed class ErrorJsonConverter : JsonConverter { return reader.TokenType switch { - JsonTokenType.StartObject => ToExpectedError(ReadOne(ref reader)), + JsonTokenType.StartObject => ToExpectedError(ReadOne(ref reader)), // deserialization always produce ExpectedError by design JsonTokenType.StartArray => ReadMany(ref reader), JsonTokenType.None => null, JsonTokenType.Null => null, @@ -42,6 +42,7 @@ public sealed class ErrorJsonConverter : JsonConverter { errors.Add(ToExpectedError(ReadOne(ref reader))); } + else if (reader.TokenType == JsonTokenType.EndArray) break; } return new ManyErrors(errors.ToImmutable()); } @@ -73,11 +74,11 @@ public sealed class ErrorJsonConverter : JsonConverter if (string.IsNullOrEmpty(propvalue)) break; - if (propname == "type" || string.Equals(propname, "type", StringComparison.InvariantCultureIgnoreCase)) + if (string.Equals(propname, "type", StringComparison.InvariantCultureIgnoreCase)) { type = propvalue; } - else if (propname == "msg" || string.Equals(propname, "msg", StringComparison.InvariantCultureIgnoreCase)) + else if (string.Equals(propname, "msg", StringComparison.InvariantCultureIgnoreCase)) { message = propvalue; } diff --git a/Railway/Railway.csproj b/Railway/Railway.csproj index 657ec05..7c094d8 100644 --- a/Railway/Railway.csproj +++ b/Railway/Railway.csproj @@ -11,7 +11,7 @@ Base for railway-oriented programming in .NET. Package includes Result object, Error class and most of the common extensions. railway-oriented;functional;result-pattern;result-object;error-handling JustFixMe - Copyright (c) 2023-2025 JustFixMe + Copyright (c) 2023-2026 JustFixMe LICENSE README.md https://github.com/JustFixMe/Just.Railway/ diff --git a/Railway/Result.cs b/Railway/Result.cs index eac49ef..cbaff5c 100644 --- a/Railway/Result.cs +++ b/Railway/Result.cs @@ -64,7 +64,7 @@ public readonly partial struct Result : IEquatable public static implicit operator Result(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Result(Exception exception) => new( - new ExceptionalError(exception ?? throw new ArgumentNullException(nameof(exception)))); + Error.New(exception ?? throw new ArgumentNullException(nameof(exception)))); [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Result(Result result) => result.State switch { @@ -75,8 +75,8 @@ public readonly partial struct Result : IEquatable [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator Result(SuccessUnit _) => new(null); - [Pure] public bool IsSuccess => Error is null; - [Pure] public bool IsFailure => Error is not null; + [Pure] public bool IsSuccess => State == ResultState.Success; + [Pure] public bool IsFailure => State == ResultState.Error; [Pure] public bool TryGetValue([MaybeNullWhen(false)]out SuccessUnit? u, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error) { @@ -236,7 +236,7 @@ public readonly struct Result : IEquatable> if (typeof(R).IsAssignableFrom(typeof(T)) && Value is null) return default(R)!; - return (R)(object)Value!; + return (R)(object)Value!; // throws InvalidCastException by design. should not use cast on incompatible types } default: throw new ResultNotInitializedException(); diff --git a/Railway/ResultExtensions.cs b/Railway/ResultExtensions.cs index 8e02c6e..cbe0c99 100644 --- a/Railway/ResultExtensions.cs +++ b/Railway/ResultExtensions.cs @@ -98,7 +98,7 @@ public static partial class ResultExtensions case ResultState.Success: if (hasErrors) goto afterLoop; break; - + default: throw new ResultNotInitializedException(nameof(results)); } } @@ -145,7 +145,19 @@ public static partial class ResultExtensions } public static async Task>> Merge(this IEnumerable>> tasks) { - var results = await Task.WhenAll(tasks).ConfigureAwait(false); + var taskList = tasks.ToList(); + var results = new Result[taskList.Count]; + + for (int i = 0; i < taskList.Count; i++) + { + var task = await Task.WhenAny(taskList); + taskList.Remove(task); + + results[i] = task.IsCompletedSuccessfully + ? task.Result + : task.Exception!; + } + return results.Merge(); } diff --git a/Raliway.Tests/EnsureExtensions/Satisfy.cs b/Raliway.Tests/EnsureExtensions/Satisfy.cs index 4c84a1d..3f88384 100644 --- a/Raliway.Tests/EnsureExtensions/Satisfy.cs +++ b/Raliway.Tests/EnsureExtensions/Satisfy.cs @@ -10,8 +10,8 @@ public class Satisfy .LessThan(100) .Result(); - Assert.True(result.IsSuccess); - Assert.Equal(69, result.Value); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe(69); } [Fact] public void WhenRequirementWasNotSatisfied_ShouldBeFailureWithDefaultError() @@ -22,8 +22,8 @@ public class Satisfy .GreaterThan(100) .Result(); - Assert.True(result.IsFailure); - Assert.Equal(error, result.Error); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(error); } [Fact] @@ -37,8 +37,8 @@ public class Satisfy .EqualTo("69") .Result(); - Assert.True(result.IsSuccess); - Assert.Equal("69", result.Value); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe("69"); } [Fact] @@ -53,7 +53,7 @@ public class Satisfy .EqualTo("69") .Result(); - Assert.True(result.IsFailure); - Assert.Equal(error, result.Error); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(error); } } diff --git a/Raliway.Tests/Errors/Construction.cs b/Raliway.Tests/Errors/Construction.cs new file mode 100644 index 0000000..f31e3e1 --- /dev/null +++ b/Raliway.Tests/Errors/Construction.cs @@ -0,0 +1,572 @@ +namespace Raliway.Tests.Errors; + +public class Construction +{ + public class New_Factory + { + [Fact] + public void With_string_creates_ExpectedError_with_default_type() + { + // Given + // When + var error = Error.New("something went wrong"); + + // Then + error.ShouldBeOfType(); + error.Type.ShouldBe("error"); + error.Message.ShouldBe("something went wrong"); + error.IsExpected.ShouldBeTrue(); + error.IsExceptional.ShouldBeFalse(); + error.IsEmpty.ShouldBeFalse(); + error.Count.ShouldBe(1); + } + + [Fact] + public void With_type_and_message_creates_ExpectedError_with_custom_type() + { + // Given + // When + var error = Error.New("validation_error", "Name is required"); + + // Then + error.ShouldBeOfType(); + error.Type.ShouldBe("validation_error"); + error.Message.ShouldBe("Name is required"); + } + + [Fact] + public void With_Exception_creates_ExceptionalError() + { + // Given + var exception = new InvalidOperationException("bad state"); + + // When + var error = Error.New(exception); + + // Then + error.ShouldBeOfType(); + error.Type.ShouldBe("System.InvalidOperationException"); + error.Message.ShouldBe("bad state"); + error.IsExpected.ShouldBeFalse(); + error.IsExceptional.ShouldBeTrue(); + error.IsEmpty.ShouldBeFalse(); + error.Count.ShouldBe(1); + } + + [Fact] + public void With_message_and_Exception_overrides_message_preserves_exception() + { + // Given + var exception = new InvalidOperationException("internal detail"); + + // When + var error = Error.New("sanitized message", exception); + + // Then + error.ShouldBeOfType(); + error.Type.ShouldBe("System.InvalidOperationException"); + error.Message.ShouldBe("sanitized message"); + error.IsExceptional.ShouldBeTrue(); + var ex = error.ToException(); + ex.ShouldBe(exception); + } + + [Fact] + public void With_AggregateException_many_inners_creates_ManyErrors() + { + // Given + var inner1 = new InvalidOperationException("first"); + var inner2 = new ArgumentException("second"); + var aggregate = new AggregateException(inner1, inner2); + + // When + var error = Error.New(aggregate); + + // Then + error.ShouldBeOfType(); + var many = (ManyErrors)error; + many.Count.ShouldBe(2); + many[0].Type.ShouldBe("System.InvalidOperationException"); + many[0].Message.ShouldBe("first"); + many[1].Type.ShouldBe("System.ArgumentException"); + many[1].Message.ShouldBe("second"); + } + + [Fact] + public void With_AggregateException_single_inner_creates_ExceptionalError() + { + // Given + var inner = new InvalidOperationException("only one"); + var aggregate = new AggregateException(inner); + + // When + var error = Error.New(aggregate); + + // Then + error.ShouldBeOfType(); + error.Type.ShouldBe("System.InvalidOperationException"); + error.Message.ShouldBe("only one"); + } + + [Fact] + public void With_AggregateException_zero_inners_creates_ExceptionalError() + { + // Given + var aggregate = new AggregateException(); + + // When + var error = Error.New(aggregate); + + // Then + error.ShouldBeOfType(); + error.Type.ShouldBe("System.AggregateException"); + } + + [Fact] + public void With_nested_AggregateException_recursively_unwraps() + { + // Given + var inner1 = new InvalidOperationException("first"); + var inner2 = new ArgumentException("second"); + var nestedAggregate = new AggregateException(inner1, inner2); + // Outer must have >1 inners to trigger ManyErrors path; one of them is itself an AggregateException + var outerAggregate = new AggregateException(nestedAggregate, new InvalidOperationException("extra")); + + // When + var error = Error.New(outerAggregate); + + // Then + error.ShouldBeOfType(); + var many = (ManyErrors)error; + many.Count.ShouldBe(3); // nested 2-flattened + extra + } + } + + public class Many_Factory + { + [Fact] + public void With_both_null_returns_empty_ManyErrors() + { + // Given + // When + var error = Error.Many(null!, null!); + + // Then + error.ShouldBeOfType(); + error.IsEmpty.ShouldBeTrue(); + error.Count.ShouldBe(0); + } + + [Fact] + public void With_error_and_null_returns_the_error() + { + // Given + var expected = Error.New("test"); + + // When + var result = Error.Many(expected, null!); + + // Then + result.ShouldBe(expected); + result.ShouldBeOfType(); + } + + [Fact] + public void With_null_and_error_returns_the_error() + { + // Given + var expected = Error.New("test"); + + // When + var result = Error.Many(null!, expected); + + // Then + result.ShouldBe(expected); + result.ShouldBeOfType(); + } + + [Fact] + public void Params_with_null_element_should_not_throw() + { + // Given + Error?[] errors = [Error.New("first"), null!, Error.New("second")]; + + // When + var exception = Record.Exception(() => Error.Many(errors!)); + + // Then + exception.ShouldBeNull(); + } + + [Fact] + public void Params_with_null_element_filters_null() + { + // Given + Error?[] errors = [Error.New("first"), null!, Error.New("second")]; + + // When + var result = Error.Many(errors!); + + // Then + result.ShouldBeOfType(); + var many = (ManyErrors)result; + many.Count.ShouldBe(2); + many[0].Message.ShouldBe("first"); + many[1].Message.ShouldBe("second"); + } + + [Fact] + public void Params_with_empty_array_returns_empty_ManyErrors() + { + // Given + // When + var result = Error.Many(Array.Empty()); + + // Then + result.ShouldBeOfType(); + result.IsEmpty.ShouldBeTrue(); + result.Count.ShouldBe(0); + } + + [Fact] + public void With_single_error_returns_it_unwrapped() + { + // Given + var single = Error.New("only one"); + + // When + var result = Error.Many(single); + + // Then + result.ShouldBe(single); + result.ShouldBeOfType(); + } + + [Fact] + public void With_ManyErrors_and_error_flattens() + { + // Given + var first = Error.New("first"); + var second = Error.New("second"); + var many = Error.Many(first, second); + var third = Error.New("third"); + + // When + var result = Error.Many(many, third); + + // Then + result.ShouldBeOfType(); + var resultMany = (ManyErrors)result; + resultMany.Count.ShouldBe(3); + resultMany[0].Message.ShouldBe("first"); + resultMany[1].Message.ShouldBe("second"); + resultMany[2].Message.ShouldBe("third"); + } + } + + public class Operator_Null_Safety + { + [Fact] + public void Error_plus_null_returns_original_error() + { + // Given + var error = Error.New("test"); + + // When + var result = error + null; + + // Then + result.ShouldBe(error); + } + + [Fact] + public void Null_plus_error_returns_the_error() + { + // Given + var error = Error.New("test"); + + // When + var result = null + error; + + // Then + result.ShouldBe(error); + } + + [Fact] + public void Null_plus_null_is_null() + { + // Given + // When + var result = (Error?)null + (Error?)null; + + // Then + result.ShouldBeNull(); + } + } + + public class Append_Edge_Cases + { + [Fact] + public void Append_null_returns_original_error() + { + // Given + var error = Error.New("test"); + + // When + var result = error.Append(null); + + // Then + result.ShouldBe(error); + } + + [Fact] + public void Append_empty_ManyErrors_returns_original_error() + { + // Given + var error = Error.New("test"); + var empty = Error.Many(Array.Empty()); + + // When + var result = error.Append(empty); + + // Then + result.ShouldBe(error); + } + + [Fact] + public void Empty_append_error_returns_the_appended_error() + { + // Given + var empty = Error.Many(Array.Empty()); + var error = Error.New("test"); + + // When + var result = empty.Append(error); + + // Then + result.ShouldBe(error); + } + } + + public class Property_Correctness + { + [Fact] + public void ExpectedError_IsEmpty_is_false() + { + var error = Error.New("test"); + error.IsEmpty.ShouldBeFalse(); + } + + [Fact] + public void ExceptionalError_IsEmpty_is_false() + { + var error = Error.New(new Exception("test")); + error.IsEmpty.ShouldBeFalse(); + } + + [Fact] + public void ManyErrors_IsEmpty_when_empty() + { + var error = Error.Many(Array.Empty()); + error.IsEmpty.ShouldBeTrue(); + } + + [Fact] + public void ManyErrors_IsExpected_when_all_are_expected() + { + // Given + var errors = Error.Many( + Error.New("err1"), + Error.New("err2")); + + // When / Then + errors.IsExpected.ShouldBeTrue(); + } + + [Fact] + public void ManyErrors_IsExpected_when_mixed_is_false() + { + // Given + var errors = Error.Many( + Error.New("err1"), + Error.New(new InvalidOperationException("boom"))); + + // When / Then + errors.IsExpected.ShouldBeFalse(); + } + + [Fact] + public void ManyErrors_IsExceptional_when_any_is_exceptional() + { + // Given + var errors = Error.Many( + Error.New("err1"), + Error.New(new InvalidOperationException("boom"))); + + // When / Then + errors.IsExceptional.ShouldBeTrue(); + } + + [Fact] + public void ManyErrors_IsExceptional_when_none_is_false() + { + // Given + var errors = Error.Many( + Error.New("err1"), + Error.New("err2")); + + // When / Then + errors.IsExceptional.ShouldBeFalse(); + } + } + + public class ToException + { + [Fact] + public void ExceptionalError_ToException_returns_original_exception() + { + // Given + var original = new InvalidOperationException("boom"); + var error = Error.New(original); + + // When + var result = error.ToException(); + + // Then + result.ShouldBe(original); + } + + [Fact] + public void ExceptionalError_without_exception_falls_back_to_ErrorException() + { + // Given + // Use deserialization path — creates ExceptionalError without internal Exception + var json = """{"type":"System.Exception","msg":"deserialized error"}"""; + var error = JsonSerializer.Deserialize(json)!; + + // When + var result = error.ToException(); + + // Then + result.ShouldNotBeOfType(); + result.ShouldBeOfType(); + var errEx = (ErrorException)result; + errEx.Type.ShouldBe("System.Exception"); + errEx.Message.ShouldBe("deserialized error"); + } + + [Fact] + public void ManyErrors_ToException_returns_AggregateException() + { + // Given + var errors = Error.Many( + Error.New("first"), + Error.New("second")); + + // When + var result = errors.ToException(); + + // Then + result.ShouldBeOfType(); + var aggregate = (AggregateException)result; + aggregate.InnerExceptions.Count.ShouldBe(2); + aggregate.InnerExceptions[0].Message.ShouldBe("first"); + aggregate.InnerExceptions[1].Message.ShouldBe("second"); + } + } + + public class ExtensionData + { + [Fact] + public void Extracted_from_Exception_Data() + { + // Given + var exception = new InvalidOperationException("boom"); + exception.Data["key1"] = "value1"; + exception.Data["key2"] = 42; // int value → ToString → "42" + + // When + var error = Error.New(exception); + + // Then + error.ExtensionData["key1"].ShouldBe("value1"); + error.ExtensionData["key2"].ShouldBe("42"); + } + + [Fact] + public void Exception_Data_with_null_value_is_skipped() + { + // Given + var exception = new InvalidOperationException("boom"); + exception.Data["key"] = null; + + // When + var error = Error.New(exception); + + // Then + error.ExtensionData.Count.ShouldBe(0); + } + + [Fact] + public void Exception_without_Data_has_empty_ExtensionData() + { + // Given + var exception = new InvalidOperationException("boom"); + + // When + var error = Error.New(exception); + + // Then + error.ExtensionData.ShouldNotBeNull(); + error.ExtensionData.IsEmpty.ShouldBeTrue(); + } + } + + public class Indexer + { + [Fact] + public void Existing_key_returns_value() + { + // Given + var exception = new InvalidOperationException("boom"); + exception.Data["mykey"] = "myvalue"; + var error = Error.New(exception); + + // When + var value = error["mykey"]; + + // Then + value.ShouldBe("myvalue"); + } + + [Fact] + public void Missing_key_returns_null() + { + // Given + var error = Error.New("test"); + + // When + var value = error["nonexistent"]; + + // Then + value.ShouldBeNull(); + } + } + + public class Deconstruct + { + [Fact] + public void Produces_Type_and_Message() + { + // Given + var error = Error.New("custom_type", "custom message"); + + // When + var (type, message) = error; + + // Then + type.ShouldBe("custom_type"); + message.ShouldBe("custom message"); + } + } +} diff --git a/Raliway.Tests/Errors/Equality.cs b/Raliway.Tests/Errors/Equality.cs new file mode 100644 index 0000000..3818bf6 --- /dev/null +++ b/Raliway.Tests/Errors/Equality.cs @@ -0,0 +1,225 @@ +namespace Raliway.Tests.Errors; + +public class Equality +{ + public class Equals_Tests + { + [Fact] + public void Identical_ExpectedErrors_are_equal() + { + var a = Error.New("custom_type", "some message"); + var b = Error.New("custom_type", "some message"); + + a.Equals(b).ShouldBeTrue(); + b.Equals(a).ShouldBeTrue(); + (a == b).ShouldBeTrue(); + } + + [Fact] + public void Different_type_ExpectedErrors_are_not_equal() + { + var a = Error.New("type_a", "same message"); + var b = Error.New("type_b", "same message"); + + a.Equals(b).ShouldBeFalse(); + (a == b).ShouldBeFalse(); + (a != b).ShouldBeTrue(); + } + + [Fact] + public void Different_message_ExpectedErrors_are_not_equal() + { + var a = Error.New("same_type", "message a"); + var b = Error.New("same_type", "message b"); + + a.Equals(b).ShouldBeFalse(); + (a == b).ShouldBeFalse(); + } + + [Fact] + public void Same_Type_and_Message_but_different_ExtensionData_are_still_equal() + { + var a = Error.New("type", "msg", new Dictionary { ["key"] = "a" }); + var b = Error.New("type", "msg", new Dictionary { ["key"] = "b" }); + + a.Equals(b).ShouldBeTrue(); + (a == b).ShouldBeTrue(); + } + + [Fact] + public void ExpectedError_and_ExceptionalError_with_same_Type_and_Message_are_equal() + { + Error expected = Error.New("shared_type", "shared message"); + Error exceptional = new ExceptionalError("shared_type", "shared message"); + + expected.Equals(exceptional).ShouldBeTrue(); + exceptional.Equals(expected).ShouldBeTrue(); + (expected == exceptional).ShouldBeTrue(); + } + + [Fact] + public void ManyErrors_with_same_elements_same_order_are_equal() + { + var a = Error.Many(Error.New("a"), Error.New("b"), Error.New("c")); + var b = Error.Many(Error.New("a"), Error.New("b"), Error.New("c")); + + a.Equals(b).ShouldBeTrue(); + (a == b).ShouldBeTrue(); + } + + [Fact] + public void ManyErrors_with_same_elements_different_order_are_not_equal() + { + var a = Error.Many(Error.New("a"), Error.New("b")); + var b = Error.Many(Error.New("b"), Error.New("a")); + + a.Equals(b).ShouldBeFalse(); + (a == b).ShouldBeFalse(); + } + } + + public class CompareTo_Tests + { + [Fact] + public void CompareTo_null_returns_minus_one() + { + var error = Error.New("type", "msg"); + + error.CompareTo(null).ShouldBe(-1); + } + + [Fact] + public void CompareTo_same_returns_zero() + { + var a = Error.New("type", "msg"); + var b = Error.New("type", "msg"); + + a.CompareTo(b).ShouldBe(0); + } + + [Fact] + public void CompareTo_different_type_compares_by_type() + { + var a = Error.New("aaa", "msg"); + var b = Error.New("bbb", "msg"); + + a.CompareTo(b).ShouldBeLessThan(0); + b.CompareTo(a).ShouldBeGreaterThan(0); + } + + [Fact] + public void CompareTo_same_type_different_message_compares_by_message() + { + var a = Error.New("type", "aaa"); + var b = Error.New("type", "bbb"); + + a.CompareTo(b).ShouldBeLessThan(0); + b.CompareTo(a).ShouldBeGreaterThan(0); + } + + [Fact] + public void ManyErrors_CompareTo_different_length_compares_by_count() + { + var shorter = Error.Many(Error.New("a"), Error.New("b")); + var longer = Error.Many(Error.New("a"), Error.New("b"), Error.New("c")); + + shorter.CompareTo(longer).ShouldBeLessThan(0); + longer.CompareTo(shorter).ShouldBeGreaterThan(0); + } + + [Fact] + public void ManyErrors_CompareTo_same_length_element_by_element() + { + var a = Error.Many(Error.New("a"), Error.New("b")); + var b = Error.Many(Error.New("a"), Error.New("c")); // differs at position 1 + + a.CompareTo(b).ShouldBeLessThan(0); + b.CompareTo(a).ShouldBeGreaterThan(0); + } + } + + public class IsSimilarTo_Tests + { + [Fact] + public void Same_Type_IsSimilarTo_returns_true_ignoring_Message() + { + var a = Error.New("custom_type", "message one"); + var b = Error.New("custom_type", "message two"); + + a.IsSimilarTo(b).ShouldBeTrue(); + } + + [Fact] + public void Different_Type_IsSimilarTo_returns_false() + { + var a = Error.New("type_a", "same message"); + var b = Error.New("type_b", "same message"); + + a.IsSimilarTo(b).ShouldBeFalse(); + } + + [Fact] + public void IsSimilarTo_null_returns_false() + { + var error = Error.New("type", "msg"); + + error.IsSimilarTo(null!).ShouldBeFalse(); + } + + [Fact] + public void ManyErrors_IsSimilarTo_different_count_returns_false() + { + var a = Error.Many(Error.New("a"), Error.New("b")); + var b = Error.Many(Error.New("a"), Error.New("b"), Error.New("c")); + + a.IsSimilarTo(b).ShouldBeFalse(); + } + + [Fact] + public void ManyErrors_IsSimilarTo_same_types_different_messages_returns_true() + { + var a = Error.Many(Error.New("type", "msg1")); + var b = Error.Many(Error.New("type", "msg2")); + + a.IsSimilarTo(b).ShouldBeTrue(); + } + + [Fact] + public void ManyErrors_IsSimilarTo_different_type_at_same_position_returns_false() + { + var a = Error.Many(Error.New("type_a", "msg"), Error.New("type_b", "msg")); + var b = Error.Many(Error.New("type_a", "msg"), Error.New("type_c", "msg")); + + a.IsSimilarTo(b).ShouldBeFalse(); + } + } + + public class GetHashCode_Tests + { + [Fact] + public void Equal_errors_have_same_hashcode() + { + var a = Error.New("type", "msg"); + var b = Error.New("type", "msg"); + + a.GetHashCode().ShouldBe(b.GetHashCode()); + } + + [Fact] + public void Equal_ManyErrors_have_same_hashcode() + { + var a = Error.Many(Error.New("a"), Error.New("b")); + var b = Error.Many(Error.New("a"), Error.New("b")); + + a.GetHashCode().ShouldBe(b.GetHashCode()); + } + + [Fact] + public void Empty_ManyErrors_hashcode_is_zero() + { + var empty = Error.Many(Array.Empty()); + + empty.GetHashCode().ShouldBe(0); + } + } +} diff --git a/Raliway.Tests/Errors/Serialization.cs b/Raliway.Tests/Errors/Serialization.cs index 30dda86..267312a 100644 --- a/Raliway.Tests/Errors/Serialization.cs +++ b/Raliway.Tests/Errors/Serialization.cs @@ -1,4 +1,4 @@ -namespace Railway.Tests.Errors; +namespace Raliway.Tests.Errors; public class Serialization { @@ -8,18 +8,17 @@ public class Serialization // Given Error many_errors = new ManyErrors( [ - Error.New("err1", "msg1", new KeyValuePair[] - { + Error.New("err1", "msg1", + [ new("ext", "ext_value"), - }), + ]), Error.New(new Exception("msg2")), ]); // When var result = JsonSerializer.Serialize(many_errors); // Then - Assert.Equal( - expected: "[{\"type\":\"err1\",\"msg\":\"msg1\",\"ext\":\"ext_value\"},{\"type\":\"System.Exception\",\"msg\":\"msg2\"}]", - result); + result.ShouldBe( + "[{\"type\":\"err1\",\"msg\":\"msg1\",\"ext\":\"ext_value\"},{\"type\":\"System.Exception\",\"msg\":\"msg2\"}]"); } [Fact] @@ -30,23 +29,17 @@ public class Serialization // When var result = JsonSerializer.Deserialize(json); // Then - Assert.IsType(result); + result.ShouldBeOfType(); ManyErrors manyErrors = (ManyErrors)result; - Assert.True(manyErrors.Count == 2); - Assert.Equal( - expected: Error.Many( + manyErrors.Count.ShouldBe(2); + manyErrors.ShouldBe( + Error.Many( Error.New("err1", "msg1"), Error.New(new Exception("msg2")) - ).ToEnumerable(), - manyErrors - ); - Assert.Equal( - expected: "ext_value1", - manyErrors[0]["ext1"]); - Assert.Equal( - expected: "ext_value2", - manyErrors[0]["ext2"]); + ).ToEnumerable()); + manyErrors[0]["ext1"].ShouldBe("ext_value1"); + manyErrors[0]["ext2"].ShouldBe("ext_value2"); } [Fact] @@ -57,20 +50,14 @@ public class Serialization // When var result = JsonSerializer.Deserialize(json); // Then - Assert.NotNull(result); - Assert.True(result.Count == 2); - Assert.Equal( - expected: Error.Many( + result.ShouldNotBeNull(); + result.Count.ShouldBe(2); + result.ShouldBe( + Error.Many( Error.New("err1", "msg1"), Error.New(new Exception("msg2")) - ).ToEnumerable(), - result - ); - Assert.Equal( - expected: "ext_value1", - result[0]["ext1"]); - Assert.Equal( - expected: "ext_value2", - result[0]["ext2"]); + ).ToEnumerable()); + result[0]["ext1"].ShouldBe("ext_value1"); + result[0]["ext2"].ShouldBe("ext_value2"); } } diff --git a/Raliway.Tests/Errors/Serialization_RoundTrip.cs b/Raliway.Tests/Errors/Serialization_RoundTrip.cs new file mode 100644 index 0000000..91bdc72 --- /dev/null +++ b/Raliway.Tests/Errors/Serialization_RoundTrip.cs @@ -0,0 +1,226 @@ +namespace Raliway.Tests.Errors; + +public class Serialization_RoundTrip +{ + public class ExpectedError_Tests + { + [Fact] + public void Round_trip_as_Error_preserves_Type_and_Message() + { + // Given + Error original = Error.New("custom_type", "custom message"); + + // When + var json = JsonSerializer.Serialize(original); + var deserialized = JsonSerializer.Deserialize(json); + + // Then + deserialized.ShouldBeOfType(); + deserialized!.Type.ShouldBe("custom_type"); + deserialized.Message.ShouldBe("custom message"); + deserialized.IsExpected.ShouldBeTrue(); + } + + [Fact] + public void Round_trip_as_ExpectedError_preserves_Type_and_Message() + { + // Given + var original = new ExpectedError("custom_type", "custom message"); + + // When + var json = JsonSerializer.Serialize(original); + var deserialized = JsonSerializer.Deserialize(json); + + // Then + deserialized!.Type.ShouldBe("custom_type"); + deserialized.Message.ShouldBe("custom message"); + } + + [Fact] + public void Deserialize_single_object_as_Error_produces_ExpectedError() + { + // Given + var json = """{"type":"foo","msg":"bar"}"""; + + // When + var error = JsonSerializer.Deserialize(json); + + // Then + error.ShouldBeOfType(); + error!.Type.ShouldBe("foo"); + error.Message.ShouldBe("bar"); + } + + [Fact] + public void Unknown_properties_become_ExtensionData() + { + // Given + var json = """{"type":"err","msg":"details","meta_key":"meta_value","flag":"1"}"""; + + // When + var error = JsonSerializer.Deserialize(json); + + // Then + error.ShouldBeOfType(); + error!.ExtensionData["meta_key"].ShouldBe("meta_value"); + error.ExtensionData["flag"].ShouldBe("1"); + } + } + + public class ExceptionalError_Tests + { + [Fact] + public void Round_trip_as_ExceptionalError_preserves_Type_and_Message() + { + // Given + Error original = Error.New(new InvalidOperationException("boom")); + + // When + var json = JsonSerializer.Serialize(original); + var deserialized = JsonSerializer.Deserialize(json); + + // Then + deserialized.ShouldBeOfType(); + deserialized!.Type.ShouldBe("System.InvalidOperationException"); + deserialized.Message.ShouldBe("boom"); + deserialized.IsExceptional.ShouldBeTrue(); + } + + [Fact] + public void ExceptionalError_serialized_then_deserialized_as_Error_becomes_ExpectedError() + { + // ErrorJsonConverter.Read always produces ExpectedError for JSON objects. + + // Given + Error original = Error.New(new InvalidOperationException("boom")); + var json = JsonSerializer.Serialize(original); + + // When — deserialize as the base Error type + var deserialized = JsonSerializer.Deserialize(json); + + // Then — should NOT be an ExceptionalError (known bug) + deserialized.ShouldBeOfType(); + deserialized!.Type.ShouldBe("System.InvalidOperationException"); + deserialized.Message.ShouldBe("boom"); + deserialized.IsExceptional.ShouldBeFalse(); + deserialized.IsExpected.ShouldBeTrue(); + } + } + + public class ManyErrors_Tests + { + [Fact] + public void Round_trip_preserves_all_elements() + { + // Given + Error original = Error.Many( + Error.New("err1", "first error"), + Error.New("err2", "second error")); + + // When + var json = JsonSerializer.Serialize(original); + var deserialized = JsonSerializer.Deserialize(json); + + // Then + deserialized.ShouldBeOfType(); + var many = (ManyErrors)deserialized!; + many.Count.ShouldBe(2); + many[0].Type.ShouldBe("err1"); + many[0].Message.ShouldBe("first error"); + many[1].Type.ShouldBe("err2"); + many[1].Message.ShouldBe("second error"); + } + + [Fact] + public void Round_trip_as_ManyErrors_preserves_elements() + { + // Given + var original = new ManyErrors([ + Error.New("err1", "first"), + Error.New("err2", "second") + ]); + + // When + var json = JsonSerializer.Serialize(original); + var deserialized = JsonSerializer.Deserialize(json); + + // Then + deserialized.ShouldBeOfType(); + deserialized!.Count.ShouldBe(2); + deserialized[0].Type.ShouldBe("err1"); + deserialized[1].Type.ShouldBe("err2"); + } + + [Fact] + public void Empty_ManyErrors_round_trip() + { + // Given + Error original = Error.Many(Array.Empty()); + + // When + var json = JsonSerializer.Serialize(original); + var deserialized = JsonSerializer.Deserialize(json); + + // Then + deserialized.ShouldBeOfType(); + deserialized!.IsEmpty.ShouldBeTrue(); + deserialized.Count.ShouldBe(0); + } + + [Fact] + public void ExtensionData_round_trip_within_ManyErrors() + { + // Given + Error original = Error.Many( + Error.New("err", "msg", new Dictionary { ["meta"] = "value" }), + Error.New("err2", "msg2")); + + // When + var json = JsonSerializer.Serialize(original); + var deserialized = JsonSerializer.Deserialize(json); + + // Then + var many = (ManyErrors)deserialized!; + many[0]["meta"].ShouldBe("value"); + } + } + + public class Null_Handling + { + [Fact] + public void Deserialize_null_JSON_returns_null() + { + // Given + var json = "null"; + + // When + var result = JsonSerializer.Deserialize(json); + + // Then + result.ShouldBeNull(); + } + } + + public class Error_Handling + { + [Fact] + public void Non_string_property_value_throws_JsonException() + { + // Given + var json = """{"type":"err","msg":"ok","bad":42}"""; + + // When / Then + Should.Throw(() => JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Malformed_JSON_throws_JsonException() + { + // Given + var json = "{not valid json at all"; + + // When / Then + Should.Throw(() => JsonSerializer.Deserialize(json)); + } + } +} diff --git a/Raliway.Tests/Raliway.Tests.csproj b/Raliway.Tests/Raliway.Tests.csproj index 2f53680..ff39b60 100644 --- a/Raliway.Tests/Raliway.Tests.csproj +++ b/Raliway.Tests/Raliway.Tests.csproj @@ -10,11 +10,14 @@ false true + true + true - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Raliway.Tests/Results/Combine.cs b/Raliway.Tests/Results/Combine.cs index 4c25450..6d3b020 100644 --- a/Raliway.Tests/Results/Combine.cs +++ b/Raliway.Tests/Results/Combine.cs @@ -11,8 +11,8 @@ public class Combine // When var result = Result.Combine(result1, result2); // Then - Assert.True(result.IsFailure); - Assert.Equal(result2.Error, result.Error); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(result2.Error); } [Fact] public void TwoResultCombination_WhenThereAreTwoErrors() @@ -23,8 +23,8 @@ public class Combine // When var result = Result.Combine(result1, result2); // Then - Assert.True(result.IsFailure); - Assert.Equal(result1.Error + result2.Error, result.Error); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(result1.Error + result2.Error); } [Fact] public void TwoResultCombination_WhenThereIsNoError() @@ -35,7 +35,7 @@ public class Combine // When var result = Result.Combine(result1, result2); // Then - Assert.True(result.IsSuccess); + result.IsSuccess.ShouldBeTrue(); } [Fact] public void ThreeResultCombination_WhenThereIsAnError() @@ -47,8 +47,8 @@ public class Combine // When Result<(int, double)> result = Result.Combine(result1, result2, result3); // Then - Assert.True(result.IsFailure); - Assert.Equal(result3.Error, result.Error); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(result3.Error); } [Fact] public void ThreeResultCombination_WhenThereAreTwoErrors() @@ -60,8 +60,8 @@ public class Combine // When Result<(int?, double)> result = Result.Combine(result1, result2, result3); // Then - Assert.True(result.IsFailure); - Assert.Equal(result1.Error + result3.Error, result.Error); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(result1.Error + result3.Error); } [Fact] public void ThreeResultCombination_WhenThereIsNoError() @@ -73,6 +73,6 @@ public class Combine // When var result = Result.Combine(result1, result2, result3); // Then - Assert.True(result.IsSuccess); + result.IsSuccess.ShouldBeTrue(); } } diff --git a/Raliway.Tests/Results/GeneralUsage.cs b/Raliway.Tests/Results/GeneralUsage.cs index 998b318..67470a9 100644 --- a/Raliway.Tests/Results/GeneralUsage.cs +++ b/Raliway.Tests/Results/GeneralUsage.cs @@ -18,12 +18,12 @@ public class GeneralUsage onSuccess: s => s.ToUpper(), onFailure: _ => { - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return ""; } ); - Assert.Equal("TEST_1;SOME", result); + result.ShouldBe("TEST_1;SOME"); } [Fact] @@ -39,30 +39,30 @@ public class GeneralUsage .Append("test") .Map((i, s) => { - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return ""; }) .Append("some") - .Bind((s1, s2) => + .Bind((s1, s2) => { - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return Result.Success(""); }) .Match( - onSuccess: _ => + onSuccess: _ => { - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return ""; }, onFailure: err => { - Assert.Equal(error, err); + err.ShouldBe(error); return "satisfied"; } ); // Then - Assert.Equal("satisfied", result); + result.ShouldBe("satisfied"); } [Fact] @@ -72,7 +72,7 @@ public class GeneralUsage // When var result = await Result.Success() - .Append(() => ValueTask.FromResult(Result.Success(1))) + .Append(() => Task.FromResult(Result.Success(1))) .Append("test") .Map((i, s) => $"{s}_{i}") .Append("some") @@ -81,12 +81,12 @@ public class GeneralUsage onSuccess: s => s.ToUpper(), onFailure: _ => { - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return ""; } ); - Assert.Equal("TEST_1;SOME", result); + result.ShouldBe("TEST_1;SOME"); } [Fact] @@ -102,31 +102,31 @@ public class GeneralUsage .Append("test") .Map((i, s) => { - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return ""; }) .Append("some") .Bind(async (s1, s2) => { - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); await Task.CompletedTask; return Result.Success(""); }) .Match( - onSuccess: _ => + onSuccess: _ => { - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return ""; }, onFailure: err => { - Assert.Equal(error, err); + err.ShouldBe(error); return "satisfied"; } ); // Then - Assert.Equal("satisfied", result); + result.ShouldBe("satisfied"); } [Fact] @@ -137,17 +137,17 @@ public class GeneralUsage // When var result = failed.TryRecover(err => { - Assert.IsType(err.ToException()); + err.ToException().ShouldBeOfType(); if (err.Type == "System.NotImplementedException") return "recovered"; - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return ""; }); // Then - Assert.True(result.IsSuccess); - Assert.Equal("recovered", result.Value); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe("recovered"); } [Fact] @@ -162,12 +162,12 @@ public class GeneralUsage if (err.Type == "System.NotImplementedException") return error; - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return ""; }); // Then - Assert.True(result.IsFailure); - Assert.Equal(error, result.Error); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(error); } [Fact] @@ -179,8 +179,8 @@ public class GeneralUsage var result = success .Extend((i, s) => Result.Success($"{i} + {s}")); - Assert.True(result.IsSuccess); - Assert.Equal((1, "2", "1 + 2"), result.Value); + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe((1, "2", "1 + 2")); } [Fact] @@ -192,12 +192,12 @@ public class GeneralUsage var result = failure .Extend((i, s) => { - Assert.Fail(); + true.ShouldBeFalse("should not reach this path"); return Result.Success(""); }); - Assert.True(result.IsFailure); - Assert.Equal(Error.New("failure"), result.Error); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(Error.New("failure")); } [Fact] @@ -209,7 +209,7 @@ public class GeneralUsage var result = success .Extend((i, s) => Result.Failure("failure")); - Assert.True(result.IsFailure); - Assert.Equal(Error.New("failure"), result.Error); + result.IsFailure.ShouldBeTrue(); + result.Error.ShouldBe(Error.New("failure")); } } diff --git a/Raliway.Tests/Usings.cs b/Raliway.Tests/Usings.cs index 999ce38..769895b 100644 --- a/Raliway.Tests/Usings.cs +++ b/Raliway.Tests/Usings.cs @@ -1,3 +1,4 @@ global using System.Text.Json; global using Xunit; +global using Shouldly; global using Just.Railway;