Validus Validation Library for F#

repository·main·Indexed 41 days ago

https://github.com/pimbrouwers/validus

An extensible validation library for F# designed for primitive checks and complex domain object construction. It features an applicative computation expression `validate { ... }` for error accumulation, built-in validators in the `Check` module for numeric, temporal, and string types, and `ValidatorGroup` for chaining rules. Validus also provides a set of custom operators in `Validus.Operators` for composing and transforming validators to create type-safe Value Objects.

Tokens
3.2K
Snippets
12
Records
12
Agent score
1%

What's inside Validus

  1. Compose and chain validators using custom operators

    main

    Validus provides a set of custom operators in Validus.Operators to compose, chain, and transform validators. This allows you to build complex validation logic from simple building blocks.

    Core Operators Reference

    OperatorDescription
    <+>Compose two validators of equal types
    *|*Map the Ok result of a validator (high precedence, for use with choice <|> )
    *|Set the Ok result of a validator to a fixed value (high precedence, for use with choice <|> )
    >>|Map the Ok result of a validator (low precedence, for chained validation)
    >|Set the Ok result of a validator to a fixed value (low precedence, for chained validation)
    >>=Bind the Ok result of a validator with a function returning a Result
    <<=Reverse-bind the Ok result of a validator with a function returning a Result
    >>%Set the Ok result of a validator to a fixed Result value
    <|>Choice: if the right-hand side validates Ok, pick that result; otherwise, continue with the next validator
    >=>Kleisli-bind two validators (can change the result type)
    <=<Reverse Kleisli-bind (right-hand side evaluated first; can change result type)
    .>>Compose two validators, but keep the result of the left-hand side (ignores right-hand side unless it errors)
    >>.Compose two validators, but keep the result of the right-hand side (ignores left-hand side unless it errors)
    .>>.Compose two validators and keep the results of both sides as a tuple
    open Validus
    open Validus.Operators
    
    // Example: Chaining a pattern check, a conversion, and a range check
    let ageValidator =
        Check.String.pattern @"\d+" *|| Int32.Parse // Match pattern, then convert to Int32
        >=> Check.Int.between 0 120                 // Then check range
  2. Validating Complex Types with Applicative Computation Expressions

    main

    Validus provides an applicative computation expression via the validate { ... } block. This allows you to run multiple validators and, instead of failing at the first error, accumulate all validation errors into a single result.

    • Use let! field = validator "label" value to validate a field and bind the result.
    • Use and! field = validator "label" value to validate an additional field while accumulating errors.
    • The return statement is only executed if all validators within the block return Ok.
    validate {
      let! first = firstNameValidator "First name" dto.FirstName
      and! last = nameValidator "Last name" dto.LastName
      and! age = Check.optional (Check.Int.between 1 120) "Age" dto.Age
    
      return { Name = { First = first; Last = last }; Age = age }
    }
  3. Creating Value Objects with Validators

    main

    To avoid 'primitive obsession', you can create Value Objects (constrained primitives) by wrapping a primitive in a private type and providing a Validator that handles the transformation from the primitive to the type.

    Example for an E164 Phone Number:

    type E164 = 
        private { E164 : string }
        static member Of : Validator<string, E164> = fun field input ->
            let e164Regex = @"^\+[1-9]\d{1,14}$"
            let message = sprintf "%s must be a valid E164 telephone number"
            input
            |> Check.WithMessage.String.pattern e164Regex message field
            |> Result.map (fun v -> { E164 = v })
    type E164 = 
        private { E164 : string }
        override x.ToString() = x.E164
    
        static member Of : Validator<string, E164> = fun field input ->
            let e164Regex = @"^\+[1-9]\d{1,14}$"
            let message = sprintf "%s must be a valid E164 telephone number"
    
            input
            |> Check.WithMessage.String.pattern e164Regex message field
            |> Result.map (fun v -> { E164 = v })
  4. Quick Start with Validus

    main

    Validus is an extensible validation library for F#. A common pattern is to receive data from an untrusted source (like a DTO), apply validators, and produce a domain object if successful. You can use Check for built-in primitive validators, Validator.create for custom logic, and the validate { ... } applicative computation expression to accumulate errors across multiple fields.

    open Validus
    
    // Example of validating a DTO into a domain object
    module Person =
        let ofDto (dto : PersonDto) =
            let nameValidator = Check.String.betweenLen 3 64
            let ageValidator = Check.optional (Check.Int.between 1 100)
    
            validate {
              let! first = nameValidator "First name" dto.FirstName
              let! last = nameValidator "Last name" dto.LastName
              let! age = ageValidator "Age" dto.Age
    
              return { Name = { First = first; Last = last }; Age = age }
            }
  5. Combining Validators with ValidatorGroup

    main

    Use ValidatorGroup to chain multiple validation rules together. You can compose rules using:

    • .And(validator): All validators must pass.
    • .Then(validator): The second validator only executes if the previous one succeeded (waterfall pattern).
    • .Build(): Finalizes the group into a single validator.

    You can also apply a validator to a collection by mapping it over the list/sequence.

    let emailValidator =
        ValidatorGroup(Check.String.betweenLen 8 512)
            .And(emailPatternValidator)
            .Then(mailAddressValidator) // only executes when prior two steps are `Ok`
            .Build()
    
    "fake@test" |> emailValidator "Login email"
  6. Validate numeric values with comparison validators

    main

    Use Check.Int (and related numeric modules) to validate that numbers meet specific comparison criteria. You can use standard error messages or provide a custom message via Check.WithMessage.

    Supported types for comparison include: int16, int, int64, decimal, float, DateTime, DateTimeOffset, and TimeSpan.

    open Validus
    
    // Standard error message
    let greaterThanOrEqualTo100 =
      Check.Int.greaterThanOrEqualTo 100 "fieldName"
    
    // Custom error message
    let msg = sprintf "%s must be greater than or equal to 100"
    let greaterThanOrEqualTo100Custom =
      Check.WithMessage.Int.greaterThanOrEqualTo 100 msg "fieldName"
    
    // Available comparison methods:
    // - greaterThanOrEqualTo
    // - lessThan
    // - lessThanOrEqualTo
  7. Creating a Custom Validator

    main

    You can create custom validation logic using Validator.create. This function takes an error message and a rule function (which returns a bool).

    let myValidator = 
        let rule v = v = "expected_value"
        let msg = "Value must be expected_value"
        Validator.create msg rule
    
    // Usage
    "expected_value" |> myValidator "My Field"
    let fooValidator =
        let fooRule v = v = "foo"
        let fooMessage = sprintf "%s must be a string that matches 'foo'"
        Validator.create fooMessage fooRule
    
    "bar" |> fooValidator "Test string"
  8. Validate collection and string lengths

    main

    Use Check.String (or similar modules for array, list, or seq) to validate the length of collections or strings.

    Available length-based validators:

    • betweenLen min max: Checks if length is within range.
    • equalsLen len: Checks if length is exactly equal to len.
    • greaterThanLen len: Checks if length is greater than len.
    • greaterThanOrEqualToLen len: Checks if length is $\ge$ len.
    • lessThanLen len: Checks if length is less than len.
    • lessThanOrEqualToLen len: Checks if length is $\le$ len.
    • empty: Checks if the collection/string is empty.
    • notEmpty: Checks if the collection/string is not empty.
    open Validus
    
    // Check if string is between 1 and 100 chars
    let between1and100Chars = Check.String.betweenLen 1 100 "fieldName"
    
    // Check if string is not empty
    let stringIsNotEmpty = Check.String.notEmpty "fieldName"
  9. Validate collections with predicates

    main

    Use Check.List.exists to validate that a collection contains at least one element that satisfies a given predicate function.

    open Validus
    
    // Checks if the list contains the value 1
    let collectionContains = Check.List.exists (fun x -> x = 1) "fieldName"
  10. Validate strings with regex patterns

    main

    Use Check.String.pattern to ensure a string matches a specific regular expression. You can provide a custom error message using Check.WithMessage.String.pattern.

    open Validus
    
    // Standard error message
    let stringIsChars = Check.String.pattern "[a-z]+" "fieldName"
    
    // Custom error message
    let msg = "must follow the pattern [a-z]"
    let stringIsCharsCustom = Check.WithMessage.String.pattern "[a-z]" msg "fieldName"
  11. Built-in Validators: Equality and Inequality

    main

    The Check module provides validators for equality and inequality. These apply to string, int16, int, int64, decimal, float, DateTime, DateTimeOffset, TimeSpan, 'a array, 'a list, 'a seq.

    Use Check.[Type].equals or Check.[Type].notEquals for default English error messages, or Check.WithMessage.[Type].equals / Check.WithMessage.[Type].notEquals to provide a custom message.

    Note: These validators require a field name string as an argument.

    // Standard error message
    let equalsFoo = Check.String.equals "foo" "fieldName"
    
    // Custom error message
    let equalsFooCustom = Check.WithMessage.String.equals "foo" "must equal foo" "fieldName"
    
    // Inequality
    let notEqualsFoo = Check.String.notEquals "foo" "fieldName"
  12. Built-in Validators: Range and Comparison

    main

    The Check module provides validators for numeric and temporal ranges. These apply to int16, int, int64, decimal, float, DateTime, DateTimeOffset, TimeSpan.

    • Check.[Type].between min max "fieldName": Checks if a value is within an inclusive range.
    • Check.[Type].greaterThan threshold "fieldName": Checks if a value is strictly greater than the threshold.

    Custom messages can be provided via Check.WithMessage.[Type].between or Check.WithMessage.[Type].greaterThan.

    // Between
    let between1and100 = Check.Int.between 1 100 "fieldName"
    
    // Greater Than
    let greaterThan100 = Check.Int.greaterThan 100 "fieldName"