Etymon.Invariants 0.1.0-preview.13

This is a prerelease version of Etymon.Invariants.
dotnet add package Etymon.Invariants --version 0.1.0-preview.13
                    
NuGet\Install-Package Etymon.Invariants -Version 0.1.0-preview.13
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Etymon.Invariants" Version="0.1.0-preview.13" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Etymon.Invariants" Version="0.1.0-preview.13" />
                    
Directory.Packages.props
<PackageReference Include="Etymon.Invariants" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Etymon.Invariants --version 0.1.0-preview.13
                    
#r "nuget: Etymon.Invariants, 0.1.0-preview.13"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Etymon.Invariants@0.1.0-preview.13
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Etymon.Invariants&version=0.1.0-preview.13&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Etymon.Invariants&version=0.1.0-preview.13&prerelease
                    
Install as a Cake Tool

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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