Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

rok-utils

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

crates.io docs.rs License: MIT

Features

  • String utilities — case conversion, truncation, pluralization, fluent builder
  • Array utilities — map, filter, reduce, chunk, unique, group_by
  • Error handling — AdonisJS-style error codes with HTTP status
  • Functional patterns — pipe, compose, tap, retry, lazy, memoize
  • Data utilities — numbers, dates, hashing, IDs
  • Type guards — JSON type guards and dot-path access

Quick Example

#![allow(unused)]
fn main() {
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()
    .lower()
    .replace(" ", "_")
    .value();
assert_eq!(result, "hello_world");
}

Why rok-utils?

FeatureDescription
Battle-tested APIsInspired by Laravel’s Str and AdonisJS’s string helpers
ErgonomicFluent Str::of() builder for chainable transformations
Error-firstAdonisJS-style typed error codes with HTTP status
Zero-bloatFeature flags ensure consumers pay only for what they use
UTF-8 nativeAll string operations use chars(), never raw bytes

Next Steps

Getting Started

Let’s explore the basics of rok-utils with practical examples.

Case Conversion

Convert strings between different casing styles:

#![allow(unused)]
fn main() {
use rok_utils::{to_snake_case, to_camel_case, to_kebab_case};

let snake = to_snake_case("HelloWorld");    // "hello_world"
let camel = to_camel_case("hello_world");   // "helloWorld"
let kebab = to_kebab_case("HelloWorld");   // "hello-world"
}

Fluent Builder

The Str::of() builder provides a chainable API:

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

let result = Str::of("  Hello World  ")
    .trim()
    .lower()
    .replace(" ", "-")
    .value();

assert_eq!(result, "hello-world");
}

Conditional Transformations

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

let result = Str::of("hello")
    .when(true, |s| s.append(" world"))
    .when(false, |s| s.append(" hidden"))
    .value();

assert_eq!(result, "hello world");
}

Array Utilities

#![allow(unused)]
fn main() {
use rok_utils::arr::{map, filter, find};

let numbers = [1, 2, 3, 4, 5];

let doubled = map(&numbers, |x| x * 2);
// [2, 4, 6, 8, 10]

let evens = filter(&numbers, |x| x % 2 == 0);
// [2, 4]

let found = find(&numbers, |x| *x == 3);
// Some(3)
}

Error Handling

#![allow(unused)]
fn main() {
use rok_utils::{RokError, RokResultExt};

fn find_user(id: u64) -> Result<String, RokError> {
    if id == 42 {
        Ok("Alice".to_string())
    } else {
        Err(RokError::NotFound(format!("User #{id}")))
    }
}

// Add context to errors
let result = find_user(42).context("Database query failed");
}

Next Steps

Installation

Add rok-utils to your Cargo.toml:

[dependencies]
rok-utils = "0.2"

Feature Flags

Enable optional features as needed:

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

Available Features

FeatureDescriptionExtra Dependencies
datesDate/time utilitieschrono
cryptoHashing and token generationsha2, md-5, subtle
idsUUID and ULID generationuuid, rand
jsonJSON type guards and path accessserde, serde_json
randomRandom string generationrand, regex
fullEnable all featuresall of above

Quick Install (All Features)

[dependencies]
rok-utils = { version = "0.2", features = ["full"] }

MSRV

Minimum Supported Rust Version is 1.92.

String Utilities

Comprehensive string manipulation functions for Rust.

Case Conversion

to_snake_case

Converts a string to snake_case:

#![allow(unused)]
fn main() {
use rok_utils::to_snake_case;

assert_eq!(to_snake_case("HelloWorld"), "hello_world");
assert_eq!(to_snake_case("XMLParser"), "xml_parser");
assert_eq!(to_snake_case("TestV2"), "test_v2");
}

to_camel_case

Converts a string to camelCase:

#![allow(unused)]
fn main() {
use rok_utils::to_camel_case;

assert_eq!(to_camel_case("hello_world"), "helloWorld");
assert_eq!(to_camel_case("test-string"), "testString");
}

to_kebab_case

Converts a string to kebab-case:

#![allow(unused)]
fn main() {
use rok_utils::to_kebab_case;

assert_eq!(to_kebab_case("HelloWorld"), "hello-world");
assert_eq!(to_kebab_case("Hello_World"), "hello-world");
}

to_pascal_case

Converts a string to PascalCase:

#![allow(unused)]
fn main() {
use rok_utils::to_pascal_case;

assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
assert_eq!(to_pascal_case("test-string"), "TestString");
}

Transformations

slug

Creates a URL-friendly slug:

#![allow(unused)]
fn main() {
use rok_utils::slug;

assert_eq!(slug("Hello World!"), "hello-world");
assert_eq!(slug("O'Reilly's Guide", '_'), "oreillys_guide");
}

truncate

Truncates a string to a specified length:

#![allow(unused)]
fn main() {
use rok_utils::truncate;

assert_eq!(truncate("Hello World!", 5), "Hello...");
assert_eq!(truncate("Hi", 10), "Hi");
}

squish

Collapses whitespace and trims:

#![allow(unused)]
fn main() {
use rok_utils::squish;

assert_eq!(squish("  hello   world  "), "hello world");
}

wrap

Wraps a string with prefix and suffix:

#![allow(unused)]
fn main() {
use rok_utils::wrap;

assert_eq!(wrap("content", "[", "]"), "[content]");
}

Inspection

is_empty

Checks if a string is empty (after trimming):

#![allow(unused)]
fn main() {
use rok_utils::is_empty;

assert!(is_empty("  "));
assert!(!is_empty(" hello "));
}

contains

Checks if string contains a substring:

#![allow(unused)]
fn main() {
use rok_utils::contains;

assert!(contains("hello world", "lo"));
assert!(!contains("hello", "xyz"));
}

starts_with / ends_with

#![allow(unused)]
fn main() {
use rok_utils::{starts_with, ends_with};

assert!(starts_with("hello world", "hello"));
assert!(ends_with("hello world", "world"));
}

Pluralization

pluralize

#![allow(unused)]
fn main() {
use rok_utils::pluralize;

assert_eq!(pluralize("cat", 1), "cat");
assert_eq!(pluralize("cat", 2), "cats");
assert_eq!(pluralize("box", 3), "boxes");
}

Requires the unicode feature flag.

See Also

Fluent Builder

The Str::of() builder provides a chainable API for complex string transformations.

Basic Usage

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

let result = Str::of("  Hello World  ")
    .trim()
    .lower()
    .replace(" ", "_")
    .value();

assert_eq!(result, "hello_world");
}

String Transformations

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

Str::of("hello")
    .trim()           // "hello"
    .lower()          // "hello"
    .upper()          // "HELLO"
    .title()          // "Hello"
    .replace("o", "0") // "hell0"
    .reverse()        // "0lleh"
    .value()
}

Conditional Transformations

when

Applies transformation conditionally:

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

let result = Str::of("hello")
    .when(true, |s| s.append(" world"))
    .when(false, |s| s.append(" hidden"))
    .value();

assert_eq!(result, "hello world");
}

when_empty

Applies transformation when string is empty:

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

let result = Str::of("")
    .when_empty(|s| s.append("default"))
    .value();

assert_eq!(result, "default");
}

when_contains

Applies transformation when string contains a substring:

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

let result = Str::of("user@example.com")
    .when_contains("@", |s| s.replace("@", " [at] "))
    .value();

assert_eq!(result, "user [at] example.com");
}

Value Extraction

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

let s = Str::of("hello");

assert_eq!(s.len(), 5);
assert!(!s.is_empty());
assert!(s.contains("ell"));
}

Piping Custom Functions

Use pipe to apply custom transformations:

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

let result = Str::of("hello")
    .pipe(|s| s.to_uppercase())
    .pipe(|s| format!("{}!", s))
    .value();

assert_eq!(result, "HELLO!");
}

Side Effects with Tap

tap allows side effects without changing the value:

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

let mut log = Vec::new();
let result = Str::of("hello")
    .tap(|s| log.push(s.len()))
    .tap(|s| log.push(s.chars().count()))
    .value();

assert_eq!(result, "hello");
assert_eq!(log, vec![5, 5]);
}

Real-World Example

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

fn format_username(name: &str) -> String {
    Str::of(name)
        .trim()
        .lower()
        .when_empty(|s| s.append("anonymous"))
        .replace(" ", "_")
        .prepend("@")
        .value()
}

assert_eq!(format_username("  JohnDoe  "), "@johndoe");
assert_eq!(format_username("   "), "@anonymous");
}

See Also

Array Utilities

Pure functional helpers for working with slices and vectors.

Transformation

map

#![allow(unused)]
fn main() {
use rok_utils::arr::map;

let numbers = [1, 2, 3, 4, 5];
let doubled = map(&numbers, |x| x * 2);
assert_eq!(doubled, vec![2, 4, 6, 8, 10]);
}

filter

#![allow(unused)]
fn main() {
use rok_utils::arr::filter;

let numbers = [1, 2, 3, 4, 5];
let evens = filter(&numbers, |x| x % 2 == 0);
assert_eq!(evens, vec![2, 4]);
}

reduce

#![allow(unused)]
fn main() {
use rok_utils::arr::reduce;

let numbers = [1, 2, 3, 4];
let sum = reduce(&numbers, 0, |acc, x| acc + x);
assert_eq!(sum, 10);
}

Chunking and Flipping

chunk

#![allow(unused)]
fn main() {
use rok_utils::arr::chunk;

let numbers = [1, 2, 3, 4, 5, 6];
let chunks = chunk(&numbers, 2);
assert_eq!(chunks, vec![vec![1, 2], vec![3, 4], vec![5, 6]]);
}

reverse

#![allow(unused)]
fn main() {
use rok_utils::arr::reverse;

let numbers = [1, 2, 3];
let reversed = reverse(&numbers);
assert_eq!(reversed, vec![3, 2, 1]);
}

Query

first / last

#![allow(unused)]
fn main() {
use rok_utils::arr::{first, last};

let items = ["a", "b", "c"];
assert_eq!(first(&items), Some(&"a"));
assert_eq!(last(&items), Some(&"c"));
}

find

#![allow(unused)]
fn main() {
use rok_utils::arr::find;

let numbers = [1, 2, 3, 4, 5];
let found = find(&numbers, |x| *x == 3);
assert_eq!(found, Some(&3));
}

contains

#![allow(unused)]
fn main() {
use rok_utils::arr::contains;

let items = ["a", "b", "c"];
assert!(contains(&items, &"b"));
assert!(!contains(&items, &"d"));
}

Set Operations

unique

#![allow(unused)]
fn main() {
use rok_utils::arr::unique;

let a = [1, 2, 3, 2, 1];
let uniq = unique(&a);
assert_eq!(uniq, vec![1, 2, 3]);
}

diff

#![allow(unused)]
fn main() {
use rok_utils::arr::diff;

let a = [1, 2, 3, 4];
let b = [2, 4];
let difference = diff(&a, &b);
assert_eq!(difference, vec![1, 3]);
}

intersect

#![allow(unused)]
fn main() {
use rok_utils::arr::intersect;

let a = [1, 2, 3];
let b = [2, 3, 4];
let intersection = intersect(&a, &b);
assert_eq!(intersection, vec![2, 3]);
}

without

#![allow(unused)]
fn main() {
use rok_utils::arr::without;

let numbers = [1, 2, 3, 4, 5];
let filtered = without(&numbers, &[2, 4]);
assert_eq!(filtered, vec![1, 3, 5]);
}

See Also

Error Handling

AdonisJS-inspired typed error system with HTTP status semantics.

RokError Enum

#![allow(unused)]
fn main() {
use rok_utils::RokError;

let err = RokError::NotFound("User #42".into());
assert_eq!(err.code(), "E_NOT_FOUND");
assert_eq!(err.status(), 404);
assert!(err.is_self_handled());
}

Error Codes

CodeStatusDescription
E_NOT_FOUND404Resource not found
E_UNAUTHORIZED401Authentication required
E_FORBIDDEN403Access denied
E_VALIDATION_FAILURE422Input validation failed
E_TOO_MANY_REQUESTS429Rate limit exceeded
E_INTERNAL500Internal server error

Result Extensions

context

Add context to any error:

#![allow(unused)]
fn main() {
use rok_utils::{RokResultExt, RokError};

fn find_user(id: u64) -> Result<String, RokError> {
    if id == 42 {
        Ok("Alice".to_string())
    } else {
        Err(RokError::NotFound(format!("User #{id}")))
    }
}

let result = find_user(42).context("Database query failed");
assert!(result.is_ok());
}

or_not_found

Convert None to NotFound error:

#![allow(unused)]
fn main() {
use rok_utils::{RokResultExt, RokError};

let users = vec!["Alice", "Bob"];
let user = users.get(0).or_not_found("User at index 0");
assert_eq!(user.unwrap(), "Alice");
}

Validation Errors

#![allow(unused)]
fn main() {
use rok_utils::RokError;

fn validate_email(email: &str) -> Result<(), RokError> {
    if email.contains('@') {
        Ok(())
    } else {
        Err(RokError::ValidationFailure {
            field: "email".to_string(),
            reason: "Must contain @".to_string(),
        })
    }
}

let result = validate_email("invalid");
assert!(matches!(result, Err(RokError::ValidationFailure { .. })));
}

Handling Different Errors

#![allow(unused)]
fn main() {
use rok_utils::RokError;

fn handle_error(err: &RokError) -> String {
    match err {
        RokError::NotFound(msg) => format!("404: {}", msg),
        RokError::ValidationFailure { field, reason } => {
            format!("Validation error on {}: {}", field, reason)
        }
        RokError::Unauthorized(msg) => format!("401: {}", msg),
        _ => format!("Error: {}", err.code()),
    }
}
}

See Also

Functional Patterns

Functional programming utilities for composition and lazy evaluation.

pipe

Thread a value through a sequence of functions:

#![allow(unused)]
fn main() {
use rok_utils::fp::pipe;

let result = pipe(5, vec![
    |x| x + 1,
    |x| x * 2,
    |x| x - 3,
]);

// (5 + 1) * 2 - 3 = 9
assert_eq!(result, 9);
}

compose

Create new functions by composing two functions:

#![allow(unused)]
fn main() {
use rok_utils::fp::compose;

let add_then_double = compose(
    |x: i32| x * 2,
    |x: i32| x + 1,
);

assert_eq!(add_then_double(5), 12); // (5 + 1) * 2
}

tap

Execute side effects without changing the value:

#![allow(unused)]
fn main() {
use rok_utils::fp::tap;

let mut log = Vec::new();
let result = tap(42, |v| log.push(*v));

assert_eq!(result, 42);
assert_eq!(log, vec![42]);
}

Lazy

Lazily initialized value:

#![allow(unused)]
fn main() {
use rok_utils::fp::Lazy;

let config = Lazy::new(|| {
    println!("Initializing...");
    "config_value".to_string()
});

println!("Before access");
let value = config.get();
println!("After access: {}", value);
}

memoize

Cache function results:

#![allow(unused)]
fn main() {
use rok_utils::fp::memoize;

let expensive = memoize(|x: i32| {
    println!("Computing...");
    x * x
});

expensive(5); // Computes
expensive(5); // Uses cached result
expensive(3); // Computes new value
}

retry

Retry failed operations:

#![allow(unused)]
fn main() {
use rok_utils::fp::retry;

let mut attempts = 0;
let result = retry(3, || {
    attempts += 1;
    if attempts < 2 {
        Err("failed")
    } else {
        Ok("success")
    }
});

assert_eq!(result.unwrap(), "success");
assert_eq!(attempts, 2);
}

or_default

Get value from Option or default:

#![allow(unused)]
fn main() {
use rok_utils::fp::or_default;

assert_eq!(or_default(Some(42)), 42);
assert_eq!(or_default(None::<i32>), 0);
}

See Also

Data Utilities

Utilities for numbers, dates, IDs, and cryptographic operations.

Numbers

format_number

Format numbers with thousand separators:

#![allow(unused)]
fn main() {
use rok_utils::data::numbers::format_number;

assert_eq!(format_number(1234567.89, 2, ','), "1,234,567.89");
assert_eq!(format_number(1000.00, 0, ','), "1,000");
}

round, ceil, floor

#![allow(unused)]
fn main() {
use rok_utils::data::numbers::{round, ceil, floor};

assert_eq!(round(3.14159, 2), 3.14);
assert_eq!(ceil(3.001, 0), 4.0);
assert_eq!(floor(3.999, 0), 3.0);
}

clamp

Constrain a value within a range:

#![allow(unused)]
fn main() {
use rok_utils::data::numbers::clamp;

assert_eq!(clamp(5.0, 0.0, 10.0), 5.0);
assert_eq!(clamp(-5.0, 0.0, 10.0), 0.0);
assert_eq!(clamp(15.0, 0.0, 10.0), 10.0);
}

Requires the dates feature flag.

Dates

today / now

#![allow(unused)]
fn main() {
use rok_utils::{today, now};

let date = today();
let timestamp = now();
}

add_days / add_hours

#![allow(unused)]
fn main() {
use rok_utils::{add_days, today};

let tomorrow = add_days(&today(), 1);
}

format_date

#![allow(unused)]
fn main() {
use rok_utils::{now, format_date};

let formatted = format_date(&now(), "%Y-%m-%d %H:%M");
}

IDs

uuid_v4

Generate a random UUID:

#![allow(unused)]
fn main() {
use rok_utils::uuid_v4;

let uuid = uuid_v4();
// "550e8400-e29b-41d4-a716-446655440000"
}

uuid_v7

Generate a time-ordered UUID:

#![allow(unused)]
fn main() {
use rok_utils::uuid_v7;

let uuid = uuid_v7();
}

is_uuid

Validate UUID strings:

#![allow(unused)]
fn main() {
use rok_utils::is_uuid;

assert!(is_uuid("550e8400-e29b-41d4-a716-446655440000"));
assert!(!is_uuid("invalid"));
}

Requires the ids feature flag.

Hashing

hash_sha256

#![allow(unused)]
fn main() {
use rok_utils::hash_sha256;

let hash = hash_sha256("password");
// "5e884898da28047d9169e02e..."
}

verify_sha256

#![allow(unused)]
fn main() {
use rok_utils::{hash_sha256, verify_sha256};

let hash = hash_sha256("password");
assert!(verify_sha256("password", &hash));
assert!(!verify_sha256("wrong", &hash));
}

generate_token

Generate a secure random token:

#![allow(unused)]
fn main() {
use rok_utils::generate_token;

let token = generate_token(32);
// 32 bytes as hex string
}

secure_compare

Constant-time comparison (prevents timing attacks):

#![allow(unused)]
fn main() {
use rok_utils::secure_compare;

assert!(secure_compare("secret", "secret"));
assert!(!secure_compare("secret", "Secret"));
}

Requires the crypto feature flag.

Type Guards

Runtime type checking and JSON path utilities.

Requires the json feature flag.

Type Guards

is_string

#![allow(unused)]
fn main() {
use rok_utils::types::is_string;
use serde_json::json;

assert!(is_string(&json!("hello")));
assert!(!is_string(&json!(42)));
}

is_number

#![allow(unused)]
fn main() {
use rok_utils::types::is_number;
use serde_json::json;

assert!(is_number(&json!(42)));
assert!(!is_number(&json!("hello")));
}

is_bool

#![allow(unused)]
fn main() {
use rok_utils::types::is_bool;
use serde_json::json;

assert!(is_bool(&json!(true)));
assert!(!is_bool(&json!(42)));
}

is_array

#![allow(unused)]
fn main() {
use rok_utils::types::is_array;
use serde_json::json;

assert!(is_array(&json!([1, 2, 3])));
assert!(!is_array(&json!({"a": 1})));
}

is_object

#![allow(unused)]
fn main() {
use rok_utils::types::is_object;
use serde_json::json;

assert!(is_object(&json!({"a": 1})));
assert!(!is_object(&json!([1, 2, 3])));
}

is_null

#![allow(unused)]
fn main() {
use rok_utils::types::is_null;
use serde_json::json;

assert!(is_null(&json!(null)));
assert!(!is_null(&json!(42)));
}

Path Access

get_path

Access nested values using dot notation:

#![allow(unused)]
fn main() {
use rok_utils::types::get_path;
use serde_json::json;

let data = json!({
    "user": {
        "address": {
            "city": "New York"
        }
    }
});

assert_eq!(get_path(&data, "user.address.city"), Some(&json!("New York")));
assert_eq!(get_path(&data, "user.missing"), None);
}

set_path

Create nested structure and set value:

#![allow(unused)]
fn main() {
use rok_utils::types::set_path;
use serde_json::json;

let data = json!({});
let data = set_path(data, "user.name", json!("Alice"));

assert_eq!(data["user"]["name"], json!("Alice"));
}

Deep Equality

deep_equal

Compare JSON values recursively:

#![allow(unused)]
fn main() {
use rok_utils::types::deep_equal;
use serde_json::json;

assert!(deep_equal(&json!({"a": 1, "b": 2}), &json!({"b": 2, "a": 1})));
assert!(!deep_equal(&json!({"a": 1}), &json!({"a": 2})));
}

URL Slug Generation

Generate URL-friendly slugs from titles.

Basic Slug

#![allow(unused)]
fn main() {
use rok_utils::slug;

fn basic_slug(title: &str) -> String {
    slug(title)
}

assert_eq!(basic_slug("Hello World!"), "hello-world");
assert_eq!(basic_slug("O'Reilly's Guide"), "oreillys-guide");
}

Custom Slug

#![allow(unused)]
fn main() {
use rok_utils::slug;

fn underscore_slug(title: &str) -> String {
    slug(title, '_')
}

assert_eq!(underscore_slug("Hello World!"), "hello_world");
}

Advanced Slug with Fluent Builder

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

fn advanced_slug(title: &str) -> String {
    Str::of(title)
        .trim()
        .lower()
        .replace("'", "")
        .replace("&", " and ")
        .replace("_", "-")
        .squish()
        .pipe(|s| slug(&s, '-'))
        .value()
}

assert_eq!(advanced_slug("  Hello World & Friends!  "), "hello-world-and-friends");
assert_eq!(advanced_slug("O'Reilly's Guide"), "oreillys-guide");
}

Complete Example

#![allow(unused)]
fn main() {
use rok_utils::str::Str;

fn generate_post_slug(title: &str, id: u64) -> String {
    let slug = Str::of(title)
        .trim()
        .lower()
        .replace("'", "")
        .replace("\"", "")
        .replace(" ", "-")
        .replace("--", "-")
        .when(|s| s.value().ends_with("-"), |s| {
            // Remove trailing dash
            s.pipe(|v| v.trim_end_matches('-').to_string())
        })
        .value();

    format!("{}-{}", id, slug)
}

assert_eq!(generate_post_slug("My Blog Post!", 123), "123-my-blog-post");
}

Input Validation

Validate user input with structured error handling.

Username Validation

#![allow(unused)]
fn main() {
use rok_utils::{RokError, Str};

fn validate_username(username: &str) -> Result<String, RokError> {
    let normalized = Str::of(username)
        .trim()
        .lower()
        .value();

    if normalized.is_empty() {
        return Err(RokError::ValidationFailure {
            field: "username".to_string(),
            reason: "Username cannot be empty".to_string(),
        });
    }

    if normalized.len() < 3 {
        return Err(RokError::ValidationFailure {
            field: "username".to_string(),
            reason: "Username must be at least 3 characters".to_string(),
        });
    }

    if normalized.len() > 20 {
        return Err(RokError::ValidationFailure {
            field: "username".to_string(),
            reason: "Username must be at most 20 characters".to_string(),
        });
    }

    Ok(normalized)
}
}

Email Validation

#![allow(unused)]
fn main() {
use rok_utils::{RokError, Str};

fn validate_email(email: &str) -> Result<String, RokError> {
    let trimmed = Str::of(email).trim().value();

    if trimmed.is_empty() {
        return Err(RokError::ValidationFailure {
            field: "email".to_string(),
            reason: "Email cannot be empty".to_string(),
        });
    }

    if !trimmed.contains('@') {
        return Err(RokError::ValidationFailure {
            field: "email".to_string(),
            reason: "Email must contain @".to_string(),
        });
    }

    let parts: Vec<&str> = trimmed.split('@').collect();
    if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
        return Err(RokError::ValidationFailure {
            field: "email".to_string(),
            reason: "Invalid email format".to_string(),
        });
    }

    Ok(trimmed.to_string())
}
}

Password Validation

#![allow(unused)]
fn main() {
use rok_utils::{RokError, Str};

fn validate_password(password: &str) -> Result<(), RokError> {
    let pwd = Str::of(password);

    if pwd.len() < 8 {
        return Err(RokError::ValidationFailure {
            field: "password".to_string(),
            reason: "Password must be at least 8 characters".to_string(),
        });
    }

    // Check for uppercase
    if !password.chars().any(|c| c.is_uppercase()) {
        return Err(RokError::ValidationFailure {
            field: "password".to_string(),
            reason: "Password must contain at least one uppercase letter".to_string(),
        });
    }

    // Check for lowercase
    if !password.chars().any(|c| c.is_lowercase()) {
        return Err(RokError::ValidationFailure {
            field: "password".to_string(),
            reason: "Password must contain at least one lowercase letter".to_string(),
        });
    }

    // Check for digit
    if !password.chars().any(|c| c.is_ascii_digit()) {
        return Err(RokError::ValidationFailure {
            field: "password".to_string(),
            reason: "Password must contain at least one digit".to_string(),
        });
    }

    Ok(())
}
}

Batch Validation

#![allow(unused)]
fn main() {
use rok_utils::{RokError, Str};

#[derive(Debug)]
struct ValidationResult {
    valid: bool,
    errors: Vec<RokError>,
}

fn validate_registration(
    username: &str,
    email: &str,
    password: &str,
) -> ValidationResult {
    let mut errors = Vec::new();

    if let Err(e) = validate_username(username) {
        errors.push(e);
    }

    if let Err(e) = validate_email(email) {
        errors.push(e);
    }

    if let Err(e) = validate_password(password) {
        errors.push(e);
    }

    ValidationResult {
        valid: errors.is_empty(),
        errors,
    }
}
}

API Responses

Build consistent API responses with rok-utils.

Basic Response

#![allow(unused)]
fn main() {
use serde::Serialize;

#[derive(Debug, Serialize)]
struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    error: Option<String>,
}

impl<T> ApiResponse<T> {
    fn ok(data: T) -> Self {
        Self {
            success: true,
            data: Some(data),
            error: None,
        }
    }

    fn err(message: &str) -> Self {
        Self {
            success: false,
            data: None,
            error: Some(message.to_string()),
        }
    }
}
}

JSON API Response

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use serde_json::json;
use rok_utils::str::Str;

#[derive(Debug, Serialize)]
struct ApiResponse {
    success: bool,
    message: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    data: Option<serde_json::Value>,
}

impl ApiResponse {
    fn success(message: &str) -> Self {
        Self {
            success: true,
            message: Some(message.to_string()),
            data: None,
        }
    }

    fn success_with_data(data: serde_json::Value) -> Self {
        Self {
            success: true,
            message: None,
            data: Some(data),
        }
    }

    fn error(message: &str) -> Self {
        Self {
            success: false,
            message: Some(message.to_string()),
            data: None,
        }
    }
}
}

Using with RokError

#![allow(unused)]
fn main() {
use serde::Serialize;
use rok_utils::{RokError, RokResultExt, Str};

#[derive(Debug, Serialize)]
struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    error: Option<ErrorDetail>,
}

#[derive(Debug, Serialize)]
struct ErrorDetail {
    code: String,
    message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    field: Option<String>,
}

impl ApiResponse<String> {
    fn from_result<T: Serialize>(result: Result<T, RokError>) -> Self {
        match result {
            Ok(data) => Self {
                success: true,
                data: Some(serde_json::to_value(data).unwrap()),
                error: None,
            },
            Err(err) => Self {
                success: false,
                data: None,
                error: Some(ErrorDetail {
                    code: err.code().to_string(),
                    message: err.to_string(),
                    field: None,
                }),
            },
        }
    }
}
}

Example Usage

use serde_json::json;

fn main() {
    // Success response
    let users = json!([{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]);
    let response = ApiResponse::success_with_data(users);
    println!("{}", serde_json::to_string_pretty(&response).unwrap());

    // Error response
    let err = RokError::ValidationFailure {
        field: "email".to_string(),
        reason: "Invalid format".to_string(),
    };
    let response = ApiResponse::<serde_json::Value>::from_result(Err(err));
    println!("{}", serde_json::to_string_pretty(&response).unwrap());
}

Error Handling Patterns

Common patterns for error handling with rok-utils.

Propagating Errors

#![allow(unused)]
fn main() {
use rok_utils::{RokError, RokResultExt};

fn read_config(path: &str) -> Result<String, RokError> {
    std::fs::read_to_string(path)
        .context("Failed to read config file")
        .map_err(|e| RokError::Wrapped {
            message: "Configuration error".to_string(),
            source: Box::new(e),
        })
}
}

Fallback Values

#![allow(unused)]
fn main() {
use rok_utils::{RokError, RokResultExt};

fn get_setting(key: &str) -> Result<String, RokError> {
    // Try to get from config
    Err(RokError::NotFound(key.to_string()))
}

fn get_with_default(key: &str, default: &str) -> String {
    get_setting(key)
        .map_err(|_| {
            // Log warning but don't fail
            eprintln!("Setting {} not found, using default", key);
        })
        .unwrap_or_else(|_| default.to_string())
}

assert_eq!(get_with_default("app_name", "MyApp"), "MyApp");
}

Mapping Errors

#![allow(unused)]
fn main() {
use rok_utils::{RokError, RokResultExt};

fn parse_number(input: &str) -> Result<i32, RokError> {
    input
        .trim()
        .parse::<i32>()
        .map_err(|_| RokError::ValidationFailure {
            field: "number".to_string(),
            reason: format!("'{}' is not a valid number", input),
        })
}
}

Collecting Errors

#![allow(unused)]
fn main() {
use rok_utils::RokError;
use std::collections::VecDeque;

#[derive(Debug)]
struct ValidationErrors {
    errors: VecDeque<RokError>,
}

impl ValidationErrors {
    fn new() -> Self {
        Self {
            errors: VecDeque::new(),
        }
    }

    fn add(&mut self, field: &str, reason: &str) {
        self.errors.push_back(RokError::ValidationFailure {
            field: field.to_string(),
            reason: reason.to_string(),
        });
    }

    fn is_empty(&self) -> bool {
        self.errors.is_empty()
    }

    fn into_result<T>(self, value: T) -> Result<T, RokError> {
        if let Some(first) = self.errors.into_iter().next() {
            Err(first)
        } else {
            Ok(value)
        }
    }
}

fn validate_form(name: &str, email: &str) -> Result<(String, String), RokError> {
    let mut errors = ValidationErrors::new();

    if name.trim().is_empty() {
        errors.add("name", "Name is required");
    }

    if !email.contains('@') {
        errors.add("email", "Invalid email format");
    }

    errors.into_result((name.to_string(), email.to_string()))
}
}

Retry with Backoff

#![allow(unused)]
fn main() {
use rok_utils::{RokError, fp::retry};
use std::time::Duration;

fn fetch_with_retry(url: &str, max_attempts: usize) -> Result<String, RokError> {
    let mut last_error = None;

    for attempt in 0..max_attempts {
        match fetch_url(url) {
            Ok(response) => return Ok(response),
            Err(e) => {
                last_error = Some(e);
                if attempt < max_attempts - 1 {
                    std::thread::sleep(Duration::from_millis(100 * (attempt + 1) as u64));
                }
            }
        }
    }

    Err(last_error.unwrap_or(RokError::Internal("Unknown error".to_string())))
}

fn fetch_url(_url: &str) -> Result<String, RokError> {
    // Simulated fetch
    Err(RokError::Internal("Not implemented".to_string()))
}
}

Contextual Errors

#![allow(unused)]
fn main() {
use rok_utils::{RokError, RokResultExt};

fn process_user(id: u64) -> Result<User, RokError> {
    let user = find_user(id)
        .context(format!("Looking up user #{}", id))?
        .or_not_found(&format!("User #{}", id))?;

    let profile = load_profile(&user)
        .context(format!("Loading profile for user #{}", id))?;

    Ok(User { profile, ..user })
}
}

Feature Flags

rok-utils uses feature flags to control which dependencies are included.

Default Features

By default, rok-utils has no optional dependencies enabled.

[dependencies]
rok-utils = "0.2"

This includes:

  • String utilities (case conversion, truncate, slug, etc.)
  • Fluent builder (Str::of())
  • Array utilities (map, filter, reduce, etc.)
  • Error handling (RokError)
  • Functional patterns (pipe, compose, tap)
  • Path utilities

Optional Features

dates

Enables date/time utilities using chrono.

rok-utils = { version = "0.2", features = ["dates"] }

Added functions:

  • now(), today(), yesterday(), tomorrow()
  • add_days(), add_hours()
  • format_date(), parse_date()
  • diff_days()

crypto

Enables hashing and token generation.

rok-utils = { version = "0.2", features = ["crypto"] }

Added functions:

  • hash_sha256(), verify_sha256()
  • generate_token()
  • secure_compare()

ids

Enables UUID and ULID generation.

rok-utils = { version = "0.2", features = ["ids"] }

Added functions:

  • uuid_v4(), uuid_v7(), ulid()
  • is_uuid(), is_ulid()

json

Enables JSON type guards and path utilities.

rok-utils = { version = "0.2", features = ["json"] }

Added functions:

  • is_string(), is_number(), is_bool(), etc.
  • get_path(), set_path()
  • deep_equal()

random

Enables random string generation.

rok-utils = { version = "0.2", features = ["random"] }

Added functions:

  • random()
  • password()
  • shuffle()

unicode

Enables Unicode-aware string operations.

rok-utils = { version = "0.2", features = ["unicode"] }

Added functions:

  • pluralize(), singular()
  • is_plural(), is_singular()

All Features

Enable all optional features:

rok-utils = { version = "0.2", features = ["full"] }

Or explicitly:

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

Migration Guide

From 0.x to 1.0

Version 1.0 freezes the public API and introduces some breaking changes.

1. RokError is non_exhaustive

You can no longer exhaustively match on RokError:

#![allow(unused)]
fn main() {
// 0.x
match err {
    RokError::NotFound(_) => ...,
    RokError::InvalidJson(_) => ...,
    // ... all variants
}

// 1.0 (requires wildcard arm)
match err {
    RokError::NotFound(_) => ...,
    RokError::InvalidJson(_) => ...,
    _ => ..., // Required
}
}

2. Lazy API Changes

The Lazy struct now uses once_cell::sync::Lazy internally:

#![allow(unused)]
fn main() {
// 0.x
let lazy = Lazy::new(|| 42);
assert!(lazy.is_initialized()); // removed

// 1.0
let lazy: Lazy<i32, fn() -> i32> = Lazy::new(|| 42);
let value = *lazy.get(); // returns &T instead of T
}

3. MSRV Bump

Minimum Supported Rust Version is now 1.92.

Update Error Handling

Add wildcard arm to prevent future breakage:

#![allow(unused)]
fn main() {
match error {
    RokError::NotFound(msg) => handle_not_found(msg),
    RokError::ValidationFailure { field, reason } => handle_validation(field, reason),
    _ => handle_internal_error(error),
}
}

Update Rust Toolchain

rustup update
rustup default stable

Contributing

Thank you for your interest in contributing to rok-utils!

Getting Started

  1. Fork the repository
  2. Clone your fork: git clone https://github.com/YOUR_USERNAME/rok-utils.git
  3. Add upstream: git remote add upstream https://github.com/ateeq1999/rok-utils.git
  4. Install Rust (latest stable)
  5. Run tests: cargo test --all-features

Development Workflow

1. Create a Branch

git checkout -b feature/your-feature-name

2. Make Changes

Follow the coding standards:

  • Run cargo fmt before committing
  • Run cargo clippy --all-features -- -D warnings
  • Add tests for new functionality
  • Keep files under 400 lines

3. Test Your Changes

cargo test --all-features
cargo test --no-default-features
cargo fmt --check
cargo clippy --all-features -- -D warnings
cargo test --doc

4. Commit and Push

git add .
git commit -m "feat: add new feature"
git push origin feature/your-feature-name

5. Open a Pull Request

Open a PR against main with a clear description.

Coding Standards

Error Handling

  • Never use unwrap() in library code
  • Return Result<T, RokError> or Option<T>
  • Use context() for error context

Naming Conventions

  • Functions: snake_case
  • Types: PascalCase
  • Traits: PascalCase

Documentation

  • Document all public items
  • Include examples for public functions
  • Update this documentation site

Reporting Issues

  • Use GitHub Issues for bugs and features
  • Include minimal reproduction steps
  • Specify Rust version and features used

License

By contributing, you agree that your contributions will be licensed under the MIT License.