utilz_rs/
lib.rs

1//! # Utilz
2//!
3//! **Ergonomic, zero-dependency utility traits for Rust.**
4//!
5//! This crate provides a collection of extension traits for common Rust types
6//! like `Option`, `Result`, `Vec`, `bool`, `&str`, `HashMap`, and more — to make your
7//! code cleaner, shorter, and more expressive without relying on macros or external dependencies.
8//!
9//! ---
10//!
11//! ## Highlights
12//!
13//! - **`OptionUtils`** – Convenient extensions for `Option<T>`
14//!   - `.if_some()`, `.or_default_with()`
15//!
16//! - **`ResultUtils`** – Cleaner result handling
17//!   - `.if_ok()`, `.if_err()`, `.unwrap_or_exit()`
18//!
19//! - **`BoolUtils`** – Fancy conditionals for `bool`
20//!   - `.toggle()`, `.not()`, `.then_val()`, `.if_true()`, `.if_false()`
21//!
22//! - **`VecUtils`** – Push conditionally into vectors
23//!   - `.push_if()`, `.push_if_with()`
24//!
25//! - **`StrUtils`** – String slice helpers
26//!   - `.contains_all()`, `.contains_any()`, `.to_title_case()`
27//!
28//! - **`MapUtils`** – Extensions for `HashMap`
29//!   - `.insert_if()`, `.get_or()`
30//!
31//! - **`MemUtils`** – Reflection-like helpers
32//!   - `.type_name()`, `.mem_size()`, `.view()`
33//!
34//! - **`IdentityUtils`** – Chainable `tap()` for debug or logging
35//!
36//! - **`PanicUtils`** – Exit on `None` or `Err` with a message
37//!   - `.unwrap_or_exit()`
38//!
39//! - **`DurationUtils`** – Time formatting helpers
40//!   - `.pretty()` → `"1h 2m 3s"`
41//!
42//! - **`ConvertUtils`** – Ergonomic `TryFrom` helpers
43//!   - `.to()`, `.to_or()`, `.to_result()`
44//!
45//! - **`ClampUtils`** – Limit a number to a range
46//!   - `.clamp_to(min, max)`
47//!
48//! - **`NumberUtils`** – Integer property checks
49//!   - `.is_even()`, `.is_odd()`
50//!
51//! - **`IteratorUtils`** – Iterator fallback logic
52//!   - `.find_map_or(f, fallback)`
53//!
54//! ---
55//!
56//! ## Quick Example
57//!
58//! ```rust
59//! use rust_utils_plus::*;
60//!
61//! let value = Some("hi");
62//! value.if_some(|v| println!("Got: {v}"));
63//!
64//! let mut enabled = true;
65//! enabled.toggle();
66//!
67//! let name = "hello world";
68//! assert!(name.contains_all(["hello", "world"]));
69//!
70//! let duration = std::time::Duration::from_secs(3666);
71//! println!("{}", duration.pretty()); // "1h 1m 6s"
72//! ```
73//!
74//! ---
75//!
76//! ## Philosophy
77//!
78//! - 🔧 100% Rust standard library
79//! - 🚫 No dependencies
80//! - ✅ Only opt-in trait imports
81//!
82//! _Use what you want, ignore the rest._
83
84use std::{any::type_name, collections::HashMap, hash::Hash, time::Duration};
85
86/// Provides sugar methods for comparing values.
87pub trait EqUtils<T: PartialEq> {
88    /// Returns true if `self == other`.
89    fn eq_to(&self, other: &T) -> bool;
90
91    /// Returns true if `self != other`.
92    fn not_eq_to(&self, other: &T) -> bool;
93}
94
95impl<T: PartialEq> EqUtils<T> for T {
96    fn eq_to(&self, other: &T) -> bool {
97        self == other
98    }
99    fn not_eq_to(&self, other: &T) -> bool {
100        self != other
101    }
102}
103
104/// Extension methods for `Option<T>`.
105pub trait OptionUtils<T> {
106    /// Returns the value inside `Some`, or the fallback if `None`.
107    fn or_default_with(self, fallback: T) -> T;
108
109    /// Executes a closure if the `Option` is `Some`.
110    ///
111    /// Returns the same `Option` back.
112    #[must_use]
113    fn if_some<F: FnOnce(&T)>(self, f: F) -> Option<T>;
114}
115
116impl<T> OptionUtils<T> for Option<T> {
117    fn or_default_with(self, fallback: T) -> T {
118        self.unwrap_or(fallback)
119    }
120
121    fn if_some<F: FnOnce(&T)>(self, f: F) -> Option<T> {
122        if let Some(ref val) = self {
123            f(val);
124        }
125        self
126    }
127}
128
129/// Extra methods for string slices (`&str`).
130pub trait StrUtils {
131    /// Returns `true` if all strings in the iterator exist in the main string.
132    fn contains_all<'a, I>(&self, parts: I) -> bool
133    where
134        I: IntoIterator<Item = &'a str>;
135
136    /// Returns `true` if any of the strings in the iterator exist in the main string.
137    fn contains_any<'a, I>(&self, parts: I) -> bool
138    where
139        I: IntoIterator<Item = &'a str>;
140
141    /// Returns a new string with the first letter capitalized.
142    fn to_title_case(&self) -> String;
143}
144
145impl StrUtils for str {
146    fn contains_all<'a, I>(&self, parts: I) -> bool
147    where
148        I: IntoIterator<Item = &'a str>,
149    {
150        parts.into_iter().all(|part| self.contains(part))
151    }
152    fn contains_any<'a, I>(&self, parts: I) -> bool
153    where
154        I: IntoIterator<Item = &'a str>,
155    {
156        parts.into_iter().any(|part| self.contains(part))
157    }
158    fn to_title_case(&self) -> String {
159        let mut chars = self.chars();
160        match chars.next() {
161            None => String::new(),
162            Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
163        }
164    }
165}
166
167/// Reflection helpers: type name and memory size.
168pub trait MemUtils {
169    /// Returns the type name of `self`.
170    fn type_name(&self) -> &'static str;
171
172    /// Returns memory size in bytes of the type.
173    fn mem_size(&self) -> usize;
174
175    /// Prints type and memory size to stdout.
176    fn view(&self);
177}
178
179impl<T> MemUtils for T {
180    fn type_name(&self) -> &'static str {
181        type_name::<T>()
182    }
183    fn mem_size(&self) -> usize {
184        std::mem::size_of::<T>()
185    }
186    fn view(&self) {
187        println!(
188            "[view] Type: {}, Size: {} bytes",
189            self.type_name(),
190            self.mem_size()
191        );
192    }
193}
194
195pub trait ConvertUtils: Sized {
196    fn to<T: TryFrom<Self>>(self) -> Option<T>;
197    fn to_or<T: TryFrom<Self>>(self, fallback: T) -> T;
198    fn to_result<T: TryFrom<Self>>(self) -> Result<T, T::Error>;
199}
200
201impl<T> ConvertUtils for T {
202    fn to<U: TryFrom<T>>(self) -> Option<U> {
203        U::try_from(self).ok()
204    }
205
206    fn to_or<U: TryFrom<T>>(self, fallback: U) -> U {
207        self.to().unwrap_or(fallback)
208    }
209
210    fn to_result<U: TryFrom<T>>(self) -> Result<U, U::Error> {
211        U::try_from(self)
212    }
213}
214
215pub trait BoolUtils {
216    #[must_use]
217    fn not(&self) -> bool;
218    fn then_val<T>(&self, val: T) -> Option<T>;
219    fn if_true<T, F: FnOnce() -> T>(&self, f: F) -> Option<T>;
220    fn if_false<T, F: FnOnce() -> T>(&self, f: F) -> Option<T>;
221    fn toggle(&mut self);
222}
223
224impl BoolUtils for bool {
225    fn not(&self) -> bool {
226        !self
227    }
228    fn then_val<T>(&self, val: T) -> Option<T> {
229        if *self { Some(val) } else { None }
230    }
231    fn if_true<T, F: FnOnce() -> T>(&self, f: F) -> Option<T> {
232        if *self { Some(f()) } else { None }
233    }
234    fn if_false<T, F: FnOnce() -> T>(&self, f: F) -> Option<T> {
235        if self.not() { Some(f()) } else { None }
236    }
237    fn toggle(&mut self) {
238        *self = !*self;
239    }
240}
241
242/// Conditional vector push helpers.
243pub trait VecUtils<T> {
244    /// Pushes the value if `cond` is `true`.
245    fn push_if(&mut self, push: T, cond: bool);
246
247    /// Lazily evaluates and pushes the value if `cond` is `true`.
248    fn push_if_with<F: FnOnce() -> T>(&mut self, cond: bool, f: F);
249}
250
251impl<T> VecUtils<T> for Vec<T> {
252    fn push_if(&mut self, push: T, cond: bool) {
253        if cond {
254            self.push(push);
255        }
256    }
257    fn push_if_with<F: FnOnce() -> T>(&mut self, cond: bool, f: F) {
258        if cond {
259            self.push(f());
260        }
261    }
262}
263
264pub trait MapUtils<K, V> {
265    fn get_or<'a>(&'a self, key: &K, fallback: &'a V) -> &'a V;
266    fn insert_if(&mut self, key: K, value: V, cond: bool);
267}
268
269impl<K: Eq + Hash, V> MapUtils<K, V> for HashMap<K, V> {
270    fn get_or<'a>(&'a self, key: &K, fallback: &'a V) -> &'a V {
271        self.get(key).unwrap_or(fallback)
272    }
273
274    fn insert_if(&mut self, key: K, value: V, cond: bool) {
275        if cond {
276            self.insert(key, value);
277        }
278    }
279}
280
281pub trait ResultUtils<T, E> {
282    fn if_ok<F: FnOnce(&T)>(self, f: F) -> Self;
283    fn if_err<F: FnOnce(&E)>(self, f: F) -> Self;
284}
285
286impl<T, E: std::fmt::Debug> ResultUtils<T, E> for Result<T, E> {
287    fn if_ok<F: FnOnce(&T)>(self, f: F) -> Self {
288        if let Ok(ref val) = self {
289            f(val);
290        }
291        self
292    }
293
294    fn if_err<F: FnOnce(&E)>(self, f: F) -> Self {
295        if let Err(ref err) = self {
296            f(err);
297        }
298        self
299    }
300}
301
302/// Pretty-formatting for `Duration`.
303pub trait DurationUtils {
304    /// Returns a formatted string like `"1h 20m 5s"`.
305    fn pretty(&self) -> String;
306}
307
308impl DurationUtils for Duration {
309    fn pretty(&self) -> String {
310        let total_secs = self.as_secs();
311        let hours = total_secs / 3600;
312        let mins = (total_secs % 3600) / 60;
313        let secs = total_secs % 60;
314        format!("{}h {}m {}s", hours, mins, secs)
315    }
316}
317
318pub trait IteratorUtils: Iterator + Sized {
319    fn find_map_or<T, F: FnMut(Self::Item) -> Option<T>>(self, f: F, fallback: T) -> T;
320}
321
322impl<I: Iterator> IteratorUtils for I {
323    fn find_map_or<T, F: FnMut(Self::Item) -> Option<T>>(mut self, f: F, fallback: T) -> T {
324        self.find_map(f).unwrap_or(fallback)
325    }
326}
327
328pub trait IdentityUtils: Sized {
329    fn tap<F: FnOnce(&Self)>(self, f: F) -> Self;
330}
331
332impl<T> IdentityUtils for T {
333    fn tap<F: FnOnce(&Self)>(self, f: F) -> Self {
334        f(&self);
335        self
336    }
337}
338
339/// Helpers to panic or exit cleanly with messages.
340pub trait PanicUtils<T> {
341    /// If `None` or `Err`, logs the message and exits the program.
342    fn unwrap_or_exit(self, msg: &str) -> T;
343}
344
345impl<T> PanicUtils<T> for Option<T> {
346    fn unwrap_or_exit(self, msg: &str) -> T {
347        self.unwrap_or_else(|| {
348            eprintln!("[FATAL]: {}", msg);
349            std::process::exit(1);
350        })
351    }
352}
353
354impl<T, U> PanicUtils<T> for Result<T, U> {
355    fn unwrap_or_exit(self, msg: &str) -> T {
356        self.unwrap_or_else(|_| {
357            eprintln!("[FATAL]: {}", msg);
358            std::process::exit(1);
359        })
360    }
361}
362
363pub trait ClampUtils {
364    fn clamp_to(self, min: Self, max: Self) -> Self;
365}
366impl ClampUtils for i32 {
367    fn clamp_to(self, min: Self, max: Self) -> Self {
368        self.max(min).min(max)
369    }
370}
371
372pub trait NumberUtils {
373    #[must_use]
374    fn is_even(&self) -> bool;
375    #[must_use]
376    fn is_odd(&self) -> bool;
377}
378
379impl NumberUtils for i32 {
380    fn is_even(&self) -> bool {
381        self % 2 == 0
382    }
383    fn is_odd(&self) -> bool {
384        self % 2 != 0
385    }
386}