17 Commits

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
ccfa9d8295 modified README.md
Some checks failed
.NET Publish / publish (push) Failing after 7m10s
2023-12-13 19:32:35 +04:00
127c5ba4eb modified README.md 2023-12-13 19:31:10 +04:00
3a8bf9394e added deploy to nuget.org
All checks were successful
.NET Test / test (push) Successful in 4m33s
2023-12-13 19:29:04 +04:00
e21c35a08a fixed publish RepositoryUrl property
All checks were successful
.NET Publish / publish (push) Successful in 1m45s
2023-12-13 18:52:40 +04:00
5a2ae19a8e added Match with fallback extensions
Some checks failed
.NET Test / test (push) Successful in 4m19s
.NET Publish / publish (push) Failing after 3m8s
2023-12-13 18:30:38 +04:00
7aaacb0ac7 changed Error serialization
All checks were successful
.NET Test / test (push) Successful in 8m8s
2023-12-13 18:20:12 +04:00
25 changed files with 1291 additions and 291 deletions

View File

@@ -9,26 +9,34 @@ 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
- name: Setup nuget source - name: Setup nuget source
run: dotnet nuget add source --name gitea_registry https://gitea.jstdev.ru/api/packages/just/nuget/index.json run: dotnet nuget add source --name gitea_registry ${{ vars.OUTPUT_NUGET_REGISTRY }}
- name: Create the package - name: Create the package
env: env:
RELEASE_VERSION: ${{ gitea.ref_name }} RELEASE_VERSION: ${{ gitea.ref_name }}
run: dotnet pack --no-restore --configuration Release --output nupkgs Railway/Railway.csproj `echo $RELEASE_VERSION | sed -E 's|^(v([0-9]+(\.[0-9]+){2}))(-([a-z0-9]+)){1}|/p:ReleaseVersion=\2 /p:VersionSuffix=\5|; s|^(v([0-9]+(\.[0-9]+){2}))$|/p:ReleaseVersion=\2|'` run: >
dotnet pack --no-restore --configuration Release --output nupkgs Railway/Railway.csproj
`echo $RELEASE_VERSION | sed -E 's|^(v([0-9]+(\.[0-9]+){2}))(-([a-z0-9]+)){1}|/p:ReleaseVersion=\2 /p:VersionSuffix=\5|; s|^(v([0-9]+(\.[0-9]+){2}))$|/p:ReleaseVersion=\2|'`
- name: Publish the package to Gitea - name: Publish the package to Gitea
run: dotnet nuget push --source gitea_registry --api-key ${{ secrets.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
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

@@ -14,13 +14,11 @@ The desire is to make somewhat user-friendly experience while using result-objec
## Getting Started ## Getting Started
### Install from local Gitea package repository ### Install from NuGet.org
```sh ```sh
# Setup NuGet registry from the command line # install the package using NuGet
dotnet nuget add source --name gitea_jstdev https://gitea.jstdev.ru/api/packages/just/nuget/index.json dotnet add package Just.Railway
# then install the package using NuGet
dotnet add package --source gitea_jstdev Just.Railway
``` ```
## Examples ## Examples
@@ -71,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()
@@ -83,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
@@ -101,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

@@ -1,14 +1,10 @@
using System.Collections; using System.Collections;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Runtime.Serialization;
using System.Text; using System.Text;
namespace Just.Railway; namespace Just.Railway;
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$$err")] [JsonConverter(typeof(ErrorJsonConverter))]
[JsonDerivedType(typeof(ExpectedError), typeDiscriminator: 0)]
[JsonDerivedType(typeof(ExceptionalError), typeDiscriminator: 1)]
[JsonDerivedType(typeof(ManyErrors))]
public abstract class Error : IEquatable<Error>, IComparable<Error> public abstract class Error : IEquatable<Error>, IComparable<Error>
{ {
protected internal Error(){} protected internal Error(){}
@@ -33,16 +29,22 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
/// </summary> /// </summary>
/// <param name="message">Error detail</param> /// <param name="message">Error detail</param>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error New(string message) => public static Error New(string message, IEnumerable<KeyValuePair<string, string>>? extensionData = null) =>
new ExpectedError("error", message); new ExpectedError("error", message)
{
ExtensionData = extensionData?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty
};
/// <summary> /// <summary>
/// Create an <see cref="ExpectedError"/> /// Create an <see cref="ExpectedError"/>
/// </summary> /// </summary>
/// <param name="type">Error code</param> /// <param name="type">Error code</param>
/// <param name="message">Error detail</param> /// <param name="message">Error detail</param>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Error New(string type, string message) => public static Error New(string type, string message, IEnumerable<KeyValuePair<string, string>>? extensionData = null) =>
new ExpectedError(type, message); new ExpectedError(type, message)
{
ExtensionData = extensionData?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty
};
/// <summary> /// <summary>
/// Create a <see cref="ManyErrors"/> /// Create a <see cref="ManyErrors"/>
/// </summary> /// </summary>
@@ -50,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(Enumerable.Empty<Error>()), (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,
@@ -76,13 +78,14 @@ public abstract class Error : IEquatable<Error>, IComparable<Error>
[Pure] public abstract string Type { get; } [Pure] public abstract string Type { get; }
[Pure] public abstract string Message { get; } [Pure] public abstract string Message { get; }
[Pure, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ImmutableDictionary<string, string>? ExtensionData { get; init; }
[Pure] public string? this[string key] => ExtensionData?.TryGetValue(key, out var value) == true ? value : null;
[Pure, JsonIgnore] public abstract int Count { get; } [Pure] public ImmutableDictionary<string, string> ExtensionData { get; internal init; } = ImmutableDictionary<string, string>.Empty;
[Pure, JsonIgnore] public abstract bool IsEmpty { get; } [Pure] public string? this[string key] => ExtensionData.TryGetValue(key, out var value) == true ? value : null;
[Pure, JsonIgnore] public abstract bool IsExpected { get; }
[Pure, JsonIgnore] public abstract bool IsExeptional { get; } [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 Error Append(Error? next) [Pure] public Error Append(Error? next)
{ {
@@ -134,17 +137,19 @@ 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))]
public sealed class ExpectedError : Error public sealed class ExpectedError : Error
{ {
[JsonConstructor]
public ExpectedError(string type, string message) public ExpectedError(string type, string message)
{ {
Type = type; Type = type;
@@ -158,10 +163,10 @@ public sealed class ExpectedError : Error
[Pure] public override string Type { get; } [Pure] public override string Type { get; }
[Pure] public override string Message { get; } [Pure] public override string Message { get; }
[Pure, JsonIgnore] public override int Count => 1; [Pure] public override int Count => 1;
[Pure, JsonIgnore] public override bool IsEmpty => false; [Pure] public override bool IsEmpty => false;
[Pure, JsonIgnore] public override bool IsExpected => true; [Pure] public override bool IsExpected => true;
[Pure, JsonIgnore] public override bool IsExeptional => false; [Pure] public override bool IsExeptional => false;
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public override IEnumerable<Error> ToEnumerable() public override IEnumerable<Error> ToEnumerable()
@@ -170,24 +175,27 @@ public sealed class ExpectedError : Error
} }
} }
[JsonConverter(typeof(ExceptionalErrorJsonConverter))]
public sealed class ExceptionalError : Error 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().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().Name, message) : this(ToErrorType(exception.GetType()), message)
{ {
Exception = exception; Exception = exception;
ExtensionData = ExtractExtensionData(exception); ExtensionData = ExtractExtensionData(exception);
} }
[JsonConstructor]
public ExceptionalError(string type, string message) public ExceptionalError(string type, string message)
{ {
Type = type; Type = type;
@@ -197,10 +205,10 @@ public sealed class ExceptionalError : Error
[Pure] public override string Type { get; } [Pure] public override string Type { get; }
[Pure] public override string Message { get; } [Pure] public override string Message { get; }
[Pure, JsonIgnore] public override int Count => 1; [Pure] public override int Count => 1;
[Pure, JsonIgnore] public override bool IsEmpty => false; [Pure] public override bool IsEmpty => false;
[Pure, JsonIgnore] public override bool IsExpected => false; [Pure] public override bool IsExpected => false;
[Pure, JsonIgnore] public override bool IsExeptional => true; [Pure] public override bool IsExeptional => true;
[Pure] public override Exception ToException() => Exception ?? base.ToException(); [Pure] public override Exception ToException() => Exception ?? base.ToException();
@@ -210,82 +218,100 @@ public sealed class ExceptionalError : Error
yield return this; yield return this;
} }
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 null; 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();
return values;
} }
} }
[DataContract] [JsonConverter(typeof(ManyErrorsJsonConverter))]
public sealed class ManyErrors : Error, IEnumerable<Error> public sealed class ManyErrors : Error, IEnumerable<Error>, IReadOnlyList<Error>
{ {
private readonly List<Error> _errors; private readonly ImmutableArray<Error> _errors;
[Pure, DataMember] public IEnumerable<Error> Errors { get => _errors; } [Pure] public IEnumerable<Error> Errors { get => _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, JsonIgnore] public override bool IsEmpty => _errors.Count == 0; [Pure] public override bool IsEmpty => _errors.IsEmpty;
[Pure, JsonIgnore] public override bool IsExpected => _errors.All(static x => x.IsExpected); [Pure] public override bool IsExpected => _errors.All(static x => x.IsExpected);
[Pure, JsonIgnore] public override bool IsExeptional => _errors.Any(static x => x.IsExeptional); [Pure] public override bool IsExeptional => _errors.Any(static x => x.IsExeptional);
[Pure] public Error this[int index] => _errors[index];
[Pure] public override Exception ToException() => new AggregateException(_errors.Select(static x => x.ToException())); [Pure] public override Exception ToException() => new AggregateException(_errors.Select(static x => x.ToException()));
[Pure] public override IEnumerable<Error> ToEnumerable() => _errors; [Pure] public override IEnumerable<Error> ToEnumerable() => _errors;
@@ -294,22 +320,19 @@ public sealed class ManyErrors : Error, IEnumerable<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)
{ {
@@ -317,15 +340,13 @@ public sealed class ManyErrors : Error, IEnumerable<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;
} }
@@ -338,40 +359,57 @@ public sealed class ManyErrors : Error, IEnumerable<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

@@ -0,0 +1,182 @@
using System.Collections.Immutable;
namespace Just.Railway;
public sealed class ErrorJsonConverter : JsonConverter<Error>
{
public override Error? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.StartObject => ToExpectedError(ReadOne(ref reader)),
JsonTokenType.StartArray => ReadMany(ref reader),
JsonTokenType.None => null,
JsonTokenType.Null => null,
_ => throw new JsonException("Unexpected JSON token.")
};
}
public override void Write(Utf8JsonWriter writer, Error value, JsonSerializerOptions options)
{
if (value is ManyErrors manyErrors)
{
writer.WriteStartArray();
foreach (var err in manyErrors)
{
WriteOne(writer, err);
}
writer.WriteEndArray();
}
else
{
WriteOne(writer, value);
}
}
internal static ManyErrors ReadMany(ref Utf8JsonReader reader)
{
var errors = ImmutableArray.CreateBuilder<Error>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.StartObject)
{
errors.Add(ToExpectedError(ReadOne(ref reader)));
}
}
return new ManyErrors(errors.ToImmutable());
}
internal static ExpectedError ToExpectedError(in (string Type, string Message, ImmutableDictionary<string, string> ExtensionData) errorInfo)
=> new(errorInfo.Type, errorInfo.Message) { ExtensionData = errorInfo.ExtensionData };
internal static (string Type, string Message, ImmutableDictionary<string, string> ExtensionData) ReadOne(ref Utf8JsonReader reader)
{
ImmutableDictionary<string, string>.Builder? extensionData = null;
string type = "error";
string message = "";
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
{
var propname = reader.GetString();
reader.Read();
if (reader.TokenType == JsonTokenType.Null)
break;
while (reader.TokenType == JsonTokenType.Comment) reader.Read();
if (!(reader.TokenType == JsonTokenType.String))
throw new JsonException("Unable to deserialize Error type.");
var propvalue = reader.GetString();
if (string.IsNullOrEmpty(propvalue))
break;
if (propname == "type" || string.Equals(propname, "type", StringComparison.InvariantCultureIgnoreCase))
{
type = propvalue;
}
else if (propname == "msg" || string.Equals(propname, "msg", StringComparison.InvariantCultureIgnoreCase))
{
message = propvalue;
}
else if (!string.IsNullOrEmpty(propname))
{
extensionData ??= ImmutableDictionary.CreateBuilder<string, string>();
extensionData[propname] = propvalue;
}
break;
}
case JsonTokenType.Comment: break;
case JsonTokenType.EndObject: goto endLoop;
default: throw new JsonException("Unable to deserialize Error type.");
}
}
endLoop:
return (type, message, extensionData?.ToImmutable() ?? ImmutableDictionary<string, string>.Empty);
}
internal static void WriteOne(Utf8JsonWriter writer, Error value)
{
writer.WriteStartObject();
writer.WriteString("type", value.Type);
writer.WriteString("msg", value.Message);
if (value.ExtensionData?.Count > 0)
{
foreach (var (key, val) in value.ExtensionData)
{
writer.WriteString(key, val);
}
}
writer.WriteEndObject();
}
}
public sealed class ExpectedErrorJsonConverter : JsonConverter<ExpectedError>
{
public override ExpectedError? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.StartObject => ErrorJsonConverter.ToExpectedError(ErrorJsonConverter.ReadOne(ref reader)),
JsonTokenType.None => null,
JsonTokenType.Null => null,
_ => throw new JsonException("Unexpected JSON token.")
};
}
public override void Write(Utf8JsonWriter writer, ExpectedError value, JsonSerializerOptions options)
{
ErrorJsonConverter.WriteOne(writer, value);
}
}
public sealed class ExceptionalErrorJsonConverter : JsonConverter<ExceptionalError>
{
public override ExceptionalError? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.StartObject => ToExceptionalError(ErrorJsonConverter.ReadOne(ref reader)),
JsonTokenType.None => null,
JsonTokenType.Null => null,
_ => throw new JsonException("Unexpected JSON token.")
};
}
public override void Write(Utf8JsonWriter writer, ExceptionalError value, JsonSerializerOptions options)
{
ErrorJsonConverter.WriteOne(writer, value);
}
private static ExceptionalError ToExceptionalError(in (string Type, string Message, ImmutableDictionary<string, string> ExtensionData) errorInfo)
=> new(errorInfo.Type, errorInfo.Message) { ExtensionData = errorInfo.ExtensionData };
}
public sealed class ManyErrorsJsonConverter : JsonConverter<ManyErrors>
{
public override ManyErrors? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.StartArray => ErrorJsonConverter.ReadMany(ref reader),
JsonTokenType.None => null,
JsonTokenType.Null => null,
_ => throw new JsonException("Unexpected JSON token.")
};
}
public override void Write(Utf8JsonWriter writer, ManyErrors value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var err in value)
{
ErrorJsonConverter.WriteOne(writer, err);
}
writer.WriteEndArray();
}
}

View File

@@ -1,16 +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>
<LangVersion>12.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>Just.Railway</AssemblyName> <AssemblyName>Just.Railway</AssemblyName>
<RootNamespace>Just.Railway</RootNamespace> <RootNamespace>Just.Railway</RootNamespace>
<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>
<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>
<RepositoryUrl>https://gitea.jstdev.ru/just/Just.Railway/</RepositoryUrl> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/JustFixMe/Just.Railway/</RepositoryUrl>
<EmitCompilerGeneratedFiles Condition="'$(Configuration)'=='Debug'">true</EmitCompilerGeneratedFiles> <EmitCompilerGeneratedFiles Condition="'$(Configuration)'=='Debug'">true</EmitCompilerGeneratedFiles>
<ReleaseVersion Condition=" '$(ReleaseVersion)' == '' ">1.0.0</ReleaseVersion> <ReleaseVersion Condition=" '$(ReleaseVersion)' == '' ">1.0.0</ReleaseVersion>
@@ -24,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,9 +9,8 @@ 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?, bool> IsEqualFunc;
public static readonly Func<T?, T?, int> CompareFunc; public static readonly Func<T?, T?, int> CompareFunc;
@@ -65,3 +63,4 @@ file static class TypeReflectionCache<T>
: right is null ? 1 : left.Value.CompareTo(right.Value); : right is null ? 1 : left.Value.CompareTo(right.Value);
#pragma warning restore CS8604 // Possible null reference argument. #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,12 +1,88 @@
using System.Collections.Immutable;
namespace Just.Railway; namespace Just.Railway;
public static partial class ResultExtensions public static partial class ResultExtensions
{ {
#region Match (with fallback)
public static T Match<T>(this in Result<T> result, Func<Error, T> fallback)
{
return result.State switch
{
ResultState.Success => result.Value,
ResultState.Error => fallback(result.Error!),
_ => throw new ResultNotInitializedException(nameof(result))
};
}
public static async Task<T> Match<T>(this Result<T> result, Func<Error, Task<T>> fallback)
{
return result.State switch
{
ResultState.Success => result.Value,
ResultState.Error => await fallback(result.Error!).ConfigureAwait(false),
_ => throw new ResultNotInitializedException(nameof(result))
};
}
public static async Task<T> Match<T>(this Task<Result<T>> resultTask, Func<Error, T> fallback)
{
var result = await resultTask.ConfigureAwait(false);
return result.State switch
{
ResultState.Success => result.Value,
ResultState.Error => fallback(result.Error!),
_ => throw new ResultNotInitializedException(nameof(resultTask))
};
}
public static async Task<T> Match<T>(this Task<Result<T>> resultTask, Func<Error, Task<T>> fallback)
{
var result = await resultTask.ConfigureAwait(false);
return result.State switch
{
ResultState.Success => result.Value,
ResultState.Error => await fallback(result.Error!).ConfigureAwait(false),
_ => throw new ResultNotInitializedException(nameof(resultTask))
};
}
public static async ValueTask<T> Match<T>(this Result<T> result, Func<Error, ValueTask<T>> fallback)
{
return result.State switch
{
ResultState.Success => result.Value,
ResultState.Error => await fallback(result.Error!).ConfigureAwait(false),
_ => throw new ResultNotInitializedException(nameof(result))
};
}
public static async ValueTask<T> Match<T>(this ValueTask<Result<T>> resultTask, Func<Error, T> fallback)
{
var result = await resultTask.ConfigureAwait(false);
return result.State switch
{
ResultState.Success => result.Value,
ResultState.Error => fallback(result.Error!),
_ => throw new ResultNotInitializedException(nameof(resultTask))
};
}
public static async ValueTask<T> Match<T>(this ValueTask<Result<T>> resultTask, Func<Error, ValueTask<T>> fallback)
{
var result = await resultTask.ConfigureAwait(false);
return result.State switch
{
ResultState.Success => result.Value,
ResultState.Error => await fallback(result.Error!).ConfigureAwait(false),
_ => throw new ResultNotInitializedException(nameof(resultTask))
};
}
#endregion
#region Merge #region Merge
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))
@@ -15,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:
@@ -28,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)
@@ -39,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))
@@ -49,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;
@@ -64,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,5 +1,3 @@
using System.Collections.Immutable;
namespace Railway.Tests.Errors; namespace Railway.Tests.Errors;
public class Serialization public class Serialization
@@ -8,35 +6,64 @@ public class Serialization
public void WhenSerializingManyErrors() public void WhenSerializingManyErrors()
{ {
// Given // Given
Error many_errors = new ManyErrors(new Error[]{ Error many_errors = new ManyErrors(
new ExpectedError("err1", "msg1"){ [
ExtensionData = ImmutableDictionary<string, string>.Empty Error.New("err1", "msg1", new KeyValuePair<string, string>[]
.Add("ext", "ext_value"), {
}, new("ext", "ext_value"),
new ExceptionalError(new Exception("msg2")), }),
}); Error.New(new Exception("msg2")),
]);
// When // When
var result = JsonSerializer.Serialize(many_errors); var result = JsonSerializer.Serialize(many_errors);
// Then // Then
Assert.Equal( Assert.Equal(
expected: "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ExtensionData\":{\"ext\":\"ext_value\"}},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]", expected: "[{\"type\":\"err1\",\"msg\":\"msg1\",\"ext\":\"ext_value\"},{\"type\":\"System.Exception\",\"msg\":\"msg2\"}]",
result); result);
} }
[Fact] [Fact]
public void WhenDeserializingManyErrors() public void WhenDeserializingManyErrorsAsError()
{ {
// Given // Given
var json = "[{\"$$err\":0,\"Type\":\"err1\",\"Message\":\"msg1\",\"ExtensionData\":{\"ext1\":\"ext_value1\",\"ext2\":\"ext_value2\"}},{\"$$err\":1,\"Type\":\"Exception\",\"Message\":\"msg2\"}]"; var json = "[{\"type\":\"err1\",\"msg\":\"msg1\",\"ext1\":\"ext_value1\",\"ext2\":\"ext_value2\"},{\"type\":\"System.Exception\",\"msg\":\"msg2\"}]";
// When // When
var result = JsonSerializer.Deserialize<Error[]>(json); var result = JsonSerializer.Deserialize<Error>(json);
// Then // Then
Assert.True(result?.Length == 2); Assert.IsType<ManyErrors>(result);
ManyErrors manyErrors = (ManyErrors)result;
Assert.True(manyErrors.Count == 2);
Assert.Equal( Assert.Equal(
expected: new ManyErrors(new Error[]{ expected: Error.Many(
new ExpectedError("err1", "msg1"), Error.New("err1", "msg1"),
new ExceptionalError(new Exception("msg2")), Error.New(new Exception("msg2"))
}), ).ToEnumerable(),
manyErrors
);
Assert.Equal(
expected: "ext_value1",
manyErrors[0]["ext1"]);
Assert.Equal(
expected: "ext_value2",
manyErrors[0]["ext2"]);
}
[Fact]
public void WhenDeserializingManyErrorsAsManyErrors()
{
// Given
var json = "[{\"type\":\"err1\",\"msg\":\"msg1\",\"ext1\":\"ext_value1\",\"ext2\":\"ext_value2\"},{\"type\":\"System.Exception\",\"msg\":\"msg2\"}]";
// When
var result = JsonSerializer.Deserialize<ManyErrors>(json);
// Then
Assert.NotNull(result);
Assert.True(result.Count == 2);
Assert.Equal(
expected: Error.Many(
Error.New("err1", "msg1"),
Error.New(new Exception("msg2"))
).ToEnumerable(),
result result
); );
Assert.Equal( Assert.Equal(

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);
}
} }