# 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.

## 3.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 Reference

```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

| 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`  |

---

## 3.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>
```

---

## 3.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
```

---

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

```rust
/// Cryptographically secure random string, URL-safe base64
/// Laravel: Str::random() / AdonisJS: string.random()
pub fn random(length: usize) -> String

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

---

## 3.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
```

---

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

Mirrors **Laravel's `Str::of()`** — every method returns `Self` for chaining.

```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 ==="
```