# rok-utils

[![crates.io](https://img.shields.io/crates/v/rok-utils.svg)](https://crates.io/crates/rok-utils)
[![docs.rs](https://img.shields.io/docsrs/rok-utils)](https://docs.rs/rok-utils)
[![CI](https://github.com/ateeq1999/rok-utils/workflows/CI/badge.svg)](https://github.com/ateeq1999/rok-utils/actions)
[![codecov](https://codecov.io/gh/ateeq1999/rok-utils/branch/main/graph/badge.svg)](https://codecov.io/gh/ateeq1999/rok-utils)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

A Laravel/AdonisJS-inspired utility crate for Rust with zero-bloat, ergonomic helpers.

> **Version:** 0.2.0  
> **Edition:** Rust 2021  
> **MSRV:** 1.92

---

## Quick Start

```toml
[dependencies]
rok-utils = "0.2"
```

```rust
use rok_utils::{to_snake_case, Str};

let snake = to_snake_case("HelloWorld");
assert_eq!(snake, "hello_world");

let result = Str::of("  Hello World  ")
    .trim()
    .to_snake_case()
    .truncate(20)
    .value();
```

---

## Features

| Module | Features |
|--------|----------|
| `str` | Case conversion, truncation, slug, pluralization, fluent builder |
| `arr` | map, filter, reduce, chunk, unique, group_by |
| `errors` | AdonisJS-style error codes with HTTP status |
| `fp` | pipe, compose, tap, retry, lazy, memoize |
| `data` | numbers, dates, UUIDs, hashing |
| `types` | JSON type guards, dot-path access |
| `fs` | ensure_dir, find_files, copy_dir_all |
| `path` | normalize, stem_ext, with_extension |

### Feature Flags

```toml
rok-utils = { version = "0.2", features = ["dates", "crypto", "ids", "json", "random"] }
```

---

## Documentation

- [API Documentation](https://docs.rs/rok-utils)
- [Usage Examples](docs/examples.md)
- [Recipes](docs/examples.md#recipes)
- [Migration Guide](docs/migration-0x-to-1.0.md)
- [Contributing](CONTRIBUTING.md)

---

## Quality Gates

```bash
cargo fmt --check
cargo clippy --all-features -- -D warnings
cargo test --all-features
cargo test --doc
```

---

## License

MIT License - see [LICENSE](LICENSE)

1. [Overview & Philosophy](#1-overview--philosophy)
2. [Crate Configuration — `Cargo.toml`](#2-crate-configuration--cargotoml)
3. [Module Structure](#3-module-structure)
4. [String Utilities — `str.rs`](#4-string-utilities--strrs)
5. [Array / Collection Utilities — `arr.rs`](#5-array--collection-utilities--arrrs)
6. [Error Handling — `errors.rs`](#6-error-handling--errorsrs)
7. [Data Utilities — `data.rs`](#7-data-utilities--datars)
8. [Functional Patterns — `fp.rs`](#8-functional-patterns--fprs)
9. [Fluent Builder API — `fluent.rs`](#9-fluent-builder-api--fluentrs)
10. [Type Helpers — `types.rs`](#10-type-helpers--typesrs)
11. [Testing Strategy](#11-testing-strategy)
12. [CI/CD Pipeline](#12-cicd-pipeline)
13. [Milestone Roadmap](#13-milestone-roadmap)

---

## 1. Overview & Philosophy

`rok-utils` is a **zero-bloat, modular utility crate** for the Rok ecosystem. It follows the same
design philosophy as Laravel's `Illuminate\Support\Str` and AdonisJS's `@adonisjs/core/helpers`:
utilities are ergonomic, predictable, and composable.

### Design Principles

- **Fluent API first** — chainable builder pattern mirroring Laravel's `Str::of()` / AdonisJS's
  `string.camelCase().truncate()` pipeline
- **No runtime panics** — all functions return `Result<T, RokError>` or `Option<T>`; never `unwrap()`
  in library code
- **Feature-flagged** — heavy dependencies (crypto, chrono, uuid) are opt-in via Cargo features
- **UTF-8 native** — all string operations use `chars()`, never raw bytes, mirroring AdonisJS's
  unicode-safe `escapeHTML` / `encodeSymbols` approach
- **`< 400 lines` per file** — every module is a single focused concern; no "god files"
- **Fully tested** — unit tests + `proptest` for invariant testing + doctests as living documentation

### Comparison Table

| Feature                  | Laravel (`Str::`)           | AdonisJS (`string.`)        | `rok-utils`              |
|--------------------------|-----------------------------|-----------------------------|--------------------------|
| Case conversion          | `snake()`, `camel()`        | `snakeCase()`, `camelCase()`| `to_snake_case()` etc.   |
| Fluent chaining          | `Str::of('x')->slug()`      | N/A                         | `Str::of("x").slug()`    |
| Slug generation          | `Str::slug()`               | `string.slug()`             | `str::slug()`            |
| Pluralize                | `Str::plural()`             | `string.plural()`           | `str::plural()` (feature)|
| Error types              | `HttpException`             | `E_HTTP_EXCEPTION`          | `RokError` enum          |
| Byte/time parsing        | N/A                         | `string.bytes.parse('1MB')` | `parse_bytes()` etc.     |
| Random string            | `Str::random(32)`           | `string.random(32)`         | `str::random(32)`        |

---

## 2. Crate Configuration — `Cargo.toml`

```toml
[package]
name        = "rok-utils"
version     = "0.1.0"
edition     = "2021"
description = "Laravel/AdonisJS-inspired utility helpers for the Rok ecosystem"
license     = "MIT"
repository  = "https://github.com/ateeq1999/rok-utils"
keywords    = ["utilities", "string", "helpers", "rok"]
categories  = ["text-processing", "data-structures"]

# ── Core (always available) ──────────────────────────────────────────
[dependencies]
thiserror   = "1.0"             # Derive-based error types
once_cell   = "1.19"            # Lazy statics / memoization

# ── Optional features ────────────────────────────────────────────────
chrono      = { version = "0.4",  optional = true }   # Date/time
serde       = { version = "1.0",  optional = true, features = ["derive"] }
serde_json  = { version = "1.0",  optional = true }
uuid        = { version = "1.6",  optional = true, features = ["v4", "v7"] }
sha2        = { version = "0.10", optional = true }   # Hashing
rand        = { version = "0.8",  optional = true }   # Random strings
unicode-segmentation = { version = "1.10", optional = true }

# ── Dev / Test ────────────────────────────────────────────────────────
[dev-dependencies]
proptest = "1.4"
criterion = { version = "0.5", features = ["html_reports"] }

# ── Feature flags ────────────────────────────────────────────────────
[features]
default  = []
full     = ["dates", "crypto", "json", "ids", "random", "unicode"]
dates    = ["dep:chrono"]
crypto   = ["dep:sha2"]
json     = ["dep:serde", "dep:serde_json"]
ids      = ["dep:uuid"]
random   = ["dep:rand"]
unicode  = ["dep:unicode-segmentation"]

[[bench]]
name    = "string_bench"
harness = false
```

### Feature Flag Usage

```rust
// Consumer opts in to only what they need:
// rok-utils = { version = "0.1", features = ["dates", "crypto"] }

#[cfg(feature = "dates")]
pub use crate::data::dates::*;

#[cfg(feature = "crypto")]
pub use crate::data::hashing::*;
```

---

## 3. Module Structure

```
rok-utils/
├── Cargo.toml
├── src/
│   ├── lib.rs               ← Public re-exports, feature gates
│   ├── str/
│   │   ├── mod.rs           ← str::* top-level re-exports
│   │   ├── case.rs          ← Case conversions (snake, camel, pascal…)
│   │   ├── transform.rs     ← slug, truncate, excerpt, squish, wrap…
│   │   ├── inspect.rs       ← is_empty, starts_with, contains, word_count…
│   │   ├── fluent.rs        ← Str::of() builder (chainable API)
│   │   └── random.rs        ← random() — requires "random" feature
│   ├── arr/
│   │   ├── mod.rs
│   │   ├── ops.rs           ← map, filter, reduce, chunk, flatten…
│   │   ├── query.rs         ← first, last, find, where_in, pluck…
│   │   └── set.rs           ← unique, diff, intersect, merge…
│   ├── errors/
│   │   ├── mod.rs
│   │   ├── kinds.rs         ← RokError enum (all variants)
│   │   ├── context.rs       ← wrap_error, add_context, ResultExt trait
│   │   └── http.rs          ← HttpError with status codes (AdonisJS-style)
│   ├── data/
│   │   ├── mod.rs
│   │   ├── numbers.rs       ← format_number, format_currency, round…
│   │   ├── dates.rs         ← now, today, format, diff (needs "dates")
│   │   ├── hashing.rs       ← hash, verify, generate_token (needs "crypto")
│   │   └── ids.rs           ← uuid_v4, uuid_v7, ulid (needs "ids")
│   ├── fp/
│   │   ├── mod.rs
│   │   ├── compose.rs       ← pipe, compose, partial, tap
│   │   ├── lazy.rs          ← Lazy<T>, memoize, once
│   │   └── macros.rs        ← macro_rules! helpers
│   └── types/
│       ├── mod.rs
│       └── guards.rs        ← is_json, is_uuid, is_ulid, is_url…
├── tests/
│   ├── str_tests.rs
│   ├── arr_tests.rs
│   ├── error_tests.rs
│   └── proptest_suite.rs
└── benches/
    └── string_bench.rs
```

---

## 4. String Utilities — `str.rs`

Modeled after **Laravel's `Illuminate\Support\Str`** and **AdonisJS's `string` helper module**.
All functions are free functions in `rok_utils::str` and also available on the fluent `Str` builder.

### 4.1 Case Conversion — `str/case.rs`

Mirrors AdonisJS `camelCase`, `snakeCase`, `pascalCase`, `dashCase`, `dotCase`, `noCase`, `sentenceCase`, `titleCase`, and Laravel `Str::camel()`, `Str::snake()`, `Str::studly()`, `Str::title()`, `Str::headline()`.

```rust
pub fn to_camel_case(s: &str) -> String
pub fn to_snake_case(s: &str) -> String
pub fn to_pascal_case(s: &str) -> String     // "studly" in Laravel
pub fn to_kebab_case(s: &str) -> String      // "dash" in AdonisJS
pub fn to_dot_case(s: &str) -> String        // AdonisJS dotCase
pub fn to_title_case(s: &str) -> String
pub fn to_headline(s: &str) -> String        // Laravel Str::headline()
pub fn to_sentence_case(s: &str) -> String   // AdonisJS sentenceCase
pub fn to_no_case(s: &str) -> String         // AdonisJS noCase — strips all casing
pub fn to_upper(s: &str) -> String
pub fn to_lower(s: &str) -> String
pub fn ucfirst(s: &str) -> String            // Laravel Str::ucfirst()
pub fn lcfirst(s: &str) -> String            // Laravel Str::lcfirst()
pub fn invert_case(s: &str) -> String
```

#### Implementation Notes

```rust
// to_snake_case: handles "TestV2" → "test_v2", "XMLParser" → "xml_parser"
// Uses char-by-char state machine, never regex, to stay no_std-compatible.

pub fn to_snake_case(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 4);
    let mut prev_upper = false;
    let mut chars = s.chars().peekable();

    while let Some(c) = chars.next() {
        if c.is_uppercase() {
            let next_lower = chars.peek().map(|n| n.is_lowercase()).unwrap_or(false);
            if !out.is_empty() && (!prev_upper || next_lower) {
                out.push('_');
            }
            out.extend(c.to_lowercase());
            prev_upper = true;
        } else if c == '-' || c == ' ' {
            out.push('_');
            prev_upper = false;
        } else {
            out.push(c);
            prev_upper = false;
        }
    }
    out
}
```

**Case Conversion Examples (AdonisJS reference table, adapted for Rust):**

| Input            | `snake`       | `camel`      | `pascal`     | `kebab`       |
|------------------|---------------|--------------|--------------|---------------|
| `"test string"`  | `test_string` | `testString` | `TestString` | `test-string` |
| `"TestV2"`       | `test_v2`     | `testV2`     | `TestV2`     | `test-v2`     |
| `"XMLParser"`    | `xml_parser`  | `xmlParser`  | `XmlParser`  | `xml-parser`  |
| `"version 1.2"`  | `version_1_2` | `version12`  | `Version12`  | `version-12`  |

---

### 4.2 Transform Functions — `str/transform.rs`

```rust
/// Laravel: Str::slug() / AdonisJS: string.slug()
pub fn slug(s: &str, separator: char) -> String

/// Laravel: Str::limit() / AdonisJS: string.truncate()
/// Optionally complete the last word before cutting (AdonisJS `completeWords` option)
pub fn truncate(s: &str, limit: usize, complete_words: bool, suffix: &str) -> String

/// AdonisJS: string.excerpt() — like truncate but strips HTML tags first
pub fn excerpt(s: &str, limit: usize, complete_words: bool) -> String

/// Laravel: Str::squish() / AdonisJS: string.condenseWhitespace()
/// Collapses all internal whitespace runs to a single space, trims edges
pub fn squish(s: &str) -> String

/// Laravel: Str::mask() — masks characters with a given char
/// e.g. mask("hello@world.com", '*', 5) → "hello@*******"
pub fn mask(s: &str, mask_char: char, index: usize) -> String

/// Laravel: Str::wrap() / Str::unwrap()
pub fn wrap(s: &str, before: &str, after: &str) -> String
pub fn unwrap(s: &str, before: &str, after: &str) -> String

/// Laravel: Str::padLeft() / Str::padRight() / Str::padBoth()
pub fn pad_left(s: &str, length: usize, pad: char) -> String
pub fn pad_right(s: &str, length: usize, pad: char) -> String
pub fn pad_both(s: &str, length: usize, pad: char) -> String

/// Laravel: Str::repeat()
pub fn repeat(s: &str, times: usize) -> String

/// Laravel: Str::reverse()
pub fn reverse(s: &str) -> String

/// Laravel: Str::replaceFirst() / Str::replaceLast()
pub fn replace_first(s: &str, from: &str, to: &str) -> String
pub fn replace_last(s: &str, from: &str, to: &str) -> String

/// Laravel: Str::finish() — ensures string ends with value
pub fn finish(s: &str, cap: &str) -> String

/// Laravel: Str::start() — ensures string starts with value
pub fn ensure_start(s: &str, prefix: &str) -> String

/// Laravel: Str::chopStart() / Str::chopEnd()
pub fn chop_start(s: &str, remove: &str) -> &str
pub fn chop_end(s: &str, remove: &str) -> &str

/// Laravel: Str::after() / Str::afterLast()
pub fn after(s: &str, needle: &str) -> &str
pub fn after_last(s: &str, needle: &str) -> &str

/// Laravel: Str::before() / Str::beforeLast()
pub fn before(s: &str, needle: &str) -> &str
pub fn before_last(s: &str, needle: &str) -> &str

/// Laravel: Str::between() / Str::betweenFirst()
pub fn between(s: &str, start: &str, end: &str) -> String
pub fn between_first(s: &str, start: &str, end: &str) -> String

/// AdonisJS: string.interpolate() — "hello {{ name }}" + map
pub fn interpolate(template: &str, vars: &std::collections::HashMap<&str, &str>) -> String

/// AdonisJS: string.sentence() — ["a","b","c"] → "a, b, and c"
pub fn to_sentence(words: &[&str], last_separator: &str) -> String

/// Laravel: Str::wordWrap()
pub fn word_wrap(s: &str, width: usize, break_with: &str) -> String

/// Laravel: Str::words() — truncate at word count
pub fn words(s: &str, count: usize, end: &str) -> String

/// Laravel: Str::swap() — multiple simultaneous replacements
pub fn swap(s: &str, replacements: &std::collections::HashMap<&str, &str>) -> String

/// Laravel: Str::deduplicate() — collapses repeated chars
pub fn deduplicate(s: &str, character: char) -> String

/// Laravel: Str::toBase64() / Str::fromBase64()
pub fn to_base64(s: &str) -> String
pub fn from_base64(s: &str) -> Result<String, RokError>

/// HTML escape — AdonisJS: string.escapeHTML()
pub fn escape_html(s: &str) -> String

/// AdonisJS: string.encodeSymbols()
pub fn encode_symbols(s: &str) -> String

/// AdonisJS: string.bytes.parse / string.seconds.parse / string.milliseconds.parse
pub fn parse_bytes(expr: &str) -> Result<u64, RokError>
pub fn parse_seconds(expr: &str) -> Result<u64, RokError>
pub fn parse_milliseconds(expr: &str) -> Result<u64, RokError>
```

---

### 4.3 Inspection Functions — `str/inspect.rs`

```rust
pub fn is_empty(s: &str) -> bool             // trims first — AdonisJS: string.isEmpty()
pub fn is_ascii(s: &str) -> bool             // Laravel: Str::isAscii()
pub fn is_json(s: &str) -> bool              // Laravel: Str::isJson()
pub fn is_url(s: &str) -> bool               // Laravel: Str::isUrl()
pub fn is_uuid(s: &str) -> bool              // Laravel: Str::isUuid()
pub fn is_ulid(s: &str) -> bool              // Laravel: Str::isUlid()
pub fn is_alphanumeric(s: &str) -> bool
pub fn is_match(s: &str, pattern: &str) -> bool  // Laravel: Str::isMatch()

pub fn length(s: &str) -> usize             // char count, not byte count
pub fn word_count(s: &str) -> usize          // Laravel: Str::wordCount()
pub fn char_at(s: &str, index: usize) -> Option<char>  // Laravel: Str::charAt()
pub fn position(s: &str, needle: &str) -> Option<usize>  // Laravel: Str::position()
pub fn substr_count(s: &str, needle: &str) -> usize

pub fn starts_with(s: &str, needle: &str) -> bool
pub fn ends_with(s: &str, needle: &str) -> bool
pub fn contains(s: &str, needle: &str) -> bool
pub fn contains_all(s: &str, needles: &[&str]) -> bool
pub fn doesnt_contain(s: &str, needle: &str) -> bool

/// AdonisJS: string.prettyHrTime() equivalent
pub fn pretty_duration(nanos: u64) -> String
```

---

### 4.4 Random Strings — `str/random.rs` _(feature = "random")_

```rust
/// Cryptographically secure random string, URL-safe base64
/// Laravel: Str::random() / AdonisJS: string.random()
///
/// string::random(32) → "8mejfWWbXbry8Rh7u8MW3o-6dxd80Thk"
pub fn random(length: usize) -> String

/// Generate a secure password (letters + digits + symbols)
/// Laravel: Str::password()
pub fn password(length: usize, symbols: bool) -> String
```

---

### 4.5 Pluralization — `str/plural.rs` _(feature = "unicode")_

```rust
/// Laravel: Str::plural() / Str::singular()
/// AdonisJS: string.plural(), string.singular(), string.pluralize()
pub fn plural(word: &str, count: usize) -> String
pub fn singular(word: &str) -> String
pub fn pluralize(word: &str, count: usize) -> String  // picks singular or plural based on count
pub fn is_plural(word: &str) -> bool
pub fn is_singular(word: &str) -> bool
```

---

### 4.6 Fluent Builder — `str/fluent.rs`

Mirrors **Laravel's `Str::of()`** — every method returns `Self` for chaining.
Unlike Laravel's PHP fluent strings, this uses Rust's move semantics and zero-copy slices where possible.

```rust
pub struct Str {
    inner: String,
}

impl Str {
    /// Entry point: Str::of("hello world")
    pub fn of(s: impl Into<String>) -> Self

    // ── Transform (mutating, returns Self) ──────────────────────────
    pub fn slug(self) -> Self
    pub fn snake(self) -> Self
    pub fn camel(self) -> Self
    pub fn pascal(self) -> Self
    pub fn kebab(self) -> Self
    pub fn title(self) -> Self
    pub fn upper(self) -> Self
    pub fn lower(self) -> Self
    pub fn trim(self) -> Self
    pub fn ltrim(self) -> Self
    pub fn rtrim(self) -> Self
    pub fn squish(self) -> Self
    pub fn truncate(self, limit: usize) -> Self
    pub fn truncate_words(self, limit: usize) -> Self
    pub fn reverse(self) -> Self
    pub fn repeat(self, times: usize) -> Self
    pub fn append(self, s: &str) -> Self
    pub fn prepend(self, s: &str) -> Self
    pub fn replace(self, from: &str, to: &str) -> Self
    pub fn replace_first(self, from: &str, to: &str) -> Self
    pub fn replace_last(self, from: &str, to: &str) -> Self
    pub fn finish(self, cap: &str) -> Self
    pub fn ensure_start(self, prefix: &str) -> Self
    pub fn wrap(self, before: &str, after: &str) -> Self
    pub fn pad_left(self, n: usize) -> Self
    pub fn pad_right(self, n: usize) -> Self
    pub fn pad_both(self, n: usize) -> Self
    pub fn mask(self, mask_char: char, from: usize) -> Self
    pub fn escape_html(self) -> Self

    // ── Conditional (Laravel whenX / whenEmpty) ─────────────────────
    pub fn when(self, condition: bool, f: impl FnOnce(Self) -> Self) -> Self
    pub fn when_empty(self, f: impl FnOnce(Self) -> Self) -> Self
    pub fn when_not_empty(self, f: impl FnOnce(Self) -> Self) -> Self
    pub fn when_contains(self, needle: &str, f: impl FnOnce(Self) -> Self) -> Self
    pub fn when_starts_with(self, prefix: &str, f: impl FnOnce(Self) -> Self) -> Self
    pub fn when_ends_with(self, suffix: &str, f: impl FnOnce(Self) -> Self) -> Self

    // ── Tap (side-effect, Laravel: tap()) ────────────────────────────
    pub fn tap(self, f: impl FnOnce(&str)) -> Self

    // ── Pipe (transform with arbitrary closure) ───────────────────────
    pub fn pipe<F: FnOnce(String) -> String>(self, f: F) -> Self

    // ── Terminal (consumes Self, returns concrete value) ─────────────
    pub fn to_string(self) -> String
    pub fn len(&self) -> usize
    pub fn is_empty(&self) -> bool
    pub fn contains(&self, needle: &str) -> bool
    pub fn starts_with(&self, prefix: &str) -> bool
    pub fn ends_with(&self, suffix: &str) -> bool
    pub fn word_count(&self) -> usize
    pub fn to_base64(self) -> String
    pub fn split(self, delimiter: &str) -> Vec<String>
    pub fn matches(&self, pattern: &str) -> bool
    pub fn match_all(&self, pattern: &str) -> Vec<String>
    pub fn exactly(&self, other: &str) -> bool
    pub fn value(self) -> String  // alias for to_string()
}
```

**Usage:**

```rust
use rok_utils::str::Str;

let result = Str::of("  Hello World  ")
    .trim()
    .to_snake_case()
    .slug()
    .truncate(20)
    .when_empty(|s| s.append("default"))
    .value();
// "hello-world"

let banner = Str::of("welcome to rok")
    .title()
    .wrap("=== ", " ===")
    .value();
// "=== Welcome To Rok ==="
```

---

## 5. Array / Collection Utilities — `arr.rs`

Inspired by **Laravel's Collection API** and **AdonisJS array helpers**. All functions are generic
over `T` and use Rust's `Iterator` trait internally.

### 5.1 Transformation — `arr/ops.rs`

```rust
/// Map over a slice, collecting into Vec<U>
pub fn map<T, U>(arr: &[T], f: impl Fn(&T) -> U) -> Vec<U>

/// Filter with predicate
pub fn filter<T: Clone>(arr: &[T], f: impl Fn(&T) -> bool) -> Vec<T>

/// Filter-map (map + unwrap Some values)
pub fn filter_map<T, U>(arr: &[T], f: impl Fn(&T) -> Option<U>) -> Vec<U>

/// Reduce with accumulator
pub fn reduce<T, A>(arr: &[T], init: A, f: impl Fn(A, &T) -> A) -> A

/// Split into fixed-size chunks — Laravel: chunk()
pub fn chunk<T: Clone>(arr: &[T], size: usize) -> Vec<Vec<T>>

/// Flatten one level
pub fn flatten<T: Clone>(arr: &[Vec<T>]) -> Vec<T>

/// Flatten all levels (recursive via trait)
pub fn flatten_deep<T: Clone>(arr: &[serde_json::Value]) -> Vec<serde_json::Value>

/// Remove falsy/empty values — Laravel: compact()
pub fn compact<T: Default + PartialEq + Clone>(arr: &[T]) -> Vec<T>

/// Take first N items
pub fn take<T: Clone>(arr: &[T], n: usize) -> Vec<T>

/// Drop first N items
pub fn skip<T: Clone>(arr: &[T], n: usize) -> Vec<T>

/// Reverse
pub fn reverse<T: Clone>(arr: &[T]) -> Vec<T>

/// Zip two slices together
pub fn zip<A: Clone, B: Clone>(a: &[A], b: &[B]) -> Vec<(A, B)>
```

### 5.2 Query — `arr/query.rs`

```rust
pub fn first<T>(arr: &[T]) -> Option<&T>
pub fn last<T>(arr: &[T]) -> Option<&T>
pub fn get<T>(arr: &[T], index: usize) -> Option<&T>

/// Find first matching element
pub fn find<T>(arr: &[T], f: impl Fn(&T) -> bool) -> Option<&T>

/// Any element matches
pub fn some<T>(arr: &[T], f: impl Fn(&T) -> bool) -> bool

/// All elements match
pub fn every<T>(arr: &[T], f: impl Fn(&T) -> bool) -> bool

/// Check membership
pub fn contains<T: PartialEq>(arr: &[T], value: &T) -> bool

/// Group elements by key — Laravel: groupBy()
pub fn group_by<T, K>(arr: &[T], key_fn: impl Fn(&T) -> K) -> HashMap<K, Vec<T>>
where K: Eq + Hash, T: Clone

/// Index by unique key — Laravel: keyBy()
pub fn key_by<T, K>(arr: &[T], key_fn: impl Fn(&T) -> K) -> HashMap<K, T>
where K: Eq + Hash, T: Clone

/// Pluck a field from each item — Laravel: pluck()
pub fn pluck<T, U>(arr: &[T], extractor: impl Fn(&T) -> U) -> Vec<U>

/// where_in style filter
pub fn where_in<T: PartialEq + Clone>(arr: &[T], values: &[T]) -> Vec<T>

pub fn count<T>(arr: &[T]) -> usize
pub fn is_empty<T>(arr: &[T]) -> bool
pub fn is_not_empty<T>(arr: &[T]) -> bool
```

### 5.3 Set Operations — `arr/set.rs`

```rust
/// Remove duplicates (preserves order) — Laravel/AdonisJS: unique()
pub fn unique<T: PartialEq + Clone>(arr: &[T]) -> Vec<T>

/// Remove specific values — Laravel: without()
pub fn without<T: PartialEq + Clone>(arr: &[T], values: &[T]) -> Vec<T>

/// Merge two slices (no dedup)
pub fn merge<T: Clone>(a: &[T], b: &[T]) -> Vec<T>

/// Set intersection
pub fn intersect<T: PartialEq + Clone>(a: &[T], b: &[T]) -> Vec<T>

/// Set difference (a - b)
pub fn diff<T: PartialEq + Clone>(a: &[T], b: &[T]) -> Vec<T>

/// Sort with comparator
pub fn sort_by<T: Clone>(arr: &[T], cmp: impl Fn(&T, &T) -> std::cmp::Ordering) -> Vec<T>

/// Shuffle (requires feature = "random")
#[cfg(feature = "random")]
pub fn shuffle<T: Clone>(arr: &[T]) -> Vec<T>
```

---

## 6. Error Handling — `errors.rs`

Inspired by **AdonisJS's typed exception system** (`E_ROUTE_NOT_FOUND`, `E_UNAUTHORIZED_ACCESS`,
`E_HTTP_EXCEPTION`, etc.) and **Laravel's `HttpException`**. Every error variant carries a
machine-readable code string matching the AdonisJS convention.

### 6.1 Core Error Enum — `errors/kinds.rs`

```rust
use thiserror::Error;

/// All rok-utils errors. Each variant maps to an AdonisJS-style error code.
#[derive(Debug, Error)]
pub enum RokError {
    // ── String errors ──────────────────────────────────────────────
    #[error("[E_INVALID_UTF8] Invalid UTF-8 input: {0}")]
    InvalidUtf8(String),

    #[error("[E_INVALID_BASE64] Cannot decode base64 string: {0}")]
    InvalidBase64(String),

    // ── Parse errors ───────────────────────────────────────────────
    #[error("[E_PARSE_BYTES] Cannot parse byte expression '{expr}': {reason}")]
    ParseBytes { expr: String, reason: String },

    #[error("[E_PARSE_DURATION] Cannot parse duration expression '{expr}': {reason}")]
    ParseDuration { expr: String, reason: String },

    // ── Data errors ────────────────────────────────────────────────
    #[error("[E_INVALID_JSON] JSON parse failed: {0}")]
    InvalidJson(String),

    #[error("[E_INVALID_UUID] '{0}' is not a valid UUID")]
    InvalidUuid(String),

    #[error("[E_INVALID_DATE] Cannot parse date '{0}'")]
    InvalidDate(String),

    // ── HTTP-style errors (AdonisJS parity) ────────────────────────
    #[error("[E_NOT_FOUND] Resource not found: {0}")]
    NotFound(String),                             // HTTP 404

    #[error("[E_UNAUTHORIZED] Unauthorized: {0}")]
    Unauthorized(String),                         // HTTP 401

    #[error("[E_FORBIDDEN] Forbidden: {0}")]
    Forbidden(String),                            // HTTP 403

    #[error("[E_VALIDATION_FAILURE] Validation failed: {field} — {reason}")]
    ValidationFailure { field: String, reason: String },  // HTTP 422

    #[error("[E_TOO_MANY_REQUESTS] Rate limit exceeded")]
    TooManyRequests,                              // HTTP 429

    #[error("[E_INTERNAL] Internal error: {0}")]
    Internal(String),                             // HTTP 500

    // ── Generic wrapped error ──────────────────────────────────────
    #[error("[E_WRAPPED] {message}: {source}")]
    Wrapped {
        message: String,
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },
}

impl RokError {
    /// AdonisJS-style error code string
    pub fn code(&self) -> &'static str {
        match self {
            Self::InvalidUtf8(_)    => "E_INVALID_UTF8",
            Self::InvalidBase64(_)  => "E_INVALID_BASE64",
            Self::ParseBytes { .. } => "E_PARSE_BYTES",
            Self::ParseDuration{..} => "E_PARSE_DURATION",
            Self::InvalidJson(_)    => "E_INVALID_JSON",
            Self::InvalidUuid(_)    => "E_INVALID_UUID",
            Self::InvalidDate(_)    => "E_INVALID_DATE",
            Self::NotFound(_)       => "E_NOT_FOUND",
            Self::Unauthorized(_)   => "E_UNAUTHORIZED",
            Self::Forbidden(_)      => "E_FORBIDDEN",
            Self::ValidationFailure{..} => "E_VALIDATION_FAILURE",
            Self::TooManyRequests   => "E_TOO_MANY_REQUESTS",
            Self::Internal(_)       => "E_INTERNAL",
            Self::Wrapped { .. }    => "E_WRAPPED",
        }
    }

    /// HTTP status code — compatible with AdonisJS status semantics
    pub fn status(&self) -> u16 {
        match self {
            Self::NotFound(_)            => 404,
            Self::Unauthorized(_)        => 401,
            Self::Forbidden(_)           => 403,
            Self::ValidationFailure{..}  => 422,
            Self::TooManyRequests        => 429,
            _                            => 500,
        }
    }

    /// Whether this error is recoverable / self-handled
    /// AdonisJS: "self-handled" exceptions convert themselves to HTTP responses
    pub fn is_self_handled(&self) -> bool {
        matches!(self,
            Self::NotFound(_)
            | Self::Unauthorized(_)
            | Self::Forbidden(_)
            | Self::ValidationFailure { .. }
            | Self::TooManyRequests
        )
    }
}
```

### 6.2 Context & Wrapping — `errors/context.rs`

```rust
/// Add context to any error (AdonisJS: error.help / error.cause pattern)
pub fn wrap_error<E>(error: E, message: impl Into<String>) -> RokError
where E: std::error::Error + Send + Sync + 'static

/// Extension trait on Result — Laravel-style ergonomics
pub trait ResultExt<T> {
    fn context(self, msg: &str) -> Result<T, RokError>;
    fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, RokError>;
    fn map_rok_err(self, variant: fn(String) -> RokError) -> Result<T, RokError>;
    fn or_not_found(self, resource: &str) -> Result<T, RokError>;
}

impl<T, E: std::error::Error + Send + Sync + 'static> ResultExt<T> for Result<T, E> {
    fn context(self, msg: &str) -> Result<T, RokError> {
        self.map_err(|e| wrap_error(e, msg))
    }

    fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, RokError> {
        self.map_err(|e| wrap_error(e, f()))
    }

    fn map_rok_err(self, variant: fn(String) -> RokError) -> Result<T, RokError> {
        self.map_err(|e| variant(e.to_string()))
    }

    fn or_not_found(self, resource: &str) -> Result<T, RokError> {
        self.map_err(|_| RokError::NotFound(resource.to_string()))
    }
}
```

**Usage:**

```rust
use rok_utils::errors::{RokError, ResultExt};

fn find_user(id: u64) -> Result<User, RokError> {
    db::find(id)
        .context("Failed to query user table")
        .or_not_found(&format!("User #{id}"))
}

// Error codes match AdonisJS convention:
let err = RokError::NotFound("User #42".into());
assert_eq!(err.code(), "E_NOT_FOUND");
assert_eq!(err.status(), 404);
assert!(err.is_self_handled());
```

### 6.3 Error Macros — `errors/macros.rs`

```rust
/// Quickly build a validation error
macro_rules! validation_err {
    ($field:expr, $reason:expr) => {
        RokError::ValidationFailure {
            field: $field.to_string(),
            reason: $reason.to_string(),
        }
    };
}

/// Bail early from a function with a RokError
macro_rules! rok_bail {
    ($variant:ident, $msg:expr) => {
        return Err(RokError::$variant($msg.to_string()))
    };
}
```

---

## 7. Data Utilities — `data.rs`

### 7.1 Numbers — `data/numbers.rs`

```rust
/// Format with thousand separators: 1_234_567.89 → "1,234,567.89"
pub fn format_number(n: f64, decimals: usize, sep: char) -> String

/// Format as currency: format_currency(1234.5, "USD") → "$1,234.50"
pub fn format_currency(amount: f64, currency: &str) -> String

/// Format as percentage: 0.1234 → "12.34%"
pub fn format_percentage(ratio: f64, decimals: usize) -> String

pub fn round(n: f64, decimals: u32) -> f64
pub fn ceil(n: f64, decimals: u32) -> f64
pub fn floor(n: f64, decimals: u32) -> f64
pub fn clamp(n: f64, min: f64, max: f64) -> f64
pub fn lerp(a: f64, b: f64, t: f64) -> f64
```

### 7.2 Dates — `data/dates.rs` _(feature = "dates")_

```rust
use chrono::{DateTime, Utc, NaiveDate, Duration};

pub fn now() -> DateTime<Utc>
pub fn today() -> NaiveDate
pub fn yesterday() -> NaiveDate
pub fn tomorrow() -> NaiveDate

/// Format date: format_date(now(), "%Y-%m-%d") → "2025-01-15"
pub fn format_date(dt: &DateTime<Utc>, fmt: &str) -> String

/// Parse from string: parse_date("2025-01-15", "%Y-%m-%d")
pub fn parse_date(s: &str, fmt: &str) -> Result<DateTime<Utc>, RokError>

/// Diff in days between two dates
pub fn diff_days(a: &NaiveDate, b: &NaiveDate) -> i64

/// Human-readable relative time: "3 days ago", "in 5 minutes"
pub fn human_diff(dt: &DateTime<Utc>) -> String

/// Add duration
pub fn add_days(dt: &NaiveDate, days: i64) -> NaiveDate
pub fn add_hours(dt: &DateTime<Utc>, hours: i64) -> DateTime<Utc>

/// AdonisJS: string.seconds.parse("10h") → 36000
/// (non-feature gated, implemented in str/transform.rs as parse_seconds)
```

### 7.3 IDs — `data/ids.rs` _(feature = "ids")_

```rust
/// UUID v4 (random) — Laravel: Str::uuid()
pub fn uuid_v4() -> String

/// UUID v7 (time-ordered) — Laravel: Str::uuid7() / Str::orderedUuid()
pub fn uuid_v7() -> String

/// ULID — Laravel: Str::ulid()
pub fn ulid() -> String

/// Validate UUID string — Laravel: Str::isUuid()
pub fn is_uuid(s: &str) -> bool

/// Validate ULID string — Laravel: Str::isUlid()
pub fn is_ulid(s: &str) -> bool
```

### 7.4 Hashing — `data/hashing.rs` _(feature = "crypto")_

```rust
/// SHA-256 hex digest
pub fn hash_sha256(input: &str) -> String

/// Verify SHA-256 hash
pub fn verify_sha256(input: &str, expected: &str) -> bool

/// Generate a secure opaque token (URL-safe base64 of random bytes)
/// Like AdonisJS opaque tokens
pub fn generate_token(bytes: usize) -> String

/// Constant-time comparison to prevent timing attacks
pub fn secure_compare(a: &str, b: &str) -> bool
```

---

## 8. Functional Patterns — `fp.rs`

Inspired by **Laravel's pipeline**, **AdonisJS middleware chaining**, and standard functional
programming utilities.

### 8.1 Core Combinators — `fp/compose.rs`

```rust
/// Thread a value through a sequence of transformations
/// Mirrors Laravel's pipeline / AdonisJS middleware chain
pub fn pipe<T>(value: T, fns: Vec<Box<dyn Fn(T) -> T>>) -> T

/// Compose two functions: compose(f, g)(x) = f(g(x))
pub fn compose<A, B, C>(f: impl Fn(B) -> C, g: impl Fn(A) -> B) -> impl Fn(A) -> C

/// Apply a side-effect then return the original value — Laravel: tap()
pub fn tap<T>(value: T, f: impl FnOnce(&T)) -> T

/// Apply function and return result — AdonisJS: apply
pub fn apply<T, U>(value: T, f: impl FnOnce(T) -> U) -> U

/// Return default if None — convenience over Option::unwrap_or_default
pub fn or_default<T: Default>(opt: Option<T>) -> T

/// Retry an operation N times before failing
pub fn retry<T, E>(times: usize, f: impl Fn() -> Result<T, E>) -> Result<T, E>
```

### 8.2 Lazy & Memoization — `fp/lazy.rs`

```rust
use once_cell::sync::OnceCell;

/// Lazily initialized value — computed only on first access
pub struct Lazy<T> {
    cell: OnceCell<T>,
    init: Box<dyn Fn() -> T + Send + Sync>,
}

impl<T> Lazy<T> {
    pub fn new(init: impl Fn() -> T + Send + Sync + 'static) -> Self
    pub fn get(&self) -> &T   // computes on first call, returns cached after
    pub fn is_initialized(&self) -> bool
}

/// Memoize a single-argument function using a HashMap cache
pub fn memoize<A, R>(f: impl Fn(A) -> R) -> impl FnMut(A) -> R
where
    A: std::hash::Hash + Eq + Clone,
    R: Clone,

/// Run a closure exactly once, cache the result (thread-safe)
/// Wraps `once_cell::sync::OnceCell`
pub fn once<T>(f: impl FnOnce() -> T) -> impl Fn() -> T
```

### 8.3 Helper Macros — `fp/macros.rs`

```rust
/// Build a Vec of boxed closures for use with pipe()
macro_rules! pipeline {
    ($($fn:expr),* $(,)?) => {
        vec![$(Box::new($fn) as Box<dyn Fn(_) -> _>),*]
    };
}

/// Unwrap or return Err with a RokError variant
macro_rules! require {
    ($opt:expr, $err:expr) => {
        match $opt {
            Some(v) => v,
            None => return Err($err),
        }
    };
}

/// Conditional expression returning a value
macro_rules! when {
    ($cond:expr => $then:expr, else $else:expr) => {
        if $cond { $then } else { $else }
    };
}
```

---

## 9. Fluent Builder API — `fluent.rs`

The top-level `Str` builder (see §4.6) is the primary ergonomic API, but `rok-utils` also provides
collection-level fluency via `Collect<T>`:

```rust
pub struct Collect<T> {
    inner: Vec<T>,
}

impl<T: Clone + 'static> Collect<T> {
    pub fn of(arr: Vec<T>) -> Self
    pub fn map<U>(self, f: impl Fn(T) -> U) -> Collect<U>
    pub fn filter(self, f: impl Fn(&T) -> bool) -> Self
    pub fn take(self, n: usize) -> Self
    pub fn skip(self, n: usize) -> Self
    pub fn unique(self) -> Self  where T: PartialEq
    pub fn chunk(self, size: usize) -> Collect<Vec<T>>
    pub fn tap(self, f: impl FnOnce(&[T])) -> Self
    pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self
    pub fn sort_by(self, cmp: impl Fn(&T, &T) -> std::cmp::Ordering) -> Self
    pub fn value(self) -> Vec<T>
    pub fn first(self) -> Option<T>
    pub fn last(self) -> Option<T>
    pub fn count(&self) -> usize
    pub fn is_empty(&self) -> bool
}
```

---

## 10. Type Helpers — `types.rs`

AdonisJS ships `@adonisjs/core/helpers/is` for runtime type guards. We mirror this:

```rust
pub fn is_string(val: &serde_json::Value) -> bool
pub fn is_number(val: &serde_json::Value) -> bool
pub fn is_bool(val: &serde_json::Value) -> bool
pub fn is_array(val: &serde_json::Value) -> bool
pub fn is_object(val: &serde_json::Value) -> bool
pub fn is_null(val: &serde_json::Value) -> bool
pub fn is_defined(val: &serde_json::Value) -> bool   // not null/undefined

/// Deep equality (recursive)
pub fn deep_equal(a: &serde_json::Value, b: &serde_json::Value) -> bool

/// Get nested value by dot path: get_path(&json, "user.address.city")
pub fn get_path<'a>(val: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value>

/// Set nested value by dot path (returns modified clone)
pub fn set_path(val: serde_json::Value, path: &str, new: serde_json::Value) -> serde_json::Value
```

---

## 11. Testing Strategy

### 11.1 Unit Tests (inline per module)

Every public function has at least:
- A happy-path test
- An edge case (empty string, zero, negative numbers, Unicode input)
- A `#[should_panic]` or `Err` assertion where appropriate

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_to_snake_case_basic() {
        assert_eq!(to_snake_case("HelloWorld"), "hello_world");
        assert_eq!(to_snake_case("XMLParser"),  "xml_parser");
        assert_eq!(to_snake_case("TestV2"),     "test_v2");
        assert_eq!(to_snake_case(""),           "");
    }

    #[test]
    fn test_truncate_complete_words() {
        let s = "This is a very long title";
        assert_eq!(truncate(s, 10, true, "..."), "This is a...");
        assert_eq!(truncate(s, 10, false, "..."), "This is a ...");
    }

    #[test]
    fn test_slug_unicode() {
        assert_eq!(slug("hello ♥ world", '-'), "hello-love-world");
    }
}
```

### 11.2 Property-Based Tests (`proptest`)

```rust
use proptest::prelude::*;

proptest! {
    /// round-trip: snake → camel → snake should be stable
    #[test]
    fn prop_snake_camel_roundtrip(s in "[a-z][a-z0-9]{0,20}(_[a-z][a-z0-9]{0,10})*") {
        let result = to_snake_case(&to_camel_case(&s));
        prop_assert_eq!(result, s);
    }

    /// truncate never returns more chars than the limit
    #[test]
    fn prop_truncate_length(s in ".*", limit in 0usize..200) {
        let result = truncate(&s, limit, false, "");
        prop_assert!(result.chars().count() <= limit + 3);  // +3 for suffix
    }

    /// unique preserves all elements that appeared at least once
    #[test]
    fn prop_unique_subset(arr in prop::collection::vec(0i32..100, 0..50)) {
        let result = unique(&arr);
        for x in &result {
            prop_assert!(arr.contains(x));
        }
    }
}
```

### 11.3 Benchmarks (Criterion)

```rust
// benches/string_bench.rs
use criterion::{criterion_group, criterion_main, Criterion};
use rok_utils::str::*;

fn bench_slug(c: &mut Criterion) {
    let input = "The Quick Brown Fox Jumps Over The Lazy Dog";
    c.bench_function("slug", |b| b.iter(|| slug(input, '-')));
}

fn bench_fluent_chain(c: &mut Criterion) {
    c.bench_function("fluent_chain", |b| {
        b.iter(|| {
            Str::of("  Hello World  ")
                .trim()
                .slug()
                .truncate(30)
                .value()
        })
    });
}

criterion_group!(benches, bench_slug, bench_fluent_chain);
criterion_main!(benches);
```

### 11.4 Doctests as Living Docs

Every public function has a `/// # Examples` block that doubles as a doctest:

```rust
/// Convert a string to snake_case.
///
/// # Examples
///
/// ```rust
/// use rok_utils::str::to_snake_case;
///
/// assert_eq!(to_snake_case("HelloWorld"), "hello_world");
/// assert_eq!(to_snake_case("XMLParser"),  "xml_parser");
/// assert_eq!(to_snake_case(""),           "");
/// ```
pub fn to_snake_case(s: &str) -> String { ... }
```

---

## 12. CI/CD Pipeline

```yaml
# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - name: Format check
        run: cargo fmt --all -- --check

      - name: Clippy (no warnings allowed)
        run: cargo clippy --all-features -- -D warnings

      - name: Tests (all features)
        run: cargo test --all-features

      - name: Tests (no default features)
        run: cargo test --no-default-features

      - name: Doctests
        run: cargo test --doc --all-features

      - name: Proptest suite
        run: cargo test --test proptest_suite --all-features

  bench:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo bench --all-features

  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo install cargo-tarpaulin
      - run: cargo tarpaulin --all-features --out Xml
      - uses: codecov/codecov-action@v3
```

---

## 13. Milestone Roadmap

### Milestone 1 — Foundation (Publishing `0.1.0`)
**Goal:** Core string, array, and error modules are complete and published to crates.io.

- [ ] `str/case.rs` — all 12 case conversion functions with doctests
- [ ] `str/transform.rs` — 25+ transform functions (slug, truncate, excerpt, squish, wrap…)
- [ ] `str/inspect.rs` — 15+ inspection functions
- [ ] `str/fluent.rs` — `Str::of()` builder with 30+ chainable methods and `when*` conditionals
- [ ] `arr/ops.rs`, `arr/query.rs`, `arr/set.rs` — full collection API
- [ ] `errors/kinds.rs` — `RokError` enum with AdonisJS-style codes + HTTP status
- [ ] `errors/context.rs` — `ResultExt` trait and `wrap_error`
- [ ] `fp/compose.rs` — `pipe`, `tap`, `compose`, `retry`
- [ ] CI green: fmt + clippy + tests + doctests
- [ ] Code coverage ≥ 90%
- [ ] All files ≤ 400 lines

### Milestone 2 — Feature Modules (`0.2.0`)
**Goal:** Optional modules enabled via feature flags are ready for production.

- [ ] `data/numbers.rs` — `format_number`, `format_currency`, `format_percentage`
- [ ] `data/dates.rs` — chrono-backed date helpers (feature = "dates")
- [ ] `data/ids.rs` — UUID v4/v7, ULID (feature = "ids")
- [ ] `data/hashing.rs` — SHA-256, tokens (feature = "crypto")
- [ ] `str/random.rs` — cryptographically secure `random()` and `password()` (feature = "random")
- [ ] `str/plural.rs` — pluralize, singular, is_plural (feature = "unicode")
- [ ] `fp/lazy.rs` — `Lazy<T>`, `memoize`, `once`
- [ ] `types/guards.rs` — JSON type guards and dot-path access

### Milestone 3 — CLI Integration (`rok-cli` uses `rok-utils`)
**Goal:** Replace all ad-hoc string handling in `rok-cli` with `rok-utils`.

- [ ] Replace internal case conversion with `rok_utils::str::to_snake_case` etc.
- [ ] Use `Str::of()` fluent builder in code generation templates
- [ ] Wire `RokError` into CLI error reporting pipeline
- [ ] `rok generate model <Name>` uses `to_pascal_case` + `to_snake_case` consistently
- [ ] Benchmark: no regression vs. previous hand-rolled string code

### Milestone 4 — SQLx / ORM Integration
**Goal:** `rok-orm` macro system uses `rok-utils` for identifier generation.

- [ ] `model!` macro uses `to_snake_case` for table names
- [ ] `migrate!` uses `uuid_v7` for migration timestamps
- [ ] `query!` builder uses `Collect<T>` for result mapping
- [ ] Error propagation: database errors wrap to `RokError::Internal`

### Milestone 5 — Docs Site & Stable API (`1.0.0`)
**Goal:** Public API is frozen and fully documented.

- [ ] `cargo doc --all-features --no-deps` generates complete API reference
- [ ] Deploy to GitHub Pages via CI
- [ ] Add `#[non_exhaustive]` to `RokError` enum
- [ ] Write migration guide from `0.x` to `1.0`
- [ ] All public types implement `Debug + Clone + Send + Sync` where applicable
- [ ] Minimum Supported Rust Version (MSRV) pinned to stable - 2 releases

---

## Summary

This plan delivers a `rok-utils` crate that:

1. **Mirrors battle-tested APIs** — Laravel's `Str` and AdonisJS's `string` helpers are the
   benchmark; every function name and behavior maps to a known analogue
2. **Ergonomic by default** — the fluent `Str::of()` builder makes complex transformations
   readable and composable without intermediate variables
3. **Error-first design** — AdonisJS-style typed error codes (`E_NOT_FOUND`, `E_VALIDATION_FAILURE`)
   with HTTP status semantics baked in from day one
4. **Zero-bloat** — feature flags ensure consumers pay only for what they use
5. **Production-ready** — proptest invariants, criterion benchmarks, and ≥90% coverage
   before any milestone ships
