stillwater 0.10.0

Pragmatic effect composition and validation for Rust - pure core, imperative shell
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# Stillwater

[![CI](https://github.com/iepathos/stillwater/actions/workflows/ci.yml/badge.svg)](https://github.com/iepathos/stillwater/actions/workflows/ci.yml)
[![Coverage](https://github.com/iepathos/stillwater/actions/workflows/coverage.yml/badge.svg)](https://github.com/iepathos/stillwater/actions/workflows/coverage.yml)
[![Security](https://github.com/iepathos/stillwater/actions/workflows/security.yml/badge.svg)](https://github.com/iepathos/stillwater/actions/workflows/security.yml)
[![Crates.io](https://img.shields.io/crates/v/stillwater)](https://crates.io/crates/stillwater)
[![License](https://img.shields.io/badge/license-MIT)](LICENSE)

> *"Still waters run pure"*

A Rust library for pragmatic effect composition and validation, emphasizing the **pure core, imperative shell** pattern.

## Philosophy

**Stillwater** embodies a simple idea:
- **Still** = Pure functions (unchanging, referentially transparent)
- **Water** = Effects (flowing, performing I/O)

Keep your business logic pure and calm like still water. Let effects flow at the boundaries.

## What Problems Does It Solve?

### 1. "I want ALL validation errors, not just the first one"

```rust
use stillwater::Validation;

// Standard Result: stops at first error โŒ
let email = validate_email(input)?;  // Stops here
let age = validate_age(input)?;      // Never reached if email fails

// Stillwater: accumulates all errors โœ“
let user = Validation::all((
    validate_email(input),
    validate_age(input),
    validate_name(input),
))?;
// Returns: Err(vec![EmailError, AgeError, NameError])
```

### 2. "How do I validate that all items have the same type before combining?"

```rust
use stillwater::validation::homogeneous::validate_homogeneous;
use std::mem::discriminant;

#[derive(Clone, Debug, PartialEq)]
enum Aggregate {
    Sum(f64),     // Can combine Sum + Sum
    Count(usize), // Can combine Count + Count
    // But Sum + Count is a type error!
}

// Without validation: runtime panic ๐Ÿ’ฅ
let mixed = vec![Aggregate::Count(5), Aggregate::Sum(10.0)];
// items.into_iter().reduce(|a, b| a.combine(b))  // PANIC!

// With validation: type-safe error accumulation โœ“
let result = validate_homogeneous(
    mixed,
    |a| discriminant(a),
    |idx, _, _| format!("Type mismatch at index {}", idx),
);

match result {
    Validation::Success(items) => {
        // Safe to combine - all same type!
        let total = items.into_iter().reduce(|a, b| a.combine(b));
    }
    Validation::Failure(errors) => {
        // All mismatches reported: ["Type mismatch at index 1"]
    }
}
```

### 3. "How do I test code with database calls?"

```rust
use stillwater::Effect;

// Pure business logic (no DB, easy to test)
fn calculate_discount(customer: &Customer, total: Money) -> Money {
    match customer.tier {
        Tier::Gold => total * 0.15,
        _ => total * 0.05,
    }
}

// Effects at boundaries (mockable)
fn process_order(id: OrderId) -> Effect<Invoice, Error, AppEnv> {
    IO::query(|db| db.fetch_order(id))        // I/O
        .and_then(|order| {
            let total = calculate_total(&order);  // Pure!
            IO::query(|db| db.fetch_customer(order.customer_id))
                .map(move |customer| (order, customer, total))
        })
        .map(|(order, customer, total)| {
            let discount = calculate_discount(&customer, total);  // Pure!
            create_invoice(order.id, total - discount)            // Pure!
        })
        .and_then(|invoice| IO::execute(|db| db.save(invoice))) // I/O
}

// Test with mock environment
#[test]
fn test_with_mock_db() {
    let env = MockEnv::new();
    let result = process_order(id).run(&env)?;
    assert_eq!(result.total, expected);
}
```

### 4. "My errors lose context as they bubble up"

```rust
use stillwater::Effect;

fetch_user(id)
    .context("Loading user profile")
    .and_then(|user| process_data(user))
    .context("Processing user data")
    .run(&env)?;

// Error output:
// Error: UserNotFound(12345)
//   -> Loading user profile
//   -> Processing user data
```

### 5. "I need clean dependency injection without passing parameters everywhere"

```rust
use stillwater::Effect;

#[derive(Clone)]
struct Config {
    timeout: u64,
    retries: u32,
}

// Functions don't need explicit config parameters
fn fetch_data() -> Effect<String, String, Config> {
    // Ask for config when needed
    Effect::asks(|cfg: &Config| {
        format!("Fetching with timeout={}", cfg.timeout)
    })
}

fn fetch_with_extended_timeout() -> Effect<String, String, Config> {
    // Temporarily modify environment for specific operations
    Effect::local(
        |cfg: &Config| Config { timeout: cfg.timeout * 2, ..*cfg },
        fetch_data()
    )
}

# tokio_test::block_on(async {
let config = Config { timeout: 30, retries: 3 };
let result = fetch_with_extended_timeout().run(&config).await?;
// Uses timeout=60 without changing the original config
# });
```

### 6. "Retry logic is scattered and hard to test"

```rust
use stillwater::{Effect, RetryPolicy};
use std::time::Duration;

// Traditional approach: retry logic mixed with business logic โŒ
// - Can't test retry behavior without calling the API
// - Can't reuse the same policy across different operations
// - Hard to tune or modify without code changes

// Stillwater: Policy as Data โœ“
// Define retry policies as pure, composable, testable values
let api_policy = RetryPolicy::exponential(Duration::from_millis(100))
    .with_max_retries(5)
    .with_max_delay(Duration::from_secs(2))
    .with_jitter(0.25);

// Test the policy without any I/O
assert_eq!(api_policy.delay_for_attempt(0), Some(Duration::from_millis(100)));
assert_eq!(api_policy.delay_for_attempt(1), Some(Duration::from_millis(200)));
assert_eq!(api_policy.delay_for_attempt(2), Some(Duration::from_millis(400)));

// Reuse the same policy across different effects
Effect::retry(|| fetch_user(id), api_policy.clone());
Effect::retry(|| save_order(order), api_policy.clone());

// Conditional retry: only retry transient failures
Effect::retry_if(
    || api_call(),
    api_policy,
    |err| matches!(err, ApiError::Timeout | ApiError::ServerError(_))
    // Don't retry client errors (4xx) - they won't succeed on retry
);

// Observability: hook into retry events for logging/metrics
Effect::retry_with_hooks(
    || api_call(),
    policy,
    |event| log::warn!(
        "Attempt {} failed: {}, retrying in {:?}",
        event.attempt, event.error, event.next_delay
    )
);
```

**Why "Policy as Data" matters:**
- **Testable**: Test retry timing without mocks or network calls
- **Reusable**: One policy definition, many use sites
- **Composable**: Builder pattern for flexible configuration
- **Inspectable**: Query policy parameters before execution
- **Safe**: Enforces bounds (max_retries OR max_delay required)

## Core Features

- **`Validation<T, E>`** - Accumulate all errors instead of short-circuiting
- **`NonEmptyVec<T>`** - Type-safe non-empty collections with guaranteed head element
- **`Effect<T, E, Env>`** - Separate pure logic from I/O effects
- **Parallel effect execution** - Run independent effects concurrently with `par_all()`, `race()`, and `par_all_limit()`
- **Retry and resilience** - Policy-as-data approach: define retry policies as pure, testable values with exponential, linear, constant, and Fibonacci backoff strategies. Includes jitter (proportional, full, decorrelated), conditional retry with predicates, retry hooks for observability, and timeout support
- **Traverse and sequence** - Transform collections with `traverse()` and `sequence()` for both validations and effects
  - Validate entire collections with error accumulation
  - Process collections with effects using fail-fast semantics
- **Reader pattern helpers** - Clean dependency injection with `ask()`, `asks()`, and `local()`
- **`Semigroup` trait** - Associative combination of values
  - Extended implementations for `HashMap`, `HashSet`, `BTreeMap`, `BTreeSet`, `Option`
  - Wrapper types: `First`, `Last`, `Intersection` for alternative semantics
- **`Monoid` trait** - Identity elements for powerful composition patterns
- **Testing utilities** - Ergonomic test helpers
  - `MockEnv` builder for composing test environments
  - Assertion macros: `assert_success!`, `assert_failure!`, `assert_validation_errors!`
  - `TestEffect` wrapper for deterministic effect testing
  - Optional `proptest` feature for property-based testing
- **Context chaining** - Never lose error context
- **Tracing integration** - Instrument effects with semantic spans using the standard `tracing` crate
- **Zero-cost abstractions** - Compiles to same code as hand-written
- **Works with `?` operator** - Integrates with Rust idioms
- **No heavy macros** - Clear types, obvious behavior

## Quick Start

```rust
use stillwater::{Validation, Effect, IO};

// 1. Validation with error accumulation
fn validate_user(input: UserInput) -> Validation<User, Vec<Error>> {
    Validation::all((
        validate_email(&input.email),
        validate_age(input.age),
        validate_name(&input.name),
    ))
    .map(|(email, age, name)| User { email, age, name })
}

// 2. Effect composition
fn create_user(input: UserInput) -> Effect<User, AppError, AppEnv> {
    // Validate (pure, accumulates errors)
    Effect::from_validation(validate_user(input))
        // Check if exists (I/O)
        .and_then(|user| {
            IO::query(|db| db.find_by_email(&user.email))
                .and_then(|existing| {
                    if existing.is_some() {
                        Effect::fail(AppError::EmailExists)
                    } else {
                        Effect::pure(user)
                    }
                })
        })
        // Save user (I/O)
        .and_then(|user| {
            IO::execute(|db| db.insert_user(&user))
                .map(|_| user)
        })
        .context("Creating new user")
}

// 3. Run at application boundary
let env = AppEnv { db, cache, logger };
let result = create_user(input).run(&env)?;
```

## Why Stillwater?

### Compared to existing solutions:

**vs. frunk:**
- โœ“ Focused on practical use cases, not type-level programming
- โœ“ Better documentation and examples
- โœ“ Effect composition, not just validation

**vs. monadic:**
- โœ“ No awkward macro syntax (`rdrdo! { ... }`)
- โœ“ Minimal overhead (one small Box per combinator, negligible for I/O-bound work)
- โœ“ Idiomatic Rust, not Haskell port

**vs. hand-rolling:**
- โœ“ Validation accumulation built-in
- โœ“ Error context handling
- โœ“ Testability patterns established
- โœ“ Composable, reusable

### What makes it "Rust-first":

- โŒ No attempt at full monad abstraction (impossible without HKTs)
- โœ“ Works with `?` operator via `Try` trait
- โœ“ Zero-cost via generics and monomorphization
- โœ“ Integrates with async/await
- โœ“ Borrows checker friendly
- โœ“ Clear error messages

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
stillwater = "0.8"

# Optional: async support
stillwater = { version = "0.8", features = ["async"] }

# Optional: tracing integration
stillwater = { version = "0.8", features = ["tracing"] }

# Optional: property-based testing
stillwater = { version = "0.8", features = ["proptest"] }

# Optional: multiple features
stillwater = { version = "0.8", features = ["async", "tracing", "proptest"] }
```

## Examples

Run any example with `cargo run --example <name>`:

| Example | Demonstrates |
|---------|--------------|
| [form_validation]examples/form_validation.rs | Validation error accumulation |
| [homogeneous_validation]examples/homogeneous_validation.rs | Type-safe validation for discriminated unions before combining |
| [nonempty]examples/nonempty.rs | NonEmptyVec type for guaranteed non-empty collections |
| [user_registration]examples/user_registration.rs | Effect composition and I/O separation |
| [error_context]examples/error_context.rs | Error trails for debugging |
| [data_pipeline]examples/data_pipeline.rs | Real-world ETL pipeline |
| [testing_patterns]examples/testing_patterns.rs | Testing pure vs effectful code |
| [reader_pattern]examples/reader_pattern.rs | Reader pattern with ask(), asks(), and local() |
| [validation]examples/validation.rs | Validation type and error accumulation patterns |
| [effects]examples/effects.rs | Effect type and composition patterns |
| [parallel_effects]examples/parallel_effects.rs | Parallel execution with par_all, race, and par_all_limit |
| [retry_patterns]examples/retry_patterns.rs | Retry policies, backoff strategies, timeouts, and resilience patterns |
| [io_patterns]examples/io_patterns.rs | IO module helpers for reading/writing |
| [pipeline]examples/pipeline.rs | Data transformation pipelines |
| [traverse]examples/traverse.rs | Traverse and sequence for collections of validations and effects |
| [monoid]examples/monoid.rs | Monoid and Semigroup traits for composition |
| [extended_semigroup]examples/extended_semigroup.rs | Semigroup for HashMap, HashSet, Option, and wrapper types |
| [tracing_demo]examples/tracing_demo.rs | Tracing integration with semantic spans and context |

See [examples/](examples/) directory for full code.

## Production Readiness

**Status: 0.8 - Production Ready for Early Adopters**

- โœ… 295 unit tests passing (includes property-based tests)
- โœ… 122 documentation tests passing
- โœ… Zero clippy warnings
- โœ… Comprehensive examples (16 runnable examples)
- โœ… Full async support
- โœ… Homogeneous validation for type-safe combining
- โœ… Testing utilities with MockEnv and assertion macros
- โœ… CI/CD pipeline with security audits

This library is stable and ready for use. The 0.x version indicates the API may evolve based on community feedback.

## Documentation

- ๐Ÿ“š [User Guide]docs/guide/README.md - Comprehensive tutorials
- ๐Ÿ“– [API Docs]https://docs.rs/stillwater - Full API reference
- ๐Ÿค” [FAQ]docs/FAQ.md - Common questions
- ๐Ÿ›๏ธ [Design]DESIGN.md - Architecture and decisions
- ๐Ÿ’ญ [Philosophy]PHILOSOPHY.md - Core principles
- ๐ŸŽฏ [Patterns]docs/PATTERNS.md - Common patterns and recipes
- ๐Ÿ”„ [Comparison]docs/COMPARISON.md - vs other libraries

## Migrating from Result

Already using `Result` everywhere? No problem! Stillwater integrates seamlessly:

```rust
// Your existing code works as-is
fn validate_email(email: &str) -> Result<Email, Error> {
    // ...
}

// Upgrade to accumulation when you need it
fn validate_form(input: FormInput) -> Validation<Form, Vec<Error>> {
    Validation::all((
        Validation::from_result(validate_email(&input.email)),
        Validation::from_result(validate_age(input.age)),
    ))
}

// Convert back to Result when needed
let result: Result<Form, Vec<Error>> = validation.into_result();
```

Start small, adopt progressively. Use `Validation` only where you need error accumulation.

## Contributing

Contributions welcome! This is a young library with room to grow:
- ๐Ÿ› Bug reports and feature requests via [issues]https://github.com/iepathos/stillwater/issues
- ๐Ÿ“– Documentation improvements
- ๐Ÿงช More examples and use cases
- ๐Ÿ’ก API feedback and design discussions

Before submitting PRs, please open an issue to discuss the change.

## License

MIT ยฉ Glen Baker <iepathos@gmail.com>

---

*"Like a still pond with water flowing through it, stillwater keeps your pure business logic calm and testable while effects flow at the boundaries."*