Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result

ConstraintDSL

ConstraintDSL exposes common constructors without the Constraint. prefix:

Verified
open Reified

module SignupRules =
    open Reified.ConstraintDSL

    let name : Constraint<string> =
        Constraint.all [ present; minLength 2; maxLength 80 ]

    let emailAddress : Constraint<string> =
        Constraint.all [ present; email; maxLength 254 ]

    let age : Constraint<int> =
        atLeast 13

    let handle : Constraint<string> =
        Constraint.all [ present; alphanumeric; noneOf [ "admin"; "root" ] ]

Each DSL name returns the same Constraint<'value> as its qualified counterpart.

Type-directed names

present, blank, optional, and the size family pick their shape from the type they are used at, so most of the time they just work:

Isolated
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL

type Item = { Sku: string }

let name : Constraint<string> = Constraint.all [ present; maxLength 80 ]
let tags : Constraint<Item list> = Constraint.all [ atLeastOne; Constraint.distinct ]

Schema.text |> Schema.constrain present

Here the surrounding type selects the shape. A standalone binding needs a type annotation because dispatch runs on the return type:

Isolated
open Reified
open Reified.ConstraintDSL

type Item = { Sku: string }

let requiredName : Constraint<string> = present
let selectedPlan : Constraint<string option> = present
let requiredItems : Constraint<Item list> = minLength 1

This selects a constraint; it does not parse, convert, or refine anything.

The catalogue

Family Names
Presence present, blank, optional
Size minLength, maxLength, lengthBetween, single, atLeastOne, atMostOne, moreThanOne
Comparison equalTo, notEqualTo, greaterThan, lessThan, atLeast, atMost
Sign positive, nonNegative, negative, nonPositive
Membership oneOf, noneOf, notContains
Format email, trimmed, numeric, alphanumeric, pattern
Number multipleOf, finite, finite32
Opaque notWith, custom, customLocalized, customWith, contramap
Other describe, orError, mapError

The sign and size names are spellings, not new primitives: positive is greaterThan 0 at the value's own numeric type, and atLeastOne is minLength 1. Each builds the same atom its general form builds, so inspection, export, and generation treat them identically.

Text sizes count code points

The size family measures text in Unicode code points, not UTF-16 code units. An emoji outside the Basic Multilingual Plane is one character, so Constraint.length 1 accepts "\U0001F600", where String.Length would report 2.

Verified
let emoji : Constraint<string> = Constraint.length 1
Constraint.satisfies emoji "\U0001F600"   // true

Counting code points gives the same size in a browser and on a server. Collections count elements, and maps count entries; the atom is shape-neutral and an interpreter combines it with the surrounding shape to reach maxLength, maxItems, or maxProperties.

Blankness is defined the same way on both runtimes: .NET's whitespace set plus U+FEFF. Adding U+FEFF makes a JSON Schema validator's whitespace a strict subset of Reified's, which is what lets present and trimmed export a sound \S pattern instead of staying runtime-only.

Names left off, and why

Some constructors are deliberately absent because they shadow names the same validation code is likely to need:

Left off Reason Reach for
all, any, contains, distinct, length, between shadow core F# operations Constraint.all, Constraint.contains, …
check shadows Schema.check Constraint.check
test, guard execution stays qualified, consistent with check Constraint.satisfies, Constraint.guard

The omissions are driven by collision alone, which is why notContains is exported although contains is not, no core operation is named notContains. All constraint execution, Constraint.satisfies, Constraint.check, Constraint.guard, and Constraint.inspect, stays qualified, so ConstraintDSL declares constraints and Constraint.* executes and inspects them.

Result adapters

orError and mapError are small structural adapters matching the corresponding Result operations. They live here because Reified.Constraint does not depend on Reified.Result, so a constraint pipeline can retain its input and finish with the application's own error type without adding a package reference:

Isolated
open Reified
open Reified.ConstraintDSL

type SignupError =
    | NameRequired
    | InvalidQuantity of Violation

let requiredName (value: string) =
    value |> Constraint.guard present |> orError NameRequired

let quantity value =
    value |> Constraint.guard (atLeast 1) |> mapError InvalidQuantity