```text
╭────────────────────────────────────╮
│ ╭────╮ ╭────╮ ╭────╮ │
│ │ │ │ │ │ │ │
╰──────┴────┴──┴────┴──┴────┴────────╯
═══════════════════════════════════════
════════════ rectify ═══════════════
═══════════════════════════════════════
╭──────┬────┬──┬────┬──┬────┬────────╮
│ │ │ │ │ │ │ │
│ ╰────╯ ╰────╯ ╰────╯ │
╰────────────────────────────────────╯
```
# rectify
[](https://hex.pm/packages/rectify)
[](https://hexdocs.pm/rectify/)
Railway-oriented programming utilities for Gleam. A port of FsToolkit.ErrorHandling concepts, focusing on **accumulating validation errors** instead of failing fast.
```sh
gleam add rectify@1
```
## Quick Start
```gleam
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 error | Accumulates all errors |
| Single error in `Error(e)` | List of errors in `Invalid(List(e))` |
| Good for early exit | Good for form validation |
| Fail-fast | Report-all |
## Modules
### `rectify` - Validation
The core `Validation` type for accumulating errors.
```gleam
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.
```gleam
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<Option> Helpers
For working with `Result(Option(a), e)` - a common pattern for operations that can fail AND may not return a value.
```gleam
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
```gleam
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
```gleam
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<Option> Pipeline
```gleam
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
```gleam
// 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
```sh
gleam run # Run the project
gleam test # Run the tests
gleam docs # Generate documentation
```
## Acknowledgements
Inspired by the excellent [FsToolkit.ErrorHandling](https://github.com/demystifyfp/FsToolkit.ErrorHandling) library for F#.
## License
This project is licensed under the [MIT License](LICENSE).