diff --git a/Railway.SourceGenerator/EnsureExtensionExecutor.cs b/Railway.SourceGenerator/EnsureExtensionExecutor.cs new file mode 100644 index 0000000..ce4c9c4 --- /dev/null +++ b/Railway.SourceGenerator/EnsureExtensionExecutor.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Just.Railway.SourceGen; + +public sealed class EnsureExtensionExecutor : IGeneratorExecutor +{ + public void Execute(SourceProductionContext context, Compilation source) + { + var methods = GenerateMethods(); + var code = $$""" + #nullable enable + using System; + using System.Linq; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.CodeDom.Compiler; + + namespace Just.Railway; + + public static partial class Ensure + { + {{methods}} + } + """; + + context.AddSource("Ensure.Extensions.g.cs", code); + } + + private string GenerateMethods() + { + List<(string ErrorParameterDecl, string ErrorValueExpr)> errorGenerationDefinitions = + [ + ("Error error = default!", "error"), + ("ErrorFactory errorFactory", "errorFactory(ensure.ValueExpression)") + ]; + + var sb = new StringBuilder(); + + sb.AppendLine($"#region Satisfies"); + errorGenerationDefinitions.ForEach(def => GenerateSatisfiesExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); + sb.AppendLine("#endregion"); + + sb.AppendLine($"#region NotNull"); + errorGenerationDefinitions.ForEach(def => GenerateNotNullExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); + sb.AppendLine("#endregion"); + + sb.AppendLine($"#region NotEmpty"); + errorGenerationDefinitions.ForEach(def => GenerateNotEmptyExtensions(sb, def.ErrorParameterDecl, def.ErrorValueExpr)); + sb.AppendLine("#endregion"); + + + return sb.ToString(); + } + + private void GenerateNotEmptyExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr) + { + string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is empty.\")"; + List<(string TemplateDef, string CollectionType, string NotEmptyTest)> typeOverloads = + [ + ("", "IEnumerable", "ensure.Value?.Any() == true"), + ("", "ICollection", "ensure.Value?.Count > 0"), + ("", "IReadOnlyCollection", "ensure.Value?.Count > 0"), + ("", "IList", "ensure.Value?.Count > 0"), + ("", "IReadOnlyList", "ensure.Value?.Count > 0"), + ("", "ISet", "ensure.Value?.Count > 0"), + ("", "IReadOnlySet", "ensure.Value?.Count > 0"), + ("", "IDictionary", "ensure.Value?.Count > 0"), + ("", "IReadOnlyDictionary", "ensure.Value?.Count > 0"), + ("", "T[]", "ensure.Value?.Length > 0"), + ("", "List", "ensure.Value?.Count > 0"), + ("", "Queue", "ensure.Value?.Count > 0"), + ("", "HashSet", "ensure.Value?.Count > 0"), + ("", "string", "!string.IsNullOrEmpty(ensure.Value)"), + ]; + + typeOverloads.ForEach(def => sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + public static Ensure<{{def.CollectionType}}> NotEmpty{{def.TemplateDef}}(this in Ensure<{{def.CollectionType}}> ensure, {{errorParameterDecl}}) + { + return ensure.State switch + { + ResultState.Success => {{def.NotEmptyTest}} + ? new(ensure.Value, 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) + { + string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} is null.\")"; + + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + public static Ensure NotNull(this in Ensure ensure, {{errorParameterDecl}}) + where T : struct + { + return ensure.State switch + { + ResultState.Success => ensure.Value.HasValue + ? new(ensure.Value.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(EnsureExtensionExecutor)}}", "1.0.0.0")] + public static Ensure NotNull(this in Ensure ensure, {{errorParameterDecl}}) + where T : notnull + { + return ensure.State switch + { + ResultState.Success => ensure.Value is not null + ? new(ensure.Value!, ensure.ValueExpression) + : new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression), + ResultState.Error => new(ensure.Error!, ensure.ValueExpression), + _ => throw new EnsureNotInitializedException(nameof(ensure)) + }; + } + """); + } + + private void GenerateSatisfiesExtensions(StringBuilder sb, string errorParameterDecl, string errorValueExpr) + { + string defaultErrorExpr = "?? Error.New(DefaultErrorType, $\"Value {{{ensure.ValueExpression}}} does not satisfy the requirement.\")"; + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + public static Ensure Satisfies(this in Ensure ensure, Func requirement, {{errorParameterDecl}}) + { + return ensure.State switch + { + ResultState.Success => requirement(ensure.Value) + ? new(ensure.Value, ensure.ValueExpression) + : new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression), + ResultState.Error => new(ensure.Error!, ensure.ValueExpression), + _ => throw new EnsureNotInitializedException(nameof(ensure)) + }; + } + """); + + GenerateSatisfiesAsyncExtensions(sb, "Task", errorParameterDecl, errorValueExpr, defaultErrorExpr); + GenerateSatisfiesAsyncExtensions(sb, "ValueTask", errorParameterDecl, errorValueExpr, defaultErrorExpr); + } + + private void GenerateSatisfiesAsyncExtensions(StringBuilder sb, string taskType, string errorParameterDecl, string errorValueExpr, string defaultErrorExpr) + { + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + public static async {{taskType}}> Satisfies(this {{taskType}}> ensureTask, Func requirement, {{errorParameterDecl}}) + { + var ensure = await ensureTask.ConfigureAwait(false); + return ensure.State switch + { + ResultState.Success => requirement(ensure.Value) + ? new(ensure.Value, ensure.ValueExpression) + : new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression), + ResultState.Error => new(ensure.Error!, ensure.ValueExpression), + _ => throw new EnsureNotInitializedException(nameof(ensureTask)) + }; + } + """); + sb.AppendLine($$""" + [PureAttribute] + [GeneratedCodeAttribute("{{nameof(EnsureExtensionExecutor)}}", "1.0.0.0")] + public static async {{taskType}}> Satisfies(this Ensure ensure, Func> requirement, {{errorParameterDecl}}) + { + return ensure.State switch + { + ResultState.Success => await requirement(ensure.Value).ConfigureAwait(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(EnsureExtensionExecutor)}}", "1.0.0.0")] + public static async {{taskType}}> Satisfies(this {{taskType}}> ensureTask, Func> requirement, {{errorParameterDecl}}) + { + var ensure = await ensureTask.ConfigureAwait(false); + return ensure.State switch + { + ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false) + ? new(ensure.Value, ensure.ValueExpression) + : new({{errorValueExpr}} {{defaultErrorExpr}}, ensure.ValueExpression), + ResultState.Error => new(ensure.Error!, ensure.ValueExpression), + _ => throw new EnsureNotInitializedException(nameof(ensureTask)) + }; + } + """); + } +} diff --git a/Railway.SourceGenerator/ExtensionsMethodGenerator.cs b/Railway.SourceGenerator/ExtensionsMethodGenerator.cs index 097f13b..32bb4e1 100644 --- a/Railway.SourceGenerator/ExtensionsMethodGenerator.cs +++ b/Railway.SourceGenerator/ExtensionsMethodGenerator.cs @@ -19,6 +19,7 @@ public class ExtensionsMethodGenerator : IIncrementalGenerator new ResultTapExecutor(), new ResultAppendExecutor(), new TryExtensionsExecutor(), + new EnsureExtensionExecutor(), }; public void Initialize(IncrementalGeneratorInitializationContext context) diff --git a/Railway/Ensure.cs b/Railway/Ensure.cs index 145b3ab..2558b35 100644 --- a/Railway/Ensure.cs +++ b/Railway/Ensure.cs @@ -1,13 +1,12 @@ namespace Just.Railway; -public static class Ensure +public static partial class Ensure { public delegate Error ErrorFactory(string valueExpression); public const string DefaultErrorType = "EnsureFailed"; [Pure] public static Ensure That(T value, [CallerArgumentExpression(nameof(value))]string valueExpression = "") => new(value, valueExpression); - [Pure] public static async Task> That(Task value, [CallerArgumentExpression(nameof(value))]string valueExpression = "") => new(await value.ConfigureAwait(false), valueExpression); [Pure] public static Result Result(this in Ensure ensure) => ensure.State switch { @@ -15,8 +14,7 @@ public static class Ensure ResultState.Error => new(ensure.Error!), _ => throw new EnsureNotInitializedException(nameof(ensure)) }; - [Pure] - public static async Task> Result(this Task> ensureTask) + [Pure] public static async Task> Result(this Task> ensureTask) { var ensure = await ensureTask.ConfigureAwait(false); return ensure.State switch @@ -26,261 +24,18 @@ public static class Ensure _ => throw new EnsureNotInitializedException(nameof(ensureTask)) }; } - [Pure] - public static Ensure Satisfies(this in Ensure ensure, Func requirement, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => requirement(ensure.Value) - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} does not satisfy the requirement."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static Ensure Satisfies(this in Ensure ensure, Func requirement, ErrorFactory errorFactory) - { - return ensure.State switch - { - ResultState.Success => requirement(ensure.Value) - ? new(ensure.Value, ensure.ValueExpression) - : new(errorFactory(ensure.ValueExpression), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static async Task> Satisfies(this Task> ensureTask, Func requirement, Error error = default!) + [Pure] public static async ValueTask> Result(this ValueTask> ensureTask) { var ensure = await ensureTask.ConfigureAwait(false); return ensure.State switch { - ResultState.Success => requirement(ensure.Value) - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} does not satisfy the requirement."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensureTask)) - }; - } - [Pure] - public static async Task> Satisfies(this Task> ensureTask, Func requirement, ErrorFactory errorFactory) - { - var ensure = await ensureTask.ConfigureAwait(false); - return ensure.State switch - { - ResultState.Success => requirement(ensure.Value) - ? new(ensure.Value, ensure.ValueExpression) - : new(errorFactory(ensure.ValueExpression), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensureTask)) - }; - } - [Pure] - public static async Task> Satisfies(this Ensure ensure, Func> requirement, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false) - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} does not satisfy the requirement."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static async Task> Satisfies(this Ensure ensure, Func> requirement, ErrorFactory errorFactory) - { - return ensure.State switch - { - ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false) - ? new(ensure.Value, ensure.ValueExpression) - : new(errorFactory(ensure.ValueExpression), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static async Task> Satisfies(this Task> ensureTask, Func> requirement, Error error = default!) - { - var ensure = await ensureTask.ConfigureAwait(false); - return ensure.State switch - { - ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false) - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} does not satisfy the requirement."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensureTask)) - }; - } - [Pure] - public static async Task> Satisfies(this Task> ensureTask, Func> requirement, ErrorFactory errorFactory) - { - var ensure = await ensureTask.ConfigureAwait(false); - return ensure.State switch - { - ResultState.Success => await requirement(ensure.Value).ConfigureAwait(false) - ? new(ensure.Value, ensure.ValueExpression) - : new(errorFactory(ensure.ValueExpression), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), + ResultState.Success => new(ensure.Value), + ResultState.Error => new(ensure.Error!), _ => throw new EnsureNotInitializedException(nameof(ensureTask)) }; } - [Pure] - public static Ensure NotNull(this in Ensure ensure, Error error = default!) - where T : struct - { - return ensure.State switch - { - ResultState.Success => ensure.Value.HasValue - ? new(ensure.Value.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is null."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static async Task> NotNull(this Task> ensureTask, Error error = default!) - where T : struct - { - var ensure = await ensureTask.ConfigureAwait(false); - return ensure.State switch - { - ResultState.Success => ensure.Value.HasValue - ? new(ensure.Value.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is null."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensureTask)) - }; - } - [Pure] - public static Ensure NotNull(this in Ensure ensure, Error error = default!) - where T : notnull - { - return ensure.State switch - { - ResultState.Success => ensure.Value is not null - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is null."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static async Task> NotNull(this Task> ensureTask, Error error = default!) - where T : notnull - { - var ensure = await ensureTask.ConfigureAwait(false); - return ensure.State switch - { - ResultState.Success => ensure.Value is not null - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is null."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensureTask)) - }; - } - - [Pure] - public static Ensure NotEmpty(this in Ensure ensure, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => ensure.Value is not null && ensure.Value.Length > 0 - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static Ensure> NotEmpty(this in Ensure> ensure, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0 - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static Ensure> NotEmpty(this in Ensure> ensure, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0 - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static Ensure> NotEmpty(this in Ensure> ensure, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0 - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static Ensure> NotEmpty(this in Ensure> ensure, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0 - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static Ensure> NotEmpty(this in Ensure> ensure, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => ensure.Value is not null && ensure.Value.Count > 0 - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static Ensure> NotEmpty(this in Ensure> ensure, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => ensure.Value is not null && ensure.Value.Any() - ? new(ensure.Value, ensure.ValueExpression) - : new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - [Pure] - public static Ensure NotEmpty(this in Ensure ensure, Error error = default!) - { - return ensure.State switch - { - ResultState.Success => string.IsNullOrEmpty(ensure.Value) - ? new(error ?? Error.New(DefaultErrorType, $"Value {{{ensure.ValueExpression}}} is empty."), ensure.ValueExpression) - : new(ensure.Value, ensure.ValueExpression), - ResultState.Error => new(ensure.Error!, ensure.ValueExpression), - _ => throw new EnsureNotInitializedException(nameof(ensure)) - }; - } - - [Pure] - public static Ensure NotWhitespace(this in Ensure ensure, Error error = default!) + [Pure] public static Ensure NotWhitespace(this in Ensure ensure, Error error = default!) { return ensure.State switch {