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
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ TinyValidations is a small compile-time validation library for application-layer

It was built for TinyDispatcher-style applications first: define a command, define its validation, register services, and let generated code do the boring work before the handler runs. Native ASP.NET integration is planned, but the core package is intentionally host-agnostic.

> Status: beta. The public shape is small and usable, but host integrations and some advanced syntax support are still evolving.
> Status: preparing for 1.0. The core package is intentionally small and host-agnostic. Native host integrations may ship separately.

## Contents

Expand All @@ -13,6 +13,7 @@ It was built for TinyDispatcher-style applications first: define a command, defi
- [Validation declarations](#validation-declarations)
- [Custom rules](#custom-rules)
- [TinyDispatcher](#tinydispatcher)
- [Design principles](#design-principles)
- [Documentation](#documentation)
- [Current limitations](#current-limitations)

Expand All @@ -33,7 +34,7 @@ The goal is not to be a huge validation framework. The goal is calm command vali

## Quick start

Install the beta package:
Install the current package:

```bash
dotnet add package TinyValidations --version 0.1.0-beta.1
Expand Down Expand Up @@ -164,6 +165,18 @@ See [`samples/TinyDispatcherAspNetCore`](samples/TinyDispatcherAspNetCore) for a

See [`samples/MediatRAspNetCore`](samples/MediatRAspNetCore) for the same application shape using a MediatR pipeline behavior.

## Design principles

TinyValidations is intentionally boring in the places that matter.

- Keep the public API small before adding convenience.
- Prefer source-generated C# over runtime reflection or expression interpretation.
- Keep the core package host-agnostic.
- Treat custom rules as the extension point for business behavior.
- Keep built-in rule declaration shapes narrow and explicit.
- Protect behavior with tests instead of testing private implementation details.
- Make dependency injection registration predictable and idempotent.

## Documentation

- [Getting started](docs/getting-started.md)
Expand All @@ -177,11 +190,11 @@ See [`samples/MediatRAspNetCore`](samples/MediatRAspNetCore) for the same applic

## Current limitations

TinyValidations is intentionally small and currently early.
TinyValidations is intentionally small.

- Source generator diagnostics are intentionally focused.
- Supported rule declaration shapes are intentionally narrow.
- Generated registration uses the current bootstrap mechanism and may change.
- Generated registration uses a bootstrap contribution mechanism that is idempotent per service collection.
- Native ASP.NET integration is planned but not implemented yet.
- Tests cover runtime behavior, generated behavior, diagnostics, and multi-assembly contributions.

Expand Down
16 changes: 16 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ The runtime library contains the public API, execution types, generated-code han

The source generator reads validation declarations and emits validation runners.

## Design Principles

TinyValidations is designed to stay small enough to understand.

- The runtime package is host-agnostic.
- Validation declarations are compile-time input, not runtime rule objects.
- Generated runners use direct C# checks for built-in rules.
- Custom async rules are the extension point for business logic and scoped services.
- Dependency injection registration must be idempotent and predictable.
- Public contracts should stay small before 1.0.
- Tests should protect user-visible behavior, not private helper trivia.

## Runtime Project

```text
Expand All @@ -25,6 +37,8 @@ src/TinyValidations
```text
src/TinyValidations.SourceGen
Analysis
Declarations
RuleInvocations
Model
Planning
Emission
Expand Down Expand Up @@ -66,6 +80,8 @@ Generated runners implement `ITinyValidationRunner<T>`.

Generated contributions register runners and custom rules with Microsoft dependency injection.

Generated contributions are added to the runtime bootstrap through module initializers. The bootstrap keeps a process-wide contribution list, but applies contributions only once per `IServiceCollection`.

## Runtime Execution

`ITinyValidator` resolves all generated runners for the command type and asks each runner to validate the command.
Expand Down
4 changes: 4 additions & 0 deletions docs/custom-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,8 @@ errors.Add(nameof(CreateUser.Email), "Email is already registered.");

The first argument is the member name. The second argument is the user-facing message.

Both values are required. TinyValidations rejects null, empty, or whitespace-only members and messages so custom-rule failures stay explicit.

`ValidationErrorCollection` is intentionally part of the public custom-rule contract for 1.0. It is small: custom rules add errors, and TinyValidations turns the collection into an immutable `ValidationResult` snapshot after validation.

For generator-native static checks, see [Extending TinyValidations](extending.md).
6 changes: 4 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ This guide shows the basic TinyValidations flow.

## Install

TinyValidations is currently published as a beta package:
Install TinyValidations:

```bash
dotnet add package TinyValidations --version 0.1.0-beta.1
```

For local development, reference the project directly.
The package is preparing for 1.0. For local development, reference the project directly.

## Register Services

Expand All @@ -25,6 +25,8 @@ var services = new ServiceCollection();
services.UseTinyValidations();
```

Calling `UseTinyValidations` more than once on the same service collection is safe.

Register any services required by custom rules before building the provider:

```csharp
Expand Down
16 changes: 7 additions & 9 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
# Roadmap

TinyValidations is beta software.
TinyValidations is preparing for 1.0.

This roadmap describes the expected direction, not a compatibility promise.

## Near Term

- Add a `.gitignore`.
- Improve source generator diagnostics.
- Add compile-and-execute generator tests.
- Expand sample projects.
- Document all supported syntax shapes.
- Add NuGet package metadata.
- Final public API review.
- Final documentation review.
- Release package metadata review.
- Keep adding behavioral tests only where they protect user-visible contracts.

## TinyDispatcher

Expand All @@ -34,6 +32,6 @@ This roadmap describes the expected direction, not a compatibility promise.

## Runtime

- Revisit `ValidationErrorCollection` as a public custom-rule surface.
- Decide whether bootstrap registration remains the long-term mechanism.
- Keep `ValidationErrorCollection` as the small public custom-rule error contract for 1.0.
- Keep bootstrap registration idempotent per service collection.
- Add more built-in helper rules only when they stay small and boring.
12 changes: 12 additions & 0 deletions src/TinyValidations/Abstractions/ValidationError.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
using System;

namespace TinyValidations;

public sealed class ValidationError
{
public ValidationError(string member, string message)
{
if (string.IsNullOrWhiteSpace(member))
{
throw new ArgumentException("Validation error member is required.", nameof(member));
}

if (string.IsNullOrWhiteSpace(message))
{
throw new ArgumentException("Validation error message is required.", nameof(message));
}

Member = member;
Message = message;
}
Expand Down
8 changes: 7 additions & 1 deletion src/TinyValidations/Abstractions/ValidationResult.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;

Expand All @@ -9,7 +10,12 @@ public sealed class ValidationResult

public ValidationResult(IReadOnlyCollection<ValidationError> errors)
{
Errors = errors;
if (errors is null)
{
throw new ArgumentNullException(nameof(errors));
}

Errors = errors.ToArray();
}

public IReadOnlyCollection<ValidationError> Errors { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

Expand All @@ -7,6 +8,11 @@ public static class TinyValidationServiceCollectionExtensions
{
public static IServiceCollection UseTinyValidations(this IServiceCollection services)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}

services.TryAddScoped<ITinyValidator, TinyValidator>();
TinyValidationBootstrap.Apply(services);
return services;
Expand Down
6 changes: 6 additions & 0 deletions src/TinyValidations/Execution/ValidationErrorCollection.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;

namespace TinyValidations;
Expand All @@ -15,6 +16,11 @@ public void Add(string member, string message)

public void AddRange(IEnumerable<ValidationError> errors)
{
if (errors is null)
{
throw new ArgumentNullException(nameof(errors));
}

_errors.AddRange(errors);
}

Expand Down
55 changes: 54 additions & 1 deletion src/TinyValidations/Generation/TinyValidationBootstrap.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -12,17 +13,34 @@ public static void AddContribution(ITinyValidationContribution contribution)
{
if (contribution is null)
{
throw new System.ArgumentNullException(nameof(contribution));
throw new ArgumentNullException(nameof(contribution));
}

lock (SyncRoot)
{
if (HasContribution(contribution))
{
return;
}

Contributions.Add(contribution);
}
}

public static void Apply(IServiceCollection services)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}

if (HasAppliedMarker(services))
{
return;
}

services.AddSingleton<TinyValidationBootstrapAppliedMarker>();

ITinyValidationContribution[] snapshot;

lock (SyncRoot)
Expand All @@ -35,4 +53,39 @@ public static void Apply(IServiceCollection services)
contribution.Register(services);
}
}

private static bool HasContribution(ITinyValidationContribution contribution)
{
foreach (var registered in Contributions)
{
if (ReferenceEquals(registered, contribution))
{
return true;
}

if (registered.GetType() == contribution.GetType())
{
return true;
}
}

return false;
}

private static bool HasAppliedMarker(IServiceCollection services)
{
foreach (var descriptor in services)
{
if (descriptor.ServiceType == typeof(TinyValidationBootstrapAppliedMarker))
{
return true;
}
}

return false;
}

private sealed class TinyValidationBootstrapAppliedMarker
{
}
}
Loading
Loading