Etymon.Invariants
0.1.0-preview.13
dotnet add package Etymon.Invariants --version 0.1.0-preview.13
NuGet\Install-Package Etymon.Invariants -Version 0.1.0-preview.13
<PackageReference Include="Etymon.Invariants" Version="0.1.0-preview.13" />
<PackageVersion Include="Etymon.Invariants" Version="0.1.0-preview.13" />
<PackageReference Include="Etymon.Invariants" />
paket add Etymon.Invariants --version 0.1.0-preview.13
#r "nuget: Etymon.Invariants, 0.1.0-preview.13"
#:package Etymon.Invariants@0.1.0-preview.13
#addin nuget:?package=Etymon.Invariants&version=0.1.0-preview.13&prerelease
#tool nuget:?package=Etymon.Invariants&version=0.1.0-preview.13&prerelease
Etymon.Invariants
Rules that must always be true of a type, declared once and exposed as data.
Part of the Etymon suite. Depends on
Etymon.Core and nothing else.
Single-field rules already have a home: Constraint in Etymon.Core, attached
to the field it restricts. This package is for the rules that cannot live on any
one field.
let invariants =
Invariants.forType "Booking" [
Invariant.after ("endDate", fun b -> b.EndDate) ("startDate", fun b -> b.StartDate)
Invariant.atMostOne "one-payment-method" [
"cardId", fun b -> b.CardId.IsSome
"invoiceId", fun b -> b.InvoiceId.IsSome
]
Invariant.create "party-fits" "a booking of more than 4 guests must last at least two nights"
(fun b -> b.Guests <= 4 || b.Nights >= 2)
|> Invariant.about [ "guests"; "endDate" ]
]
Every rule runs
Invariants.check invariants booking
endDate: endDate must be after startDate
cardId: at most one of cardId, invoiceId may be given
guests: a booking of more than 4 guests must last at least two nights
Three broken rules, three errors, in declaration order. An error lands on the first field the rule concerns, so it appears next to the input a person would look at, and every field it concerns is carried as data so a form can highlight all of them.
Name is a stable code (endDate-after-startDate) that a caller can branch on;
Description is the prose. Nobody should have to parse a message.
Composing with a schema
Schema.fromJson bookingSchema payload
|> Invariants.andThen bookingInvariants
Field-level errors and cross-field errors arrive together rather than in two rounds. One caveat, stated because it is the only place in Etymon where an error hides another: if a field fails to decode, the cross-field rules cannot run, because there is no value to check. That is unavoidable rather than a design choice.
What this package cannot do
Your original instinct may have been "the constructor enforces the invariants." F# has no mechanism for that short of a compiler plugin or IL weaving, and a library claiming otherwise would be lying to you.
What it can do is make the call one line, and make the rules visible:
type Booking = private Booking of BookingFields
module Booking =
let create fields =
Invariants.enforce invariants fields |> Validation.map Booking
enforce is check under a different name, named for the place it belongs so
that its absence from a constructor reads as an omission rather than as nothing
at all. And because the rules are data, a type whose constructor does not enforce
its own invariants is something a reviewer — or a test — can notice.
Why the rules are data
Invariants.rules hands back the list. That is what lets other packages read
them: Etymon.Invariants.FsCheck generates values that satisfy them,
Etymon.Schema.Sql can turn the expressible ones into database checks, and
documentation can list them without restating anything.
A rule written inline inside a constructor is a rule nothing else can see.
Invariants.describe bookingInvariants
// [ "endDate must be after startDate"
// "at most one of cardId, invoiceId may be given"
// "a booking of more than 4 guests must last at least two nights" ]
Built-in shapes
after and notBefore (the one everybody writes first and gets subtly wrong by
allowing equality), compare, allOrNone, atMostOne, exactlyOne, and
whenever for a rule that only applies in some circumstances. create for
anything else.
Public surface
The complete public surface of this package, every value with its full signature
and its documentation, is in Surface.fsi beside this file. It is written by the
build from the implementation, so it is where to learn what a function hands
back without compiling anything.
Licence
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Etymon.Core (>= 0.1.0-preview.13)
- FSharp.Core (>= 8.0.100)
-
net8.0
- Etymon.Core (>= 0.1.0-preview.13)
- FSharp.Core (>= 8.0.100)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Etymon.Invariants:
| Package | Downloads |
|---|---|
|
Etymon.Invariants.FsCheck
FsCheck generators that produce only values a schema accepts, generators of values it should reject, and ready-made property checks for round-tripping, determinism and error placement. Values are generated as JSON and decoded through the schema itself, so they are valid by the same definition production code uses. |
|
|
Etymon
The Etymon suite: one schema definition as the source of truth for JSON codecs, validation, OpenAPI, TypeScript, configuration and database structure. This package has no code of its own; it references the parts of the suite that cost nothing but FSharp.Core. The adapters (Etymon.Api.Giraffe, Etymon.Api.AspNetCore) and the generator package (Etymon.Invariants.FsCheck) are deliberately left out, because each carries a dependency that would then be everyone's. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0-preview.13 | 30 | 9/22/2026 |
| 0.1.0-preview.12 | 32 | 9/22/2026 |
| 0.1.0-preview.11 | 26 | 9/22/2026 |
| 0.1.0-preview.10 | 33 | 9/22/2026 |
| 0.1.0-preview.9 | 27 | 9/22/2026 |
| 0.1.0-preview.8 | 30 | 9/22/2026 |
| 0.1.0-preview.7 | 40 | 9/21/2026 |
| 0.1.0-preview.6 | 37 | 9/21/2026 |
| 0.1.0-preview.5 | 42 | 9/21/2026 |
| 0.1.0-preview.4 | 48 | 9/21/2026 |
| 0.1.0-preview.3 | 45 | 9/21/2026 |
| 0.1.0-preview.2 | 46 | 9/21/2026 |
| 0.1.0-preview.1 | 45 | 9/20/2026 |