╭────────────────────────────────────╮
    │      ╭────╮  ╭────╮  ╭────╮        │
    │      │    │  │    │  │    │        │
    ╰──────┴────┴──┴────┴──┴────┴────────╯
    
    ═══════════════════════════════════════
    
    ════════════ rectify ═══════════════
    
    ═══════════════════════════════════════
    
    ╭──────┬────┬──┬────┬──┬────┬────────╮
    │      │    │  │    │  │    │        │
    │      ╰────╯  ╰────╯  ╰────╯        │
    ╰────────────────────────────────────╯

rectify

Package Version Hex Docs

Railway-oriented programming utilities for Gleam. A port of FsToolkit.ErrorHandling concepts, focusing on accumulating validation errors instead of failing fast.

gleam add rectify@1

Quick Start

import rectify

// Individual validators return Validation
type User {
  User(name: String, email: String, age: Int)
}

fn validate_name(name: String) -> rectify.Validation(String, String) {
  case string.trim(name) {
    "" -> rectify.invalid("Name is required")
    n -> rectify.valid(n)
  }
}

fn validate_email(email: String) -> rectify.Validation(String, String) {
  case string.contains(email, "@") {
    True -> rectify.valid(email)
    False -> rectify.invalid("Invalid email address")
  }
}

fn validate_age(age: Int) -> rectify.Validation(Int, String) {
  case age >= 0 && age <= 150 {
    True -> rectify.valid(age)
    False -> rectify.invalid("Age must be between 0 and 150")
  }
}

// Collect ALL errors, not just the first one
let result = rectify.map3(
  validate_name(""),
  validate_email("not-an-email"),
  validate_age(200),
  User,
)

// result = Invalid(["Name is required", "Invalid email address", "Age must be between 0 and 150"])
// vs Result which would only give you the first error

Why Validation instead of Result?

Result(a, e)Validation(a, e)
Stops at first errorAccumulates all errors
Single error in Error(e)List of errors in Invalid(List(e))
Good for early exitGood for form validation
Fail-fastReport-all

Modules

rectify - Validation

The core Validation type for accumulating errors.

import rectify

// Constructors
rectify.valid(42)                    // Valid(42)
rectify.invalid("oops")              // Invalid(["oops"])
rectify.invalid_many(["a", "b"])     // Invalid(["a", "b"])

// Mapping - errors accumulate!
rectify.map2(valid(2), valid(3), fn(a, b) { a + b })        // Valid(5)
rectify.map2(invalid("a"), invalid("b"), fn(a, b) { a + b }) // Invalid(["a", "b"])
rectify.map3(v1, v2, v3, User)  // Up to map5 available

// Conversions
rectify.to_result(valid(42))           // Ok(42)
rectify.of_result(Error("e"))          // Invalid(["e"])

rectify/option - Option Utilities

Additional helpers for Gleam’s Option type.

import rectify/option as ropt

// Predicates
ropt.is_some(Some(42))    // True
ropt.is_none(None)        // True

// Defaults
ropt.default_to(Some(42), 0)     // 42
ropt.default_to(None, 0)         // 0
ropt.default_with(None, fn() { expensive() })

// Combining
ropt.map2(Some(2), Some(3), fn(a, b) { a + b })  // Some(5)
ropt.map3(opt1, opt2, opt3, fn(a, b, c) { a + b + c })

// Collections
ropt.choose_somes([Some(1), None, Some(2)])  // [1, 2]
ropt.first_some([None, Some(2), Some(3)])    // Some(2)

// Conversions
ropt.to_result(Some(42), "not found")  // Ok(42)
ropt.of_result(Ok(42))                 // Some(42)

rectify/result_option - Result

For working with Result(Option(a), e) - a common pattern for operations that can fail AND may not return a value.

import rectify/result_option as ro

// Constructors
ro.some(42)        // Ok(Some(42))
ro.none()          // Ok(None)
ro.error("e")      // Error("e")

// Mapping
ro.map(Ok(Some(5)), fn(n) { n * 2 })     // Ok(Some(10))
ro.bind(Ok(Some(5)), fn(n) { ro.some(n * 2) })

// Predicates
ro.is_some(Ok(Some(42)))     // True
ro.is_none(Ok(None))         // True
ro.is_error(Error("e"))      // True

// Conversions
ro.to_option(Ok(Some(42)))        // Some(42)
ro.of_option(Some(42))            // Ok(Some(42))
ro.of_result(Ok(42))              // Ok(Some(42))
ro.to_result(Ok(Some(42)), 0)     // Ok(42)
ro.to_result(Ok(None), 0)         // Ok(0) - default value

Common Patterns

Form Validation

import rectify

type Form {
  Form(name: String, email: String, age: Int)
}

fn validate_form(name: String, email: String, age: Int) {
  rectify.map3(
    validate_name(name),
    validate_email(email),
    validate_age(age),
    Form,
  )
  |> rectify.to_result  // Convert to Result for standard error handling
}

// Usage
case validate_form("", "bad-email", -5) {
  Ok(form) -> create_user(form)
  Error(errors) -> show_validation_errors(errors)
}

Option Chaining

import rectify/option as ropt
import gleam/option.{Some, None}

// Combine multiple optional lookups
let result = ropt.map3(
  dict.get(users, "alice"),     // Some(user1)
  dict.get(users, "bob"),       // None
  dict.get(users, "charlie"),   // Some(user3)
  fn(a, b, c) { [a, b, c] }
)
// result = None (because bob was None)

// Find first available fallback
ropt.first_some([
  dict.get(config, "primary_url"),
  dict.get(config, "fallback_url"),
  Some("default"),
])

Result

import rectify/result_option as ro

// Database lookup that can fail (Error) or not find result (Ok(None))
fn find_user(id: Int) -> Result(Option(User), DbError) {
  // ... database code
}

// Transform through pipeline
find_user(42)
|> ro.map(fn(user) { user.name })
|> ro.bind(fn(name) { 
  case name {
    "" -> ro.none()
    _ -> ro.some(name)
  }
})
|> ro.to_result("unknown")  // Get Result(String, String)

Comparison with Gleam’s Result

// Result - fail fast
let r1 = Ok(1)
let r2 = Error("error 1")
let r3 = Error("error 2")

use a <- result.try(r1)
use b <- result.try(r2)  // Stops here, never sees "error 2"
use c <- result.try(r3)
Ok(a + b + c)
// Error("error 1")

// Validation - collect all
let v1 = rectify.valid(1)
let v2 = rectify.invalid("error 1")
let v3 = rectify.invalid("error 2")

rectify.map3(v1, v2, v3, fn(a, b, c) { a + b + c })
// Invalid(["error 1", "error 2"])

Development

gleam run   # Run the project
gleam test  # Run the tests
gleam docs   # Generate documentation

Acknowledgements

Inspired by the excellent FsToolkit.ErrorHandling library for F#.

License

This project is licensed under the MIT License.

Search Document