11 Commits
v1.0.0 ... main

Author SHA1 Message Date
0f45b6b463 added dotnet 10 support
All checks were successful
.NET Test / .NET tests (push) Successful in 2m9s
.NET Publish / .NET tests (push) Successful in 1m0s
2025-11-11 22:32:04 +04:00
9485ad3ec4 update Copyright
All checks were successful
.NET Test / test (push) Successful in 1m1s
.NET Publish / publish (push) Successful in 56s
2025-07-31 23:35:06 +04:00
595b63dc23 support for dotnet9 compilation
All checks were successful
.NET Test / test (push) Successful in 2m2s
2025-07-29 23:22:28 +04:00
dd81fecdd6 added Extend extension methods generation
All checks were successful
.NET Test / test (push) Successful in 1m56s
.NET Publish / publish (push) Successful in 1m6s
2024-04-08 22:46:34 +04:00
9c9b734c51 switched to ImmutableArray as underlying type for ManyErrors
All checks were successful
.NET Test / test (push) Successful in 3m32s
2024-02-29 19:49:44 +04:00
f2f0221f76 added NotEqualTo
All checks were successful
.NET Test / test (push) Successful in 1m21s
.NET Publish / publish (push) Successful in 52s
2024-02-11 00:31:22 +04:00
e909eeae10 extended Ensure api
All checks were successful
.NET Test / test (push) Successful in 1m10s
2024-02-11 00:11:14 +04:00
719b4e85f5 extended recover tests
All checks were successful
.NET Test / test (push) Successful in 4m42s
.NET Publish / publish (push) Successful in 5m53s
2023-12-18 20:55:48 +04:00
3d34a3021d added TryRecover extensions
All checks were successful
.NET Test / test (push) Successful in 1m14s
2023-12-18 18:24:15 +04:00
57e83fbafa removed some new lang features to support net6.0 and net7.0
All checks were successful
.NET Test / test (push) Successful in 1m5s
.NET Publish / publish (push) Successful in 51s
2023-12-15 12:37:39 +04:00
c02fdc5492 added more nuget info
All checks were successful
.NET Test / test (push) Successful in 29m24s
.NET Publish / publish (push) Successful in 10m18s
2023-12-13 20:15:56 +04:00
24 changed files with 952 additions and 245 deletions

View File

@@ -9,14 +9,18 @@ on:
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: .NET tests
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v5
- name: Setup .NET - name: Setup .NET
uses: https://github.com/actions/setup-dotnet@v3 uses: https://github.com/actions/setup-dotnet@v4
with: with:
dotnet-version: 8.x dotnet-version: 10.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore Railway/Railway.csproj run: dotnet restore Railway/Railway.csproj
@@ -35,4 +39,4 @@ jobs:
run: dotnet nuget push --source gitea_registry --api-key ${{ secrets.LOCAL_NUGET_PACKAGE_TOKEN }} nupkgs/*.nupkg run: dotnet nuget push --source gitea_registry --api-key ${{ secrets.LOCAL_NUGET_PACKAGE_TOKEN }} nupkgs/*.nupkg
- name: Publish the package to NuGet.org - name: Publish the package to NuGet.org
run: dotnet nuget push --api-key ${{ secrets.NUGET_PACKAGE_TOKEN }} nupkgs/*.nupkg run: dotnet nuget push --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_PACKAGE_TOKEN }} nupkgs/*.nupkg

View File

@@ -14,28 +14,47 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: .NET tests
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v5
- name: Setup .NET - name: Setup .NET
uses: https://github.com/actions/setup-dotnet@v3 uses: https://github.com/actions/setup-dotnet@v4
with: with:
dotnet-version: 8.x dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build .NET 10.0
run: dotnet build --no-restore run: dotnet build --no-restore --framework net10.0 --configuration Release ./Raliway.Tests/Raliway.Tests.csproj
- name: Test - name: Build .NET 9.0
run: dotnet test --no-build --verbosity normal --logger trx --results-directory "TestResults-8.x" run: dotnet build --no-restore --framework net9.0 --configuration Release ./Raliway.Tests/Raliway.Tests.csproj
- name: Build .NET 8.0
run: dotnet build --no-restore --framework net8.0 --configuration Release ./Raliway.Tests/Raliway.Tests.csproj
- name: Test .NET 10.0
run: dotnet run --no-build --framework net10.0 --configuration Release --project ./Raliway.Tests/Raliway.Tests.csproj -- -trx TestResults/results-net10.trx
- name: Test .NET 9.0
run: dotnet run --no-build --framework net9.0 --configuration Release --project ./Raliway.Tests/Raliway.Tests.csproj -- -trx TestResults/results-net9.trx
- name: Test .NET 8.0
run: dotnet run --no-build --framework net8.0 --configuration Release --project ./Raliway.Tests/Raliway.Tests.csproj -- -trx TestResults/results-net8.trx
- name: Upload dotnet test results - name: Upload dotnet test results
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: dotnet-results-8.x name: test-results
path: TestResults-8.x path: TestResults
if: ${{ always() }} if: ${{ always() }}
retention-days: 30 retention-days: 30

View File

@@ -1,3 +1,4 @@
{ {
"dotnet.defaultSolution": "Just.Railway.sln" "dotnet.defaultSolution": "Just.Railway.sln",
"dotnetAcquisitionExtension.enableTelemetry": false
} }

View File

@@ -1,4 +1,4 @@
Copyright (c) 2023 JustFixMe Copyright (c) 2023-2025 JustFixMe
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -69,9 +69,9 @@ Result<T> Bar()
```csharp ```csharp
Result<int> result = GetResult(); Result<int> result = GetResult();
var value = result string value = result
.Append("new") .Append("new") // -> Result<(int, string)>
.Map((i, s) => $"{s} result {i}") .Map((i, s) => $"{s} result {i}") // -> Result<string>
.Match( .Match(
onSuccess: x => x, onSuccess: x => x,
onFailure: err => err.ToString() onFailure: err => err.ToString()
@@ -81,6 +81,17 @@ var value = result
Result<int> GetResult() => Result.Success(1); Result<int> GetResult() => Result.Success(1);
``` ```
#### Recover from failure
```csharp
Result<string> failed = new NotImplementedException();
Result<string> result = failed.TryRecover(err => err.Type == "System.NotImplementedException"
? "recovered"
: err);
// result with value: "recovered"
```
### Try ### Try
```csharp ```csharp
@@ -99,9 +110,9 @@ int SomeFunction() => 1;
### Ensure ### Ensure
```csharp ```csharp
var value = GetValue(); int? value = GetValue();
Result<int> result = Ensure.That(value) Result<int> result = Ensure.That(value) // -> Ensure<int?>
.NotNull() .NotNull() // -> Ensure<int>
.Satisfies(i => i < 100) .Satisfies(i => i < 100)
.Result(); .Result();

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
@@ -41,22 +38,82 @@ public sealed class EnsureExtensionsExecutor : IGeneratorExecutor
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine($"#region Satisfies"); sb.AppendLine("#region Satisfies");
errorGenerationDefinitions.ForEach(def => GenerateSatisfiesExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); errorGenerationDefinitions.ForEach(def => GenerateSatisfiesExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
sb.AppendLine($"#region NotNull"); sb.AppendLine("#region Null");
errorGenerationDefinitions.ForEach(def => GenerateNullExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
sb.AppendLine("#region NotNull");
errorGenerationDefinitions.ForEach(def => GenerateNotNullExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); errorGenerationDefinitions.ForEach(def => GenerateNotNullExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
sb.AppendLine($"#region NotEmpty"); sb.AppendLine("#region NotEmpty");
errorGenerationDefinitions.ForEach(def => GenerateNotEmptyExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); errorGenerationDefinitions.ForEach(def => GenerateNotEmptyExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion"); sb.AppendLine("#endregion");
sb.AppendLine("#region NotWhitespace");
errorGenerationDefinitions.ForEach(def => GenerateNotWhitespaceExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
sb.AppendLine("#region True");
errorGenerationDefinitions.ForEach(def => GenerateTrueExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
sb.AppendLine("#region False");
errorGenerationDefinitions.ForEach(def => GenerateFalseExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
sb.AppendLine("#region EqualTo");
errorGenerationDefinitions.ForEach(def => GenerateEqualToExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
sb.AppendLine("#region NotEqualTo");
errorGenerationDefinitions.ForEach(def => GenerateNotEqualToExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
sb.AppendLine("#region LessThan");
errorGenerationDefinitions.ForEach(def => GenerateLessThanExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
sb.AppendLine("#region GreaterThan");
errorGenerationDefinitions.ForEach(def => GenerateGreaterThanExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
sb.AppendLine("#region LessThanOrEqualTo");
errorGenerationDefinitions.ForEach(def => GenerateLessThanOrEqualToExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
sb.AppendLine("#region GreaterThanOrEqualTo");
errorGenerationDefinitions.ForEach(def => GenerateGreaterThanOrEqualToExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr));
sb.AppendLine("#endregion");
return sb.ToString(); return sb.ToString();
} }
private void GenerateNotWhitespaceExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is empty or consists exclusively of white-space characters.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<string> NotWhitespace(this in Ensure<string> ensure, {{errorParameterDecl}})
{
return ensure.State switch
{
ResultState.Success => string.IsNullOrWhiteSpace(ensure.Value)
? new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression)
: new(ensure.Value!, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
private void GenerateNotEmptyExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr) private void GenerateNotEmptyExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{ {
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is empty.\")"; string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is empty.\")";
@@ -95,6 +152,44 @@ public sealed class EnsureExtensionsExecutor : IGeneratorExecutor
""")); """));
} }
private void GenerateNullExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is not null.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T?> Null<T>(this in Ensure<T?> ensure, {{errorParameterDecl}})
where T : struct
{
return ensure.State switch
{
ResultState.Success => !ensure.Value.HasValue
? new(default(T?)!, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T?> Null<T>(this in Ensure<T?> ensure, {{errorParameterDecl}})
where T : class
{
return ensure.State switch
{
ResultState.Success => ensure.Value is null
? new(default(T?)!, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
private void GenerateNotNullExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr) private void GenerateNotNullExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{ {
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is null.\")"; string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is null.\")";
@@ -206,4 +301,204 @@ public sealed class EnsureExtensionsExecutor : IGeneratorExecutor
} }
"""); """);
} }
private void GenerateTrueExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is not true.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<bool> True(this in Ensure<bool> ensure, {{errorParameterDecl}})
{
return ensure.State switch
{
ResultState.Success => ensure.Value == true
? new(ensure.Value!, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<bool> True(this in Ensure<bool?> ensure, {{errorParameterDecl}})
{
return ensure.State switch
{
ResultState.Success => ensure.Value == true
? new(ensure.Value.Value, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
private void GenerateFalseExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is not false.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<bool> False(this in Ensure<bool> ensure, {{errorParameterDecl}})
{
return ensure.State switch
{
ResultState.Success => ensure.Value == false
? new(ensure.Value!, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<bool> False(this in Ensure<bool?> ensure, {{errorParameterDecl}})
{
return ensure.State switch
{
ResultState.Success => ensure.Value == false
? new(ensure.Value.Value, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
private void GenerateEqualToExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is not equal to requirement.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T> EqualTo<T>(this in Ensure<T> ensure, T requirement, {{errorParameterDecl}})
where T : IEquatable<T>
{
return ensure.State switch
{
ResultState.Success => ensure.Value.Equals(requirement)
? new(ensure.Value, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
private void GenerateNotEqualToExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is equal to requirement.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T> NotEqualTo<T>(this in Ensure<T> ensure, T requirement, {{errorParameterDecl}})
where T : IEquatable<T>
{
return ensure.State switch
{
ResultState.Success => !ensure.Value.Equals(requirement)
? new(ensure.Value, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
private void GenerateLessThanExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is not less than requirement.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T> LessThan<T>(this in Ensure<T> ensure, T requirement, {{errorParameterDecl}})
where T : IComparable<T>
{
return ensure.State switch
{
ResultState.Success => ensure.Value.CompareTo(requirement) < 0
? new(ensure.Value, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
private void GenerateGreaterThanExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is not greater than requirement.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T> GreaterThan<T>(this in Ensure<T> ensure, T requirement, {{errorParameterDecl}})
where T : IComparable<T>
{
return ensure.State switch
{
ResultState.Success => ensure.Value.CompareTo(requirement) > 0
? new(ensure.Value, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
private void GenerateLessThanOrEqualToExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is greater than requirement.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T> LessThanOrEqualTo<T>(this in Ensure<T> ensure, T requirement, {{errorParameterDecl}})
where T : IComparable<T>
{
return ensure.State switch
{
ResultState.Success => ensure.Value.CompareTo(requirement) <= 0
? new(ensure.Value, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
private void GenerateGreaterThanOrEqualToExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr)
{
string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is less than requirement.\")";
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(EnsureExtensionsExecutor)}}", "1.0.0.0")]
public static Ensure<T> GreaterThanOrEqualTo<T>(this in Ensure<T> ensure, T requirement, {{errorParameterDecl}})
where T : IComparable<T>
{
return ensure.State switch
{
ResultState.Success => ensure.Value.CompareTo(requirement) >= 0
? new(ensure.Value, ensure.ValueExpression)
: new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
""");
}
} }

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Just.Railway.SourceGen; namespace Just.Railway.SourceGen;
@@ -17,6 +13,8 @@ public class ExtensionsMethodGenerator : IIncrementalGenerator
new ResultMapExecutor(), new ResultMapExecutor(),
new ResultBindExecutor(), new ResultBindExecutor(),
new ResultTapExecutor(), new ResultTapExecutor(),
new ResultExtendExecutor(),
new ResultTryRecoverExecutor(),
new ResultAppendExecutor(), new ResultAppendExecutor(),
new TryExtensionsExecutor(), new TryExtensionsExecutor(),
new EnsureExtensionsExecutor(), new EnsureExtensionsExecutor(),

View File

@@ -1,4 +1,3 @@
using System;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Just.Railway.SourceGen; namespace Just.Railway.SourceGen;

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Just.Railway.SourceGen; namespace Just.Railway.SourceGen;
@@ -462,12 +460,4 @@ internal sealed class ResultAppendExecutor : ResultExtensionsExecutor
} }
"""); """);
} }
internal static string JoinArguments(string arg1, string arg2) => (arg1, arg2) switch
{
("", "") => "",
(string arg, "") => arg,
("", string arg) => arg,
_ => $"{arg1}, {arg2}"
};
} }

View File

@@ -0,0 +1,160 @@
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
namespace Just.Railway.SourceGen;
internal sealed class ResultExtendExecutor : ResultExtensionsExecutor
{
protected override string ExtensionType => "Extend";
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount)
{
if (argCount == 0 || argCount == Constants.MaxResultTupleSize)
{
return;
}
var templateArgNames = Enumerable.Range(1, argCount)
.Select(i => $"T{i}")
.ToImmutableArray();
var expandedTemplateArgNames = templateArgNames.Add("R");
string resultTypeDef = GenerateResultTypeDef(templateArgNames);
string resultValueExpansion = GenerateResultValueExpansion(templateArgNames);
string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames);
string methodTemplateDecl = GenerateTemplateDecl(expandedTemplateArgNames);
string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result<R>"));
sb.AppendLine($"#region {resultTypeDef}");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")]
public static {{resultExpandedTypeDef}} Extend{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func{{bindTemplateDecl}} extensionFunc)
{
if (result.State == ResultState.Bottom)
{
throw new ResultNotInitializedException(nameof(result));
}
else if (result.IsFailure)
{
return result.Error!;
}
var extension = extensionFunc({{resultValueExpansion}});
if (extension.State == ResultState.Bottom)
{
throw new ResultNotInitializedException(nameof(extensionFunc));
}
else if (extension.IsFailure)
{
return extension.Error!;
}
return Result.Success({{JoinArguments(resultValueExpansion, "extension.Value")}});
}
""");
GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, resultValueExpansion);
GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, resultValueExpansion);
sb.AppendLine("#endregion");
}
private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray<string> templateArgNames, string resultTypeDef, string resultValueExpansion)
{
var expandedTemplateArgNames = templateArgNames.Add("R");
string resultExpandedTypeDef = GenerateResultTypeDef(expandedTemplateArgNames);
string methodTemplateDecl = GenerateTemplateDecl(expandedTemplateArgNames);
string bindTemplateDecl = GenerateTemplateDecl(templateArgNames.Add("Result<R>"));
string asyncActionTemplateDecl = GenerateTemplateDecl(templateArgNames.Add($"{taskType}<Result<R>>"));
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{bindTemplateDecl}} extensionFunc)
{
var result = await resultTask.ConfigureAwait(false);
if (result.State == ResultState.Bottom)
{
throw new ResultNotInitializedException(nameof(resultTask));
}
else if (result.IsFailure)
{
return result.Error!;
}
var extension = extensionFunc({{resultValueExpansion}});
if (extension.State == ResultState.Bottom)
{
throw new ResultNotInitializedException(nameof(extensionFunc));
}
else if (extension.IsFailure)
{
return extension.Error!;
}
return Result.Success({{JoinArguments(resultValueExpansion, "extension.Value")}});
}
""");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func{{asyncActionTemplateDecl}} extensionFunc)
{
if (result.State == ResultState.Bottom)
{
throw new ResultNotInitializedException(nameof(result));
}
else if (result.IsFailure)
{
return result.Error!;
}
var extension = await extensionFunc({{resultValueExpansion}}).ConfigureAwait(false);
if (extension.State == ResultState.Bottom)
{
throw new ResultNotInitializedException(nameof(extensionFunc));
}
else if (extension.IsFailure)
{
return extension.Error!;
}
return Result.Success({{JoinArguments(resultValueExpansion, "extension.Value")}});
}
""");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultExtendExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<{{resultExpandedTypeDef}}> Extend{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func{{asyncActionTemplateDecl}} extensionFunc)
{
var result = await resultTask.ConfigureAwait(false);
if (result.State == ResultState.Bottom)
{
throw new ResultNotInitializedException(nameof(resultTask));
}
else if (result.IsFailure)
{
return result.Error!;
}
var extension = await extensionFunc({{resultValueExpansion}}).ConfigureAwait(false);
if (extension.State == ResultState.Bottom)
{
throw new ResultNotInitializedException(nameof(extensionFunc));
}
else if (extension.IsFailure)
{
return extension.Error!;
}
return Result.Success({{JoinArguments(resultValueExpansion, "extension.Value")}});
}
""");
}
}

View File

@@ -50,6 +50,13 @@ internal abstract class ResultExtensionsExecutor : IGeneratorExecutor
1 => $"Result<{string.Join(", ", templateArgNames)}>", 1 => $"Result<{string.Join(", ", templateArgNames)}>",
_ => $"Result<({string.Join(", ", templateArgNames)})>", _ => $"Result<({string.Join(", ", templateArgNames)})>",
}; };
protected static string JoinArguments(string arg1, string arg2) => (arg1, arg2) switch
{
("", "") => "",
(string arg, "") => arg,
("", string arg) => arg,
_ => $"{arg1}, {arg2}"
};
protected static string GenerateResultValueExpansion(ImmutableArray<string> templateArgNames) protected static string GenerateResultValueExpansion(ImmutableArray<string> templateArgNames)
{ {

View File

@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Text; using System.Text;

View File

@@ -0,0 +1,85 @@
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Just.Railway.SourceGen;
internal sealed class ResultTryRecoverExecutor : ResultExtensionsExecutor
{
protected override string ExtensionType => "TryRecover";
protected override void GenerateMethodsForArgCount(StringBuilder sb, int argCount)
{
if (argCount > 1) return;
var templateArgNames = Enumerable.Repeat("T", argCount)
.ToImmutableArray();
string methodTemplateDecl = GenerateTemplateDecl(templateArgNames);
string resultTypeDef = GenerateResultTypeDef(templateArgNames);
sb.AppendLine($"#region {resultTypeDef}");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")]
public static {{resultTypeDef}} TryRecover{{methodTemplateDecl}}(this in {{resultTypeDef}} result, Func<Error, {{resultTypeDef}}> recover)
{
return result.State switch
{
ResultState.Success => ({{resultTypeDef}})result.Value,
ResultState.Error => recover(result.Error!),
_ => throw new ResultNotInitializedException(nameof(result))
};
}
""");
GenerateAsyncMethods("Task", sb, templateArgNames, resultTypeDef, methodTemplateDecl);
GenerateAsyncMethods("ValueTask", sb, templateArgNames, resultTypeDef, methodTemplateDecl);
sb.AppendLine("#endregion");
}
private static void GenerateAsyncMethods(string taskType, StringBuilder sb, ImmutableArray<string> 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<Error, {{resultTypeDef}}> recover)
{
var result = await resultTask.ConfigureAwait(false);
return result.State switch
{
ResultState.Success => ({{resultTypeDef}})result.Value,
ResultState.Error => recover(result.Error!),
_ => throw new ResultNotInitializedException(nameof(resultTask))
};
}
""");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodTemplateDecl}}(this {{resultTypeDef}} result, Func<Error, {{taskType}}<{{resultTypeDef}}>> recover)
{
return result.State switch
{
ResultState.Success => ({{resultTypeDef}})result.Value,
ResultState.Error => await recover(result.Error!).ConfigureAwait(false),
_ => throw new ResultNotInitializedException(nameof(result))
};
}
""");
sb.AppendLine($$"""
[PureAttribute]
[GeneratedCodeAttribute("{{nameof(ResultTryRecoverExecutor)}}", "1.0.0.0")]
public static async {{taskType}}<{{resultTypeDef}}> TryRecover{{methodTemplateDecl}}(this {{taskType}}<{{resultTypeDef}}> resultTask, Func<Error, {{taskType}}<{{resultTypeDef}}>> recover)
{
var result = await resultTask.ConfigureAwait(false);
return result.State switch
{
ResultState.Success => ({{resultTypeDef}})result.Value,
ResultState.Error => await recover(result.Error!).ConfigureAwait(false),
_ => throw new ResultNotInitializedException(nameof(resultTask))
};
}
""");
}
}

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Text; using System.Text;

View File

@@ -8,44 +8,9 @@ public static partial class Ensure
[Pure] public static Ensure<T> That<T>(T value, [CallerArgumentExpression(nameof(value))]string valueExpression = "") => new(value, valueExpression); [Pure] public static Ensure<T> That<T>(T value, [CallerArgumentExpression(nameof(value))]string valueExpression = "") => new(value, valueExpression);
[Pure] public static Result<T> Result<T>(this in Ensure<T> ensure) => ensure.State switch [Pure] public static Result<T> Result<T>(this in Ensure<T> ensure) => ensure;
{ [Pure] public static async Task<Result<T>> Result<T>(this Task<Ensure<T>> ensureTask) => await ensureTask.ConfigureAwait(false);
ResultState.Success => new(ensure.Value), [Pure] public static async ValueTask<Result<T>> Result<T>(this ValueTask<Ensure<T>> ensureTask) => await ensureTask.ConfigureAwait(false);
ResultState.Error => new(ensure.Error!),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
[Pure] public static async Task<Result<T>> Result<T>(this Task<Ensure<T>> ensureTask)
{
var ensure = await ensureTask.ConfigureAwait(false);
return ensure.State switch
{
ResultState.Success => new(ensure.Value),
ResultState.Error => new(ensure.Error!),
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
};
}
[Pure] public static async ValueTask<Result<T>> Result<T>(this ValueTask<Ensure<T>> ensureTask)
{
var ensure = await ensureTask.ConfigureAwait(false);
return ensure.State switch
{
ResultState.Success => new(ensure.Value),
ResultState.Error => new(ensure.Error!),
_ => throw new EnsureNotInitializedException(nameof(ensureTask))
};
}
[Pure] public static Ensure<string> NotWhitespace(this in Ensure<string> ensure, Error error = default!)
{
return ensure.State switch
{
ResultState.Success => string.IsNullOrWhiteSpace(ensure.Value)
? new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty or consists exclusively of white-space characters."), ensure.ValueExpression)
: new(ensure.Value, ensure.ValueExpression),
ResultState.Error => new(ensure.Error!, ensure.ValueExpression),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
}
} }
public readonly struct Ensure<T> public readonly struct Ensure<T>
@@ -60,6 +25,7 @@ public readonly struct Ensure<T>
Value = value; Value = value;
ValueExpression = valueExpression; ValueExpression = valueExpression;
State = ResultState.Success; State = ResultState.Success;
Error = default;
} }
internal Ensure(Error error, string valueExpression) internal Ensure(Error error, string valueExpression)
@@ -69,10 +35,31 @@ public readonly struct Ensure<T>
Value = default!; Value = default!;
State = ResultState.Error; State = ResultState.Error;
} }
[Pure]
public static implicit operator Result<T>(in Ensure<T> ensure) => ensure.State switch
{
ResultState.Success => new(ensure.Value),
ResultState.Error => new(ensure.Error!),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
[Pure]
public static explicit operator Result(in Ensure<T> ensure) => ensure.State switch
{
ResultState.Success => new(null),
ResultState.Error => new(ensure.Error!),
_ => throw new EnsureNotInitializedException(nameof(ensure))
};
} }
[Serializable] [Serializable]
public class EnsureNotInitializedException(string variableName = "this") : InvalidOperationException("Ensure was not properly initialized.") public class EnsureNotInitializedException : InvalidOperationException
{ {
public string VariableName { get; } = variableName; public EnsureNotInitializedException(string variableName = "this")
: base("Ensure was not properly initialized.")
{
VariableName = variableName;
}
public string VariableName { get; }
} }

View File

@@ -52,7 +52,7 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error Many(Error error1, Error error2) => (error1, error2) switch public static Error Many(Error error1, Error error2) => (error1, error2) switch
{ {
(null, null) => new ManyErrors([]), (null, null) => new ManyErrors(ImmutableArray<Error>.Empty),
(Error err, null) => err, (Error err, null) => err,
(Error err, { IsEmpty: true }) => err, (Error err, { IsEmpty: true }) => err,
(null, Error err) => err, (null, Error err) => err,
@@ -137,12 +137,14 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
return string.Compare(Message, other.Message); return string.Compare(Message, other.Message);
} }
[Pure] public override string ToString() => Message; [Pure] public sealed override string ToString() => Message;
[Pure] public void Deconstruct(out string type, out string message) [Pure] public void Deconstruct(out string type, out string message)
{ {
type = Type; type = Type;
message = Message; message = Message;
} }
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal virtual Error AccessUnsafe(int position) => this;
} }
[JsonConverter(typeof(ExpectedErrorJsonConverter))] [JsonConverter(typeof(ExpectedErrorJsonConverter))]
@@ -178,14 +180,17 @@ public sealed class ExceptionalError : Error
{ {
internal readonly Exception? Exception; internal readonly Exception? Exception;
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static string ToErrorType(Type exceptionType) => exceptionType.FullName ?? exceptionType.Name;
internal ExceptionalError(Exception exception) internal ExceptionalError(Exception exception)
: this(exception.GetType().FullName ?? exception.GetType().Name, exception.Message) : this(ToErrorType(exception.GetType()), exception.Message)
{ {
Exception = exception; Exception = exception;
ExtensionData = ExtractExtensionData(exception); ExtensionData = ExtractExtensionData(exception);
} }
internal ExceptionalError(string message, Exception exception) internal ExceptionalError(string message, Exception exception)
: this(exception.GetType().FullName ?? exception.GetType().Name, message) : this(ToErrorType(exception.GetType()), message)
{ {
Exception = exception; Exception = exception;
ExtensionData = ExtractExtensionData(exception); ExtensionData = ExtractExtensionData(exception);
@@ -216,78 +221,93 @@ public sealed class ExceptionalError : Error
private static ImmutableDictionary<string, string> ExtractExtensionData(Exception exception) private static ImmutableDictionary<string, string> ExtractExtensionData(Exception exception)
{ {
if (!(exception.Data?.Count > 0)) if (!(exception.Data?.Count > 0))
return ImmutableDictionary<string, string>.Empty; return ImmutableDictionary<string, string>.Empty;
List<KeyValuePair<string, string>>? values = null; var values = GetGenericExtData(exception);
foreach (var key in exception.Data.Keys) return values is not null ? values.ToImmutable() : ImmutableDictionary<string, string>.Empty;
}
private static ImmutableDictionary<string, string>.Builder? GetGenericExtData(Exception ex)
{
ImmutableDictionary<string, string>.Builder? values = null;
foreach (var key in ex.Data.Keys)
{ {
if (key is null) continue; if (key is null) continue;
var value = exception.Data[key]; var value = ex.Data[key];
if (value is null) continue; if (value is null) continue;
var keyString = key.ToString(); var keyString = key.ToString();
var valueString = value.ToString(); var valueString = value.ToString();
if (string.IsNullOrEmpty(keyString) || string.IsNullOrEmpty(valueString)) continue; if (string.IsNullOrEmpty(keyString) || string.IsNullOrEmpty(valueString)) continue;
values ??= []; values ??= ImmutableDictionary.CreateBuilder<string, string>();
values.Add(new(keyString, valueString)); values.Add(keyString, valueString);
} }
return values?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty;
return values;
} }
} }
[JsonConverter(typeof(ManyErrorsJsonConverter))] [JsonConverter(typeof(ManyErrorsJsonConverter))]
public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error> public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
{ {
private readonly List<Error> _errors; private readonly ImmutableArray<Error> _errors;
[Pure] public IEnumerable<Error> Errors { get => _errors; } [Pure] public IEnumerable<Error> Errors { get => _errors; }
internal ManyErrors(List<Error> errors) => _errors = errors; internal ManyErrors(ImmutableArray<Error> errors) => _errors = errors;
internal ManyErrors(Error head, Error tail) internal ManyErrors(Error head, Error tail)
{ {
_errors = new List<Error>(head.Count + tail.Count); var headCount = head.Count;
var tailCount = tail.Count;
var errors = ImmutableArray.CreateBuilder<Error>(headCount + tailCount);
if (head.Count == 1) if (headCount > 0)
_errors.Add(head); AppendSanitized(errors, head);
else if (head.Count > 1)
_errors.AddRange(head.ToEnumerable());
if (tail.Count == 1) if (tailCount > 0)
_errors.Add(tail); AppendSanitized(errors, tail);
else if (tail.Count > 1)
_errors.AddRange(tail.ToEnumerable()); _errors = errors.MoveToImmutable();
} }
public ManyErrors(IEnumerable<Error> errors) public ManyErrors(IEnumerable<Error> errors)
{ {
_errors = errors.SelectMany(x => x.ToEnumerable()) var unpackedErrors = ImmutableArray.CreateBuilder<Error>();
.Where(x => !x.IsEmpty)
.ToList(); foreach (var err in errors)
{
if (err.IsEmpty) continue;
AppendSanitized(unpackedErrors, err);
}
_errors = unpackedErrors.ToImmutable();
} }
[Pure] public override string Type => "many_errors"; [Pure] public override string Type => "many_errors";
[Pure] public override string Message => ToFullArrayString();
[Pure] public override string ToString() => ToFullArrayString();
[Pure] private string ToFullArrayString() private string? _lazyMessage = null;
[Pure] public override string Message => _lazyMessage ??= ToFullArrayString(_errors);
[Pure] private static string ToFullArrayString(in ImmutableArray<Error> errors)
{ {
var separator = Environment.NewLine; var separator = Environment.NewLine;
var lastIndex = _errors.Count - 1;
var sb = new StringBuilder(); var sb = new StringBuilder();
for (int i = 0; i < _errors.Count; i++) for (int i = 0; i < errors.Length; i++)
{ {
sb.Append(_errors[i]); sb.Append(errors[i]);
if (i < lastIndex) sb.Append(separator);
sb.Append(separator);
} }
sb.Remove(sb.Length - separator.Length, separator.Length);
return sb.ToString(); return sb.ToString();
} }
[Pure] public override int Count => _errors.Count; [Pure] public override int Count => _errors.Length;
[Pure] public override bool IsEmpty => _errors.Count == 0; [Pure] public override bool IsEmpty => _errors.IsEmpty;
[Pure] public override bool IsExpected => _errors.All(static x => x.IsExpected); [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 IsExeptional => _errors.Any(static x => x.IsExeptional);
@@ -300,22 +320,19 @@ public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
{ {
if (other is null) if (other is null)
return -1; return -1;
if (other.Count != _errors.Count) if (other.Count != _errors.Length)
return _errors.Count.CompareTo(other.Count); return _errors.Length.CompareTo(other.Count);
var compareResult = 0; for (int i = 0; i < _errors.Length; i++)
int i = 0;
foreach (var otherErr in other.ToEnumerable())
{ {
var thisErr = _errors[i++]; var compareResult = _errors[i].CompareTo(other.AccessUnsafe(i));
compareResult = thisErr.CompareTo(otherErr);
if (compareResult != 0) if (compareResult != 0)
{ {
return compareResult; return compareResult;
} }
} }
return compareResult; return 0;
} }
[Pure] public override bool IsSimilarTo([NotNullWhen(true)] Error? other) [Pure] public override bool IsSimilarTo([NotNullWhen(true)] Error? other)
{ {
@@ -323,15 +340,13 @@ public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
{ {
return false; return false;
} }
if (_errors.Count != other.Count) if (_errors.Length != other.Count)
{ {
return false; return false;
} }
int i = 0; for (int i = 0; i < _errors.Length; i++)
foreach (var otherErr in other.ToEnumerable())
{ {
var thisErr = _errors[i++]; if (!_errors[i].IsSimilarTo(other.AccessUnsafe(i)))
if (!thisErr.IsSimilarTo(otherErr))
{ {
return false; return false;
} }
@@ -344,40 +359,57 @@ public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
{ {
return false; return false;
} }
if (_errors.Count != other.Count) if (_errors.Length != other.Count)
{ {
return false; return false;
} }
int i = 0; for (int i = 0; i < _errors.Length; i++)
foreach (var otherErr in other.ToEnumerable())
{ {
var thisErr = _errors[i++]; if (!_errors[i].Equals(other.AccessUnsafe(i)))
if (!thisErr.Equals(otherErr))
{ {
return false; return false;
} }
} }
return true; return true;
} }
[Pure] public override int GetHashCode()
private int? _lazyHashCode = null;
[Pure] public override int GetHashCode() => _lazyHashCode ??= CalcHashCode(_errors);
private static int CalcHashCode(in ImmutableArray<Error> errors)
{ {
if (_errors.Count == 0) if (errors.IsEmpty)
return 0; return 0;
var hash = new HashCode(); var hash = new HashCode();
foreach (var err in _errors) foreach (var err in errors)
{ {
hash.Add(err); hash.Add(err);
} }
return hash.ToHashCode(); return hash.ToHashCode();
} }
[Pure] public IEnumerator<Error> GetEnumerator() => _errors.GetEnumerator();
[Pure] IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); [Pure] public ImmutableArray<Error>.Enumerator GetEnumerator() => _errors.GetEnumerator();
[Pure] IEnumerator<Error> IEnumerable<Error>.GetEnumerator() => Errors.GetEnumerator();
[Pure] IEnumerator IEnumerable.GetEnumerator() => Errors.GetEnumerator();
internal static void AppendSanitized(ImmutableArray<Error>.Builder errors, Error error)
{
if (error is ManyErrors many)
errors.AddRange(many._errors);
else
errors.Add(error);
}
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] internal override Error AccessUnsafe(int position) => _errors[position];
} }
[Serializable] [Serializable]
public sealed class ErrorException(string type, string message) : Exception(message) public sealed class ErrorException : Exception
{ {
public string Type { get; } = type ?? nameof(ErrorException); public ErrorException(string type, string message) : base(message)
{
Type = type ?? nameof(ErrorException);
}
public string Type { get; }
} }

View File

@@ -35,7 +35,7 @@ public sealed class ErrorJsonConverter : JsonConverter<Error>
internal static ManyErrors ReadMany(ref Utf8JsonReader reader) internal static ManyErrors ReadMany(ref Utf8JsonReader reader)
{ {
List<Error> errors = []; var errors = ImmutableArray.CreateBuilder<Error>();
while (reader.Read()) while (reader.Read())
{ {
if (reader.TokenType == JsonTokenType.StartObject) if (reader.TokenType == JsonTokenType.StartObject)
@@ -43,13 +43,13 @@ public sealed class ErrorJsonConverter : JsonConverter<Error>
errors.Add(ToExpectedError(ReadOne(ref reader))); errors.Add(ToExpectedError(ReadOne(ref reader)));
} }
} }
return new ManyErrors(errors); return new ManyErrors(errors.ToImmutable());
} }
internal static ExpectedError ToExpectedError(in (string Type, string Message, ImmutableDictionary<string, string> ExtensionData) errorInfo) internal static ExpectedError ToExpectedError(in (string Type, string Message, ImmutableDictionary<string, string> ExtensionData) errorInfo)
=> new(errorInfo.Type, errorInfo.Message) { ExtensionData = errorInfo.ExtensionData }; => new(errorInfo.Type, errorInfo.Message) { ExtensionData = errorInfo.ExtensionData };
internal static (string Type, string Message, ImmutableDictionary<string, string> ExtensionData) ReadOne(ref Utf8JsonReader reader) internal static (string Type, string Message, ImmutableDictionary<string, string> ExtensionData) ReadOne(ref Utf8JsonReader reader)
{ {
List<KeyValuePair<string, string>>? extensionData = null; ImmutableDictionary<string, string>.Builder? extensionData = null;
string type = "error"; string type = "error";
string message = ""; string message = "";
while (reader.Read()) while (reader.Read())
@@ -83,8 +83,8 @@ public sealed class ErrorJsonConverter : JsonConverter<Error>
} }
else if (!string.IsNullOrEmpty(propname)) else if (!string.IsNullOrEmpty(propname))
{ {
extensionData ??= []; extensionData ??= ImmutableDictionary.CreateBuilder<string, string>();
extensionData.Add(new(propname, propvalue)); extensionData[propname] = propvalue;
} }
break; break;
@@ -95,7 +95,7 @@ public sealed class ErrorJsonConverter : JsonConverter<Error>
} }
} }
endLoop: endLoop:
return (type, message, extensionData?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty); return (type, message, extensionData?.ToImmutable() ?? ImmutableDictionary<string, string>.Empty);
} }
internal static void WriteOne(Utf8JsonWriter writer, Error value) internal static void WriteOne(Utf8JsonWriter writer, Error value)
{ {

View File

@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>Just.Railway</AssemblyName> <AssemblyName>Just.Railway</AssemblyName>
@@ -10,8 +11,9 @@
<Description>Base for railway-oriented programming in .NET. Package includes Result object, Error class and most of the common extensions.</Description> <Description>Base for railway-oriented programming in .NET. Package includes Result object, Error class and most of the common extensions.</Description>
<PackageTags>railway-oriented;functional;result-pattern;result-object;error-handling</PackageTags> <PackageTags>railway-oriented;functional;result-pattern;result-object;error-handling</PackageTags>
<Authors>JustFixMe</Authors> <Authors>JustFixMe</Authors>
<Copyright>Copyright (c) 2023 JustFixMe</Copyright> <Copyright>Copyright (c) 2023-2025 JustFixMe</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/JustFixMe/Just.Railway/</RepositoryUrl> <RepositoryUrl>https://github.com/JustFixMe/Just.Railway/</RepositoryUrl>
<EmitCompilerGeneratedFiles Condition="'$(Configuration)'=='Debug'">true</EmitCompilerGeneratedFiles> <EmitCompilerGeneratedFiles Condition="'$(Configuration)'=='Debug'">true</EmitCompilerGeneratedFiles>
@@ -26,6 +28,11 @@
<InternalsVisibleTo Include="$(AssemblyName).Tests" /> <InternalsVisibleTo Include="$(AssemblyName).Tests" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath=""/>
<None Include="..\LICENSE" Pack="true" Visible="false" PackagePath=""/>
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Railway.SourceGenerator\Railway.SourceGenerator.csproj" <ProjectReference Include="..\Railway.SourceGenerator\Railway.SourceGenerator.csproj"
OutputItemType="Analyzer" OutputItemType="Analyzer"

View File

@@ -1,5 +1,4 @@
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
namespace Just.Railway; namespace Just.Railway;
@@ -10,58 +9,58 @@ internal static class ReflectionHelper
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Compare<T>(T? left, T? right) => TypeReflectionCache<T>.CompareFunc(left, right); public static int Compare<T>(T? left, T? right) => TypeReflectionCache<T>.CompareFunc(left, right);
}
file static class TypeReflectionCache<T> private static class TypeReflectionCache<T>
{
public static readonly Func<T?, T?, bool> IsEqualFunc;
public static readonly Func<T?, T?, int> CompareFunc;
static TypeReflectionCache()
{ {
var type = typeof(T); public static readonly Func<T?, T?, bool> IsEqualFunc;
var isNullableStruct = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); public static readonly Func<T?, T?, int> CompareFunc;
var underlyingType = isNullableStruct ? type.GenericTypeArguments.First() : type;
var thisType = typeof(TypeReflectionCache<T>);
var equatableType = typeof(IEquatable<>).MakeGenericType(underlyingType); static TypeReflectionCache()
if (equatableType.IsAssignableFrom(underlyingType))
{ {
var isEqualFunc = thisType.GetMethod(isNullableStruct ? nameof(IsEqualNullable) : nameof(IsEqual), BindingFlags.Static | BindingFlags.Public) var type = typeof(T);
!.MakeGenericMethod(underlyingType); var isNullableStruct = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
var underlyingType = isNullableStruct ? type.GenericTypeArguments.First() : type;
var thisType = typeof(TypeReflectionCache<T>);
IsEqualFunc = (Func<T?, T?, bool>)Delegate.CreateDelegate(typeof(Func<T?, T?, bool>), isEqualFunc); var equatableType = typeof(IEquatable<>).MakeGenericType(underlyingType);
} if (equatableType.IsAssignableFrom(underlyingType))
else {
{ var isEqualFunc = thisType.GetMethod(isNullableStruct ? nameof(IsEqualNullable) : nameof(IsEqual), BindingFlags.Static | BindingFlags.Public)
IsEqualFunc = static (left, right) => left is null ? right is null : left.Equals(right); !.MakeGenericMethod(underlyingType);
IsEqualFunc = (Func<T?, T?, bool>)Delegate.CreateDelegate(typeof(Func<T?, T?, bool>), isEqualFunc);
}
else
{
IsEqualFunc = static (left, right) => left is null ? right is null : left.Equals(right);
}
var comparableType = typeof(IComparable<>).MakeGenericType(underlyingType);
if (comparableType.IsAssignableFrom(underlyingType))
{
var compareFunc = thisType.GetMethod(isNullableStruct ? nameof(CompareNullable) : nameof(Compare), BindingFlags.Static | BindingFlags.Public)
!.MakeGenericMethod(underlyingType);
CompareFunc = (Func<T?, T?, int>)Delegate.CreateDelegate(typeof(Func<T?, T?, int>), compareFunc);
}
else
{
CompareFunc = static (left, right) => left is null
? right is null ? 0 : -1
: right is null ? 1 : left.GetHashCode().CompareTo(right.GetHashCode());
}
} }
var comparableType = typeof(IComparable<>).MakeGenericType(underlyingType); #pragma warning disable CS8604 // Possible null reference argument.
if (comparableType.IsAssignableFrom(underlyingType)) [Pure] public static bool IsEqual<R>(R? left, R? right) where R : notnull, IEquatable<R>, T => left is null ? right is null : left.Equals(right);
{ [Pure] public static bool IsEqualNullable<R>(R? left, R? right) where R : struct, IEquatable<R> => left is null ? right is null : right is not null && left.Value.Equals(right.Value);
var compareFunc = thisType.GetMethod(isNullableStruct ? nameof(CompareNullable) : nameof(Compare), BindingFlags.Static | BindingFlags.Public)
!.MakeGenericMethod(underlyingType);
CompareFunc = (Func<T?, T?, int>)Delegate.CreateDelegate(typeof(Func<T?, T?, int>), compareFunc); [Pure] public static int Compare<R>(R? left, R? right) where R : notnull, IComparable<R>, T => left is null
} ? right is null ? 0 : -1
else : right is null ? 1 : left.CompareTo(right);
{ [Pure] public static int CompareNullable<R>(R? left, R? right) where R : struct, IComparable<R> => left is null
CompareFunc = static (left, right) => left is null ? right is null ? 0 : -1
? right is null ? 0 : -1 : right is null ? 1 : left.Value.CompareTo(right.Value);
: right is null ? 1 : left.GetHashCode().CompareTo(right.GetHashCode()); #pragma warning restore CS8604 // Possible null reference argument.
}
} }
#pragma warning disable CS8604 // Possible null reference argument.
[Pure] public static bool IsEqual<R>(R? left, R? right) where R : notnull, IEquatable<R>, T => left is null ? right is null : left.Equals(right);
[Pure] public static bool IsEqualNullable<R>(R? left, R? right) where R : struct, IEquatable<R> => left is null ? right is null : right is not null && left.Value.Equals(right.Value);
[Pure] public static int Compare<R>(R? left, R? right) where R : notnull, IComparable<R>, T => left is null
? right is null ? 0 : -1
: right is null ? 1 : left.CompareTo(right);
[Pure] public static int CompareNullable<R>(R? left, R? right) where R : struct, IComparable<R> => left is null
? right is null ? 0 : -1
: right is null ? 1 : left.Value.CompareTo(right.Value);
#pragma warning restore CS8604 // Possible null reference argument.
} }

View File

@@ -1,4 +1,3 @@
namespace Just.Railway; namespace Just.Railway;
internal enum ResultState : byte internal enum ResultState : byte
@@ -8,6 +7,13 @@ internal enum ResultState : byte
public readonly partial struct Result : IEquatable<Result> public readonly partial struct Result : IEquatable<Result>
{ {
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Simplified source generation")]
internal SuccessUnit Value
{
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new();
}
internal readonly Error? Error; internal readonly Error? Error;
internal readonly ResultState State; internal readonly ResultState State;
@@ -36,12 +42,18 @@ public readonly partial struct Result : IEquatable<Result>
public static Result<(T1, T2, T3, T4, T5)> Success<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) => new((value1, value2, value3, value4, value5)); public static Result<(T1, T2, T3, T4, T5)> Success<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) => new((value1, value2, value3, value4, value5));
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Result Failure(string error) => Error.New(error ?? throw new ArgumentNullException(nameof(error)));
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Result Failure(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); public static Result Failure(Error error) => new(error ?? throw new ArgumentNullException(nameof(error)));
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Result Failure(Exception exception) => new(Error.New(exception) ?? throw new ArgumentNullException(nameof(exception))); public static Result Failure(Exception exception) => new(Error.New(exception) ?? throw new ArgumentNullException(nameof(exception)));
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Result<T> Failure<T>(string error) => Error.New(error ?? throw new ArgumentNullException(nameof(error)));
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Result<T> Failure<T>(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); public static Result<T> Failure<T>(Error error) => new(error ?? throw new ArgumentNullException(nameof(error)));
@@ -51,23 +63,28 @@ public readonly partial struct Result : IEquatable<Result>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Result(Error error) => new(error ?? throw new ArgumentNullException(nameof(error))); public static implicit operator Result(Error error) => new(error ?? throw new ArgumentNullException(nameof(error)));
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Result(Exception exception) => new(
new ExceptionalError(exception ?? throw new ArgumentNullException(nameof(exception))));
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Result<SuccessUnit>(Result result) => result.State switch public static implicit operator Result<SuccessUnit>(Result result) => result.State switch
{ {
ResultState.Success => new(new SuccessUnit()), ResultState.Success => new(new SuccessUnit()),
ResultState.Error => new(result.Error!), ResultState.Error => new(result.Error!),
_ => throw new ResultNotInitializedException(nameof(result)) _ => throw new ResultNotInitializedException(nameof(result))
}; };
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator Result(SuccessUnit _) => new(null);
[Pure] public bool IsSuccess => Error is null; [Pure] public bool IsSuccess => Error is null;
[Pure] public bool IsFailure => Error is not null; [Pure] public bool IsFailure => Error is not null;
[Pure] public bool Success([MaybeNullWhen(false)]out SuccessUnit? u, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error) [Pure] public bool TryGetValue([MaybeNullWhen(false)]out SuccessUnit? u, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error)
{ {
switch (State) switch (State)
{ {
case ResultState.Success: case ResultState.Success:
u = new SuccessUnit(); u = new SuccessUnit();
error = default; error = null;
return true; return true;
case ResultState.Error: case ResultState.Error:
@@ -136,26 +153,33 @@ public readonly struct Result<T> : IEquatable<Result<T>>
{ {
Value = value; Value = value;
State = ResultState.Success; State = ResultState.Success;
Error = default;
} }
[Pure] public static explicit operator Result(Result<T> result) => result.State switch [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator Result(Result<T> result) => result.State switch
{ {
ResultState.Success => new(null), ResultState.Success => new(null),
ResultState.Error => new(result.Error!), ResultState.Error => new(result.Error!),
_ => throw new ResultNotInitializedException(nameof(result)) _ => throw new ResultNotInitializedException(nameof(result))
}; };
[Pure] public static implicit operator Result<T>(Error error) => new(error); [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
[Pure] public static implicit operator Result<T>(T value) => new(value); public static implicit operator Result<T>(Error error) => new(error);
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Result<T>(Exception exception) => new(
new ExceptionalError(exception ?? throw new ArgumentNullException(nameof(exception))));
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Result<T>(T value) => new(value);
[Pure] public bool IsSuccess => State == ResultState.Success; [Pure] public bool IsSuccess => State == ResultState.Success;
[Pure] public bool IsFailure => State == ResultState.Error; [Pure] public bool IsFailure => State == ResultState.Error;
[Pure] public bool Success([MaybeNullWhen(false)]out T value, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error) [Pure] public bool TryGetValue([MaybeNullWhen(false)]out T value, [MaybeNullWhen(true), NotNullWhen(false)]out Error? error)
{ {
switch (State) switch (State)
{ {
case ResultState.Success: case ResultState.Success:
value = Value; value = Value;
error = default; error = null;
return true; return true;
case ResultState.Error: case ResultState.Error:
@@ -263,7 +287,12 @@ public readonly struct SuccessUnit : IEquatable<SuccessUnit>
} }
[Serializable] [Serializable]
public class ResultNotInitializedException(string variableName = "this") : InvalidOperationException("Result was not properly initialized.") public class ResultNotInitializedException : InvalidOperationException
{ {
public string VariableName { get; } = variableName; public ResultNotInitializedException(string variableName = "this")
: base("Result was not properly initialized.")
{
VariableName = variableName;
}
public string VariableName { get; }
} }

View File

@@ -1,3 +1,5 @@
using System.Collections.Immutable;
namespace Just.Railway; namespace Just.Railway;
public static partial class ResultExtensions public static partial class ResultExtensions
@@ -80,7 +82,7 @@ public static partial class ResultExtensions
public static Result Merge(this IEnumerable<Result> results) public static Result Merge(this IEnumerable<Result> results)
{ {
List<Error>? errors = null; ImmutableArray<Error>.Builder? errors = null;
bool hasErrors = false; bool hasErrors = false;
foreach (var result in results.OrderBy(x => x.State)) foreach (var result in results.OrderBy(x => x.State))
@@ -89,8 +91,8 @@ public static partial class ResultExtensions
{ {
case ResultState.Error: case ResultState.Error:
hasErrors = true; hasErrors = true;
errors ??= []; errors ??= ImmutableArray.CreateBuilder<Error>();
errors.Add(result.Error!); ManyErrors.AppendSanitized(errors, result.Error!);
break; break;
case ResultState.Success: case ResultState.Success:
@@ -102,7 +104,7 @@ public static partial class ResultExtensions
} }
afterLoop: afterLoop:
return hasErrors return hasErrors
? new(new ManyErrors(errors!)) ? new(new ManyErrors(errors!.ToImmutable()))
: new(null); : new(null);
} }
public static async Task<Result> Merge(this IEnumerable<Task<Result>> tasks) public static async Task<Result> Merge(this IEnumerable<Task<Result>> tasks)
@@ -113,8 +115,8 @@ public static partial class ResultExtensions
public static Result<IEnumerable<T>> Merge<T>(this IEnumerable<Result<T>> results) public static Result<IEnumerable<T>> Merge<T>(this IEnumerable<Result<T>> results)
{ {
List<T>? values = null; ImmutableList<T>.Builder? values = null;
List<Error>? errors = null; ImmutableArray<Error>.Builder? errors = null;
bool hasErrors = false; bool hasErrors = false;
foreach (var result in results.OrderBy(x => x.State)) foreach (var result in results.OrderBy(x => x.State))
@@ -123,13 +125,13 @@ public static partial class ResultExtensions
{ {
case ResultState.Error: case ResultState.Error:
hasErrors = true; hasErrors = true;
errors ??= []; errors ??= ImmutableArray.CreateBuilder<Error>();
errors.Add(result.Error!); ManyErrors.AppendSanitized(errors, result.Error!);
break; break;
case ResultState.Success: case ResultState.Success:
if (hasErrors) goto afterLoop; if (hasErrors) goto afterLoop;
values ??= []; values ??= ImmutableList.CreateBuilder<T>();
values.Add(result.Value); values.Add(result.Value);
break; break;
@@ -138,8 +140,8 @@ public static partial class ResultExtensions
} }
afterLoop: afterLoop:
return hasErrors return hasErrors
? new(new ManyErrors(errors!)) ? new(new ManyErrors(errors!.ToImmutable()))
: new((IEnumerable<T>?)values ?? Array.Empty<T>()); : new(values is not null ? values.ToImmutable() : ImmutableList<T>.Empty);
} }
public static async Task<Result<IEnumerable<T>>> Merge<T>(this IEnumerable<Task<Result<T>>> tasks) public static async Task<Result<IEnumerable<T>>> Merge<T>(this IEnumerable<Task<Result<T>>> tasks)
{ {

View File

@@ -7,6 +7,7 @@ public class Satisfy
{ {
var result = Ensure.That(69) var result = Ensure.That(69)
.Satisfies(i => i < 100) .Satisfies(i => i < 100)
.LessThan(100)
.Result(); .Result();
Assert.True(result.IsSuccess); Assert.True(result.IsSuccess);
@@ -18,6 +19,7 @@ public class Satisfy
var error = Error.New(Ensure.DefaultErrorType, "Value {69} does not satisfy the requirement."); var error = Error.New(Ensure.DefaultErrorType, "Value {69} does not satisfy the requirement.");
var result = Ensure.That(69) var result = Ensure.That(69)
.Satisfies(i => i > 100) .Satisfies(i => i > 100)
.GreaterThan(100)
.Result(); .Result();
Assert.True(result.IsFailure); Assert.True(result.IsFailure);
@@ -32,6 +34,7 @@ public class Satisfy
.NotEmpty() .NotEmpty()
.NotWhitespace() .NotWhitespace()
.Satisfies(s => s == "69") .Satisfies(s => s == "69")
.EqualTo("69")
.Result(); .Result();
Assert.True(result.IsSuccess); Assert.True(result.IsSuccess);
@@ -47,6 +50,7 @@ public class Satisfy
.NotEmpty() .NotEmpty()
.NotWhitespace() .NotWhitespace()
.Satisfies(s => s == "69") .Satisfies(s => s == "69")
.EqualTo("69")
.Result(); .Result();
Assert.True(result.IsFailure); Assert.True(result.IsFailure);

View File

@@ -1,23 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<AssemblyName>Just.Railway.Tests</AssemblyName> <AssemblyName>Just.Railway.Tests</AssemblyName>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="xunit.v3" Version="3.2.0" />
<PackageReference Include="xunit" Version="2.6.1" /> <PackageReference Include="coverlet.collector" Version="6.0.4">
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@@ -128,4 +128,88 @@ public class GeneralUsage
// Then // Then
Assert.Equal("satisfied", result); Assert.Equal("satisfied", result);
} }
[Fact]
public void RecoverResultFromFailureState()
{
// Given
Result<string> failed = new NotImplementedException();
// When
var result = failed.TryRecover(err =>
{
Assert.IsType<NotImplementedException>(err.ToException());
if (err.Type == "System.NotImplementedException")
return "recovered";
Assert.Fail();
return "";
});
// Then
Assert.True(result.IsSuccess);
Assert.Equal("recovered", result.Value);
}
[Fact]
public void WhenCanNotRecoverResultFromFailureState()
{
// Given
var error = Error.New("test");
Result<string> failed = new NotImplementedException();
// When
var result = failed.TryRecover(err =>
{
if (err.Type == "System.NotImplementedException")
return error;
Assert.Fail();
return "";
});
// Then
Assert.True(result.IsFailure);
Assert.Equal(error, result.Error);
}
[Fact]
public void WhenExtendingSuccessWithSuccess_ShouldReturnSuccess()
{
var success = Result.Success(1)
.Append("2");
var result = success
.Extend((i, s) => Result.Success($"{i} + {s}"));
Assert.True(result.IsSuccess);
Assert.Equal((1, "2", "1 + 2"), result.Value);
}
[Fact]
public void WhenExtendingFailureWithSuccess_ShouldNotEvaluateExtension()
{
var failure = Result.Success(1)
.Append(Result.Failure<string>("failure"));
var result = failure
.Extend((i, s) =>
{
Assert.Fail();
return Result.Success("");
});
Assert.True(result.IsFailure);
Assert.Equal(Error.New("failure"), result.Error);
}
[Fact]
public void WhenExtendingSuccessWithFailure_ShouldReturnFailure()
{
var success = Result.Success(1)
.Append("2");
var result = success
.Extend((i, s) => Result.Failure<string>("failure"));
Assert.True(result.IsFailure);
Assert.Equal(Error.New("failure"), result.Error);
}
} }