Chest type that is like both Option and Result rolled into one.
- Rust 100%
|
|
||
|---|---|---|
| examples | ||
| src | ||
| tests | ||
| .gitignore | ||
| ARCHITECTURE.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| LICENSE-APACHE | ||
| LICENSE-MIT | ||
| PUBLISHING.md | ||
| README.md | ||
| TROUBLESHOOTING.md | ||
cada
The Problem
Multi-step operations in Rust turn into deeply nested match and if let
chains that are painful to read and painful to maintain:
fn process_order(db: &Database, order_id: i32) -> Option<String> {
if let Some(order) = db.find_order(order_id) {
if let Some(user) = db.find_user(order.user_id) {
if let Some(address) = db.find_address(user.address_id) {
if address.country == "US" {
if let Some(tax) = db.tax_rate(&address.state) {
Some(format!(
"Ship to {} at {}% tax",
user.name,
tax * 100.0
))
} else { None }
} else { None }
} else { None }
} else { None }
} else { None }
}
Every new step pushes you one level deeper. The happy path is buried under boilerplate, and adding error context means even more nesting.
The Solution
cada! flattens this into a clean, linear pipeline:
use cada::cada;
fn process_order(db: &Database, order_id: i32) -> Option<String> {
cada! {
order <- db.find_order(order_id);
user <- db.find_user(order.user_id);
address <- db.find_address(user.address_id);
guard address.country == "US";
tax <- db.tax_rate(&address.state);
yield format!("Ship to {} at {}% tax", user.name, tax * 100.0)
}
}
Each step either produces a value (<-) or filters (guard). If any step
fails, the whole thing short-circuits — no nesting, no else { None }.
It works with Option, Result, Vec, and more. No new types required.
Borrow checker friendly
Because the macro expands to nested for-loops (not closures), variables from
earlier bindings are directly in scope for later ones. No fighting with
lifetimes or move:
use cada::prelude::*;
use cada::cada;
fn get_name() -> Chest<String> { Full("Alice".to_string()) }
fn get_suffix(name: &str) -> Chest<String> { Full(format!("{}!", name)) }
fn validate(a: &str, b: &str) -> Chest<String> { Full(format!("{}-{}", a, b)) }
let result: Chest<String> = cada! {
a <- get_name();
b <- get_suffix(&a); // borrows a
c <- validate(&a, &b); // borrows both a and b
yield format!("{}{}: {}", a, b, c)
};
Try doing that with .and_then(|a| ...) chains — the borrow checker will
fight you every step of the way.
The Bonus: Chest<T>
When Option and Result aren't enough — you need to distinguish "not found"
from "something broke", attach error codes, or chain failure context —
Chest<T> fills the gap. And it works seamlessly inside cada!:
use cada::prelude::*;
use cada::cada;
fn find_user(id: i32) -> Chest<String> {
if id == 1 { Full("Alice".to_string()) } else { Chest::Empty }
}
let result: Chest<String> = cada! {
user <- find_user(99) ?~ "user not found", 404u16;
yield user
};
assert!(result.is_failure());
assert_eq!(result.failure_msg(), Some("user not found"));
assert_eq!(result.failure_param::<u16>(), Some(&404));
Chest<T> has three states: Full(T) (value present), Empty (absent, no
error), and Failure(FailureInfo) (absent, with error context). The ?~
syntax annotates why something failed, so you get rich diagnostics without
writing error-handling boilerplate.
When to Use What
| Situation | Use |
|---|---|
| Value may be absent, no error info needed | Option |
| Operation can fail, one error type | Result |
| Absent vs failed matters, or you need chained failures / typed params | Chest |
cada! vs .map/.and_then chains:
Use cada! when you have:
- More than 2 steps chained together
- Guards (filtering conditions) mixed in
- Multiple source types mixed (e.g.
Vec+Option) - Borrow-checker friction from closures borrowing earlier values
For simple 1-2 step chains, .map and .and_then are fine.
Quick Start
Add to your Cargo.toml:
[dependencies]
cada = "0.1"
use cada::prelude::*;
use cada::cada;
// Chest basics
let name: Chest<String> = Full("Alice".to_string());
let greeting = name.map(|n| format!("Hello, {}!", n));
assert_eq!(greeting, Full("Hello, Alice!".to_string()));
// For-comprehension with Chest
let result: Chest<String> = cada! {
user <- Full("Alice".to_string());
age <- Full(30);
guard age >= 18;
yield format!("{} (age {})", user, age)
};
assert_eq!(result, Full("Alice (age 30)".to_string()));
// For-comprehension with Vec (cross product)
let pairs: Vec<(i32, i32)> = cada! {
x <- vec![1, 2, 3];
y <- vec![10, 20];
yield (x, y)
};
assert_eq!(pairs, vec![(1, 10), (1, 20), (2, 10), (2, 20), (3, 10), (3, 20)]);
Runnable Examples
The examples/ directory contains working programs you can run immediately:
# Chest basics: Full, Empty, Failure, monadic operations
cargo run --example basic
# cada! macro: Option, Vec, Chest, guards, failure annotation
cargo run --example comprehension
# JSON querying with typed extraction (requires serde_json feature)
cargo run --example json_query --features serde_json
Chest<T> In Depth
Chest<T> is an enum with three variants:
enum Chest<T> {
Full(T), // Value present
Empty, // Value absent
Failure(FailureInfo), // Value absent, with error context
}
Why not just Option/Result?
Option has no error context. Result forces you to pick a single error type.
Chest gives you:
EmptyvsFailure-- distinguish "nothing here" from "something went wrong"- Failure chaining -- build a chain of failure messages with
compound_fail_msg - Typed failure parameters -- attach HTTP status codes, error codes, etc. via
with_param - Runtime type inspection --
chest_create!+as_a::<T>()for type-erased pipelines
Monadic operations
use cada::prelude::*;
let chest = Full(21);
// map, and_then (flatMap), filter
let doubled = chest.map(|x| x * 2); // Full(42)
let chained = Full(10).and_then(|x| Full(x + 5)); // Full(15)
let filtered = Full(42).filter(|x| *x > 0); // Full(42)
let gone = Full(-1).filter(|x| *x > 0); // Empty
// Extraction
let val = Full(42).open_or(0); // 42
let val = Chest::<i32>::Empty.open_or(99); // 99
let val = Full(42).open_or_else(|| expensive_default()); // 42
Failure annotation
use cada::prelude::*;
// Convert Empty to Failure with a message
let chest: Chest<i32> = Chest::Empty;
let annotated = chest.fail_msg("value required");
assert!(annotated.is_failure());
assert_eq!(annotated.failure_msg(), Some("value required"));
// Chain failures
let inner: Chest<i32> = Chest::Failure(FailureInfo::new("not found"));
let outer = inner.compound_fail_msg("lookup failed");
// failure_chain() returns the chain of FailureInfo refs
// Attach typed parameters (e.g. HTTP status codes)
let chest: Chest<i32> = Chest::Failure(FailureInfo::new("not found"));
let with_code = chest.with_param(404u16);
assert_eq!(with_code.failure_param::<u16>(), Some(&404));
Runtime type inspection
use cada::prelude::*;
use cada::chest_create;
// chest_create! wraps a value in Chest<TypedValue>, preserving runtime type info
let chest = chest_create!(42i32);
// Inspect and downcast
assert!(chest.is_a::<i32>());
assert!(!chest.is_a::<String>());
let as_int: Chest<i32> = chest.as_a::<i32>(); // Full(42)
let chest2 = chest_create!("hello".to_string());
let as_str: Chest<String> = chest2.as_a::<String>(); // Full("hello")
Conversions
use cada::prelude::*;
// From/Into Option
let chest: Chest<i32> = Some(42).into(); // Full(42)
let opt: Option<i32> = Full(42).into(); // Some(42)
// From/Into Result
let chest: Chest<i32> = Ok::<_, std::io::Error>(42).into(); // Full(42)
let result: Result<i32, FailureInfo> = Full(42).into(); // Ok(42)
// To Vec (0 or 1 elements)
let items = Full(42).to_vec(); // vec![42]
// Catch panics
let safe = tryo(|| might_panic()); // Full(result) or Failure
let safe = tryo_result(fallible_call()); // Result -> Chest
cada! Macro
The cada! macro provides Haskell-style for-comprehensions. It desugars
to nested for-loops with no closures, so the borrow checker sees flat scoping and
variables from earlier bindings can be freely referenced later.
Syntax
cada! {
pattern <- source; // bind
pattern <- source ?~ "error message"; // bind with failure annotation
pattern <- source ?~ "error msg", param; // bind with message + typed param
guard condition; // filter
guard condition, ?~ "error message"; // filter with failure annotation
guard condition, ?~ "error msg", param; // filter with message + typed param
let pattern = expression; // local binding
yield expression // final value (no trailing semicolon)
}
The first binding's type determines the output type.
What Happens When Things Fail
Understanding short-circuit behavior is key to using cada! effectively.
Empty propagation — if any binding produces no value, the rest is skipped:
use cada::cada;
let result: Option<i32> = cada! {
x <- Some(10);
y <- None::<i32>; // short-circuits here
yield x + y // never reached
};
assert_eq!(result, None);
Failure annotation with ?~ — turns an empty/failed binding into a
Failure with a message (only meaningful for Chest and Result output):
use cada::prelude::*;
use cada::cada;
let result: Chest<i32> = cada! {
x <- Full(10);
y <- Chest::<i32>::Empty ?~ "y was missing", 400u16;
yield x + y
};
assert!(result.is_failure());
assert_eq!(result.failure_msg(), Some("y was missing"));
assert_eq!(result.failure_param::<u16>(), Some(&400));
Guard failure — if a guard is false, the current iteration is skipped. For monadic types this means empty output; for collections it filters that item:
use cada::cada;
// Monadic: guard failure → empty result
let result: Option<i32> = cada! {
x <- Some(10);
guard x > 100; // false → short-circuit
yield x
};
assert_eq!(result, None);
// Collection: guard failure → item filtered out
let result: Vec<i32> = cada! {
x <- vec![1, 2, 3, 4, 5];
guard x % 2 == 0; // keeps only even numbers
yield x
};
assert_eq!(result, vec![2, 4]);
With Option
use cada::cada;
let result: Option<String> = cada! {
name <- Some("Alice".to_string());
len <- Some(name.len()); // borrows `name` freely
yield format!("{}: {} chars", name, len)
};
assert_eq!(result, Some("Alice: 5 chars".to_string()));
With Result
use cada::cada;
use cada::CadaError;
let result: Result<i32, CadaError> = cada! {
x <- Ok::<_, std::io::Error>(10);
y <- Ok::<_, std::io::Error>(20);
yield x + y
};
assert_eq!(result.unwrap(), 30);
With collections
use cada::cada;
// Filter
let evens: Vec<i32> = cada! {
x <- vec![1, 2, 3, 4, 5];
guard x % 2 == 0;
yield x
};
assert_eq!(evens, vec![2, 4]);
// Nested iteration (cross product)
let product: Vec<(i32, &str)> = cada! {
x <- vec![1, 2];
s <- vec!["a", "b"];
yield (x, s)
};
assert_eq!(product, vec![(1, "a"), (1, "b"), (2, "a"), (2, "b")]);
With Chest (failure annotation)
use cada::prelude::*;
use cada::cada;
fn find_user(id: i32) -> Chest<String> {
if id == 1 { Full("Alice".to_string()) } else { Chest::Empty }
}
let result: Chest<String> = cada! {
user <- find_user(99) ?~ "user not found", 404u16;
yield user
};
assert!(result.is_failure());
assert_eq!(result.failure_msg(), Some("user not found"));
assert_eq!(result.failure_param::<u16>(), Some(&404));
Mixing types
Different source types can be mixed freely. The first source determines the output type:
use cada::prelude::*;
use cada::cada;
fn validate(x: i32) -> Option<i32> {
if x > 0 { Some(x) } else { None }
}
// Vec source with Option secondary -- filters out None results
let result: Vec<(i32, i32)> = cada! {
x <- vec![1, -2, 3, -4];
y <- validate(x);
yield (x, y)
};
assert_eq!(result, vec![(1, 1), (3, 3)]);
Supported Types
| Type | As source (<-) |
As output (first source) |
|---|---|---|
Chest<T> |
Yes | Yes |
Option<T> |
Yes | Yes |
Result<T, E> |
Yes | Yes |
Vec<T> |
Yes | Yes |
VecDeque<T> |
Yes | Yes |
LinkedList<T> |
Yes | Yes |
HashSet<T> |
Yes | Yes |
BTreeSet<T> |
Yes | Yes |
BinaryHeap<T> |
Yes | Yes |
HashMap<K, V> |
Yes | Yes |
BTreeMap<K, V> |
Yes | Yes |
JsonValue |
Yes | Yes (→ Vec<Y>) |
JsonValue requires the serde_json feature.
Features
serde-- enablesSerialize/DeserializeforChest<T>serde_json-- enables LINQ-style JSON querying viaJsonValuewith typed extraction markers incada!bindings
[dependencies]
cada = { version = "0.1", features = ["serde_json"] }
JSON Querying (serde_json feature)
The serde_json feature adds JsonValue, a wrapper around serde_json::Value
that supports path navigation with / and integrates with cada! for
typed extraction.
Path Navigation
use cada::prelude::*;
let v = JsonValue::new(serde_json::json!({"a": {"b": 42}}));
let inner = v / "a" / "b";
assert_eq!(inner.into_inner(), serde_json::json!(42));
// / distributes across arrays
let v = JsonValue::new(serde_json::json!([{"a": 1}, {"a": 2}, "skip"]));
let result = v / "a";
assert_eq!(result.into_inner(), serde_json::json!([1, 2]));
Typed Extraction in Comprehensions
Use name: Type <- expr syntax to perform type-filtered extraction:
use cada::prelude::*;
use cada::cada;
let data = serde_json::json!({
"users": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]
});
let names: Vec<String> = cada! {
user: Array <- JsonValue::new(data) / "users";
name: String <- user / "name";
yield name
};
assert_eq!(names, vec!["Alice".to_string(), "Bob".to_string()]);
Available type markers:
| Marker | Extracts | Rust type |
|---|---|---|
Array |
JSON array elements | JsonValue |
String |
JSON string | String |
Number |
JSON number | f64 |
Object |
JSON object | JsonValue |
Bool |
JSON boolean | bool |
Null |
JSON null | () |
I64 |
JSON integer | i64 |
U64 |
JSON unsigned integer | u64 |
Isize |
JSON integer | isize |
Usize |
JSON unsigned integer | usize |
F64 |
JSON number (same as Number) |
f64 |
Non-matching values are silently skipped (zero iterations).
Extending
Implement CadaSource and CadaFirst for your own types to make them
work inside cada!. See src/impls/ for examples.
Further Reading
- ARCHITECTURE.md — trait hierarchy, macro dispatch state machine, desugaring examples, glossary
- TROUBLESHOOTING.md — common compiler errors,
cargo expand, the.clone()pitfall,?~explained - CONTRIBUTING.md — development setup, testing, guidelines
- CHANGELOG.md — release history
- PUBLISHING.md — how to publish to crates.io
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.