rok-utils
A Laravel/AdonisJS-inspired utility crate for Rust with zero-bloat, ergonomic helpers.
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?
| Feature | Description |
|---|---|
| Battle-tested APIs | Inspired by Laravel’s Str and AdonisJS’s string helpers |
| Ergonomic | Fluent Str::of() builder for chainable transformations |
| Error-first | AdonisJS-style typed error codes with HTTP status |
| Zero-bloat | Feature flags ensure consumers pay only for what they use |
| UTF-8 native | All string operations use chars(), never raw bytes |
Next Steps
- Installation — Add to your project
- Getting Started — Quick tutorial
- String Utilities — Explore string functions
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
- String Utilities — Explore all string functions
- Fluent Builder — Deep dive into chainable API
- Recipes — Common patterns and solutions
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
| Feature | Description | Extra Dependencies |
|---|---|---|
dates | Date/time utilities | chrono |
crypto | Hashing and token generation | sha2, md-5, subtle |
ids | UUID and ULID generation | uuid, rand |
json | JSON type guards and path access | serde, serde_json |
random | Random string generation | rand, regex |
full | Enable all features | all 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
unicodefeature flag.
See Also
- Fluent Builder — Chainable string transformations
- Recipes: URL Slug Generation
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
- String Utilities — All available string functions
- Recipes: Validation
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
| Code | Status | Description |
|---|---|---|
E_NOT_FOUND | 404 | Resource not found |
E_UNAUTHORIZED | 401 | Authentication required |
E_FORBIDDEN | 403 | Access denied |
E_VALIDATION_FAILURE | 422 | Input validation failed |
E_TOO_MANY_REQUESTS | 429 | Rate limit exceeded |
E_INTERNAL | 500 | Internal 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
datesfeature 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
idsfeature 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
cryptofeature flag.
Type Guards
Runtime type checking and JSON path utilities.
Requires the
jsonfeature 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.
Recommended Updates
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
- Fork the repository
- Clone your fork:
git clone https://github.com/YOUR_USERNAME/rok-utils.git - Add upstream:
git remote add upstream https://github.com/ateeq1999/rok-utils.git - Install Rust (latest stable)
- 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 fmtbefore 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>orOption<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.