Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ nested `IUnionMembers` explicitly so it stays out of the public API), `Valid.cs`
- **V8 Dependent steps:** `Then`/`Ensure` short-circuit; deliberately not called `Bind`, and no `SelectMany`.
- **V9 Zero allocation** on the valid path for value types in Release builds; an invalid validation allocates its
errors (documented exclusion).
- **V10 Async, minimal:** `MatchAsync`, `MapAsync`, `ThenAsync`, `Validated.CombineAsync` over any mix of
`Validated` and `Task<Validated>`; `ValueTask` results with dotResult's R8 semantics.
- **V10 Async, minimal:** `MatchAsync`, `MapAsync`, `ThenAsync`, and `Validated.CombineAsync` over 2–8
`Task<Validated>` inputs (a synchronous one goes in as `Task.FromResult(x)`; mixing bare validations and tasks cannot
infer types, see #16). `ValueTask` results with dotResult's R8 semantics. Split sync and async checks, then `Zip`.
- **V11 Equality:** values by `EqualityComparer<T>.Default`, errors as sequences; valid never equals invalid. No ordering.
- **V12 Collections** accumulate all errors. **V13** Small helpers (`Check`, `FromNullable`, `ValidationError`).
- **V14 Quality gates:** AOT/trimming clean, `AnalysisLevel latest-all` + `TreatWarningsAsErrors`, PublicAPI analyzer,
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,21 @@ string message = await user.MatchAsync(
Validated<string, string> name = await user.MapAsync(found => LoadDisplayNameAsync(found));
```

Independent checks that need I/O combine with `Validated.CombineAsync`, the asynchronous `Combine`: the tasks run
concurrently and every error of every check comes back. Every input is a task, so a synchronous check goes in as
`Task.FromResult(...)` (C# cannot infer the types of a mix of plain validations and tasks). Often it reads better to
combine the synchronous checks on their own and join both parts with `Zip`:

```csharp
var local = Validated.Combine(Name(dto.Name), Age(dto.Age), (name, age) => (name, age));
var remote = await Validated.CombineAsync(
EmailIsFree(dto.Email), // Task<Validated<string, string>>
LoginIsFree(dto.Login),
(email, login) => (email, login));

Validated<User, string> user = local.Zip(remote, (l, r) => new User(l.name, l.age, r.email, r.login));
```

The async members are deliberately few. The delegates return `Task<T>`; the methods return `ValueTask<T>`, already
complete (and allocation-free) when the case passes through. Arguments are checked immediately; anything a delegate
throws comes out when the result is awaited, and an `OperationCanceledException` for a requested token gives a
Expand Down
13 changes: 13 additions & 0 deletions smoke/DotValid.AotSmoke/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@
Check(described == "valid 42" && await failed.MatchAsync(static _ => Task.FromResult(0), static errors => errors.Length) == 2, "MatchAsync");
Check(await age.MapAsync(static value => Task.FromResult(value + 1)) == valid(43) && await failed.MapAsync(static value => Task.FromResult(value)) == failed, "MapAsync");
Check(await Parse("7").ThenAsync(static value => Task.FromResult(Adult(value))) == invalid("too young"), "ThenAsync");
var remote = await Validated.CombineAsync(
Task.FromResult(Parse("x")),
Later(invalid("email taken")),
Task.FromResult(age),
static (a, b, c) => a + b + c);
Check(remote == invalid("not a number", "email taken"), "CombineAsync accumulates");
Check(age.Zip(remote, static (a, b) => a + b) == remote, "Zip joins a sync and an async part");

if (failures.Count > 0)
{
Expand All @@ -83,6 +90,12 @@
Invalid<string>(var errors) => $"{errors.Length} errors",
};

static async Task<Validated<int, string>> Later(Validated<int, string> validated)
{
await Task.Yield();
return validated;
}

static Validated<int, string> Adult(int value) => value >= 18 ? valid(value) : invalid("too young");

static Validated<int, string> Parse(string text) =>
Expand Down
7 changes: 7 additions & 0 deletions src/DotValid/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ static DotValid.Validated.Combine<T1, T2, T3, T4, T5, TResult, TError>(DotValid.
static DotValid.Validated.Combine<T1, T2, T3, T4, TResult, TError>(DotValid.Validated<T1, TError> first, DotValid.Validated<T2, TError> second, DotValid.Validated<T3, TError> third, DotValid.Validated<T4, TError> fourth, System.Func<T1, T2, T3, T4, TResult>! combine) -> DotValid.Validated<TResult, TError>
static DotValid.Validated.Combine<T1, T2, T3, TResult, TError>(DotValid.Validated<T1, TError> first, DotValid.Validated<T2, TError> second, DotValid.Validated<T3, TError> third, System.Func<T1, T2, T3, TResult>! combine) -> DotValid.Validated<TResult, TError>
static DotValid.Validated.Combine<T1, T2, TResult, TError>(DotValid.Validated<T1, TError> first, DotValid.Validated<T2, TError> second, System.Func<T1, T2, TResult>! combine) -> DotValid.Validated<TResult, TError>
static DotValid.Validated.CombineAsync<T1, T2, T3, T4, T5, T6, T7, T8, TResult, TError>(System.Threading.Tasks.Task<DotValid.Validated<T1, TError>>! first, System.Threading.Tasks.Task<DotValid.Validated<T2, TError>>! second, System.Threading.Tasks.Task<DotValid.Validated<T3, TError>>! third, System.Threading.Tasks.Task<DotValid.Validated<T4, TError>>! fourth, System.Threading.Tasks.Task<DotValid.Validated<T5, TError>>! fifth, System.Threading.Tasks.Task<DotValid.Validated<T6, TError>>! sixth, System.Threading.Tasks.Task<DotValid.Validated<T7, TError>>! seventh, System.Threading.Tasks.Task<DotValid.Validated<T8, TError>>! eighth, System.Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult>! combine) -> System.Threading.Tasks.ValueTask<DotValid.Validated<TResult, TError>>
static DotValid.Validated.CombineAsync<T1, T2, T3, T4, T5, T6, T7, TResult, TError>(System.Threading.Tasks.Task<DotValid.Validated<T1, TError>>! first, System.Threading.Tasks.Task<DotValid.Validated<T2, TError>>! second, System.Threading.Tasks.Task<DotValid.Validated<T3, TError>>! third, System.Threading.Tasks.Task<DotValid.Validated<T4, TError>>! fourth, System.Threading.Tasks.Task<DotValid.Validated<T5, TError>>! fifth, System.Threading.Tasks.Task<DotValid.Validated<T6, TError>>! sixth, System.Threading.Tasks.Task<DotValid.Validated<T7, TError>>! seventh, System.Func<T1, T2, T3, T4, T5, T6, T7, TResult>! combine) -> System.Threading.Tasks.ValueTask<DotValid.Validated<TResult, TError>>
static DotValid.Validated.CombineAsync<T1, T2, T3, T4, T5, T6, TResult, TError>(System.Threading.Tasks.Task<DotValid.Validated<T1, TError>>! first, System.Threading.Tasks.Task<DotValid.Validated<T2, TError>>! second, System.Threading.Tasks.Task<DotValid.Validated<T3, TError>>! third, System.Threading.Tasks.Task<DotValid.Validated<T4, TError>>! fourth, System.Threading.Tasks.Task<DotValid.Validated<T5, TError>>! fifth, System.Threading.Tasks.Task<DotValid.Validated<T6, TError>>! sixth, System.Func<T1, T2, T3, T4, T5, T6, TResult>! combine) -> System.Threading.Tasks.ValueTask<DotValid.Validated<TResult, TError>>
static DotValid.Validated.CombineAsync<T1, T2, T3, T4, T5, TResult, TError>(System.Threading.Tasks.Task<DotValid.Validated<T1, TError>>! first, System.Threading.Tasks.Task<DotValid.Validated<T2, TError>>! second, System.Threading.Tasks.Task<DotValid.Validated<T3, TError>>! third, System.Threading.Tasks.Task<DotValid.Validated<T4, TError>>! fourth, System.Threading.Tasks.Task<DotValid.Validated<T5, TError>>! fifth, System.Func<T1, T2, T3, T4, T5, TResult>! combine) -> System.Threading.Tasks.ValueTask<DotValid.Validated<TResult, TError>>
static DotValid.Validated.CombineAsync<T1, T2, T3, T4, TResult, TError>(System.Threading.Tasks.Task<DotValid.Validated<T1, TError>>! first, System.Threading.Tasks.Task<DotValid.Validated<T2, TError>>! second, System.Threading.Tasks.Task<DotValid.Validated<T3, TError>>! third, System.Threading.Tasks.Task<DotValid.Validated<T4, TError>>! fourth, System.Func<T1, T2, T3, T4, TResult>! combine) -> System.Threading.Tasks.ValueTask<DotValid.Validated<TResult, TError>>
static DotValid.Validated.CombineAsync<T1, T2, T3, TResult, TError>(System.Threading.Tasks.Task<DotValid.Validated<T1, TError>>! first, System.Threading.Tasks.Task<DotValid.Validated<T2, TError>>! second, System.Threading.Tasks.Task<DotValid.Validated<T3, TError>>! third, System.Func<T1, T2, T3, TResult>! combine) -> System.Threading.Tasks.ValueTask<DotValid.Validated<TResult, TError>>
static DotValid.Validated.CombineAsync<T1, T2, TResult, TError>(System.Threading.Tasks.Task<DotValid.Validated<T1, TError>>! first, System.Threading.Tasks.Task<DotValid.Validated<T2, TError>>! second, System.Func<T1, T2, TResult>! combine) -> System.Threading.Tasks.ValueTask<DotValid.Validated<TResult, TError>>
static DotValid.Validated.Errors<T, TError>(this System.Collections.Generic.IEnumerable<DotValid.Validated<T, TError>>! source) -> System.Collections.Generic.IEnumerable<TError>!
static DotValid.Validated.FromNullable<T, TError>(T? value, System.Func<TError>! error) -> DotValid.Validated<T!, TError>
static DotValid.Validated.FromNullable<T, TError>(T? value, System.Func<TError>! error) -> DotValid.Validated<T, TError>
Expand Down
Loading
Loading