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//! - **`Log`** – In-memory logger with filtering support
14//!   - `.log_info()`, `.log_warn()`, `.print_logs()`, `.set_up_logger()`, `.clear()`...
15//!
16//! - **`OptionUtils`** – Convenient extensions for `Option<T>`
17//!   - `.if_some()`, `.or_default_with()`
18//!
19//! - **`ResultUtils`** – Cleaner result handling
20//!   - `.if_ok()`, `.if_err()`, `.unwrap_or_exit()`
21//!
22//! - **`BoolUtils`** – Fancy conditionals for `bool`
23//!   - `.toggle()`, `.not()`, `.then_val()`, `.if_true()`, `.if_false()`
24//!
25//! - **`VecUtils`** – Push conditionally into vectors
26//!   - `.push_if()`, `.push_if_with()`
27//!
28//! - **`StrUtils`** – String slice helpers
29//!   - `.contains_all()`, `.contains_any()`, `.to_title_case()`
30//!
31//! - **`MapUtils`** – Extensions for `HashMap`
32//!   - `.insert_if()`, `.get_or()`
33//!
34//! - **`MemUtils`** – Reflection-like helpers
35//!   - `.type_name()`, `.mem_size()`, `.view()`
36//!
37//! - **`IdentityUtils`** – Chainable `tap()` for debug or logging
38//!
39//! - **`PanicUtils`** – Exit on `None` or `Err` with a message
40//!   - `.unwrap_or_exit()`
41//!
42//! - **`DurationUtils`** – Time formatting helpers
43//!   - `.pretty()` → `"1h 2m 3s"`
44//!
45//! - **`ConvertUtils`** – Ergonomic `TryFrom` helpers
46//!   - `.to()`, `.to_or()`, `.to_result()`
47//!
48//! - **`ClampUtils`** – Limit a number to a range
49//!   - `.clamp_to(min, max)`
50//!
51//! - **`NumberUtils`** – Integer property checks
52//!   - `.is_even()`, `.is_odd()`
53//!
54//! - **`IteratorUtils`** – Iterator fallback logic
55//!   - `.find_map_or(f, fallback)`
56//!
57//! ---
58//!
59//! ## Quick Example
60//!
61//! ```rust
62//! use utilz_rs::*;
63//!
64//! let value = Some("hi");
65//! value.if_some(|v| println!("Got: {v}"));
66//!
67//! let mut enabled = true;
68//! enabled.toggle();
69//!
70//! let name = "hello world";
71//! assert!(name.contains_all(["hello", "world"]));
72//!
73//! let duration = std::time::Duration::from_secs(3666);
74//! println!("{}", duration.pretty()); // "1h 1m 6s"
75//! ```
76//!
77//! ---
78//!
79//! ## Philosophy
80//!
81//! - 🔧 100% Rust standard library
82//! - 🚫 No dependencies
83//! - ✅ Only opt-in trait imports
84//!
85//! _Use what you want, ignore the rest._
86
87use std::{
88    any::type_name,
89    collections::HashMap,
90    hash::Hash,
91    sync::RwLock,
92    time::{Duration, SystemTime, UNIX_EPOCH},
93};
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum LogLevel {
97    Error,
98    Debug,
99    Info,
100    Warn,
101}
102
103struct Logger {
104    message: String,
105    level: LogLevel,
106    time: SystemTime,
107}
108
109pub struct Log;
110
111static LOGS: RwLock<Vec<Logger>> = RwLock::new(Vec::new());
112static LOG_LEVEL: RwLock<LogLevel> = RwLock::new(LogLevel::Debug);
113
114fn get_level() -> LogLevel {
115    *LOG_LEVEL.read().unwrap()
116}
117
118fn level_priority(level: LogLevel) -> u8 {
119    match level {
120        LogLevel::Error => 1,
121        LogLevel::Warn => 2,
122        LogLevel::Info => 3,
123        LogLevel::Debug => 4,
124    }
125}
126
127impl Log {
128    pub fn set_up_logger(level: LogLevel) {
129        *LOG_LEVEL.write().unwrap() = level;
130    }
131
132    pub fn log_with_level(level: LogLevel, message: &str) {
133        if level_priority(level) <= level_priority(get_level()) {
134            let log = Logger {
135                message: message.to_string(),
136                level,
137                time: SystemTime::now(),
138            };
139
140            LOGS.write().unwrap().push(log);
141        }
142    }
143
144    pub fn log(message: &str) {
145        Self::log_with_level(get_level(), message);
146    }
147
148    pub fn log_info(message: &str) {
149        Self::log_with_level(LogLevel::Info, message);
150    }
151
152    pub fn log_debug(message: &str) {
153        Self::log_with_level(LogLevel::Debug, message);
154    }
155
156    pub fn log_error(message: &str) {
157        Self::log_with_level(LogLevel::Error, message);
158    }
159
160    pub fn log_warn(message: &str) {
161        Self::log_with_level(LogLevel::Warn, message);
162    }
163
164    pub fn get_logs() -> Vec<String> {
165        LOGS.read()
166            .unwrap()
167            .iter()
168            .map(|log| {
169                let since_unix = log
170                    .time
171                    .duration_since(UNIX_EPOCH)
172                    .unwrap_or(Duration::from_secs(0));
173                format!(
174                    "[{:?}] @ {}s → {}",
175                    log.level,
176                    since_unix.as_secs(),
177                    log.message
178                )
179            })
180            .collect()
181    }
182
183    pub fn print_logs() {
184        for log in Self::get_logs() {
185            println!("{}", log);
186        }
187    }
188
189    pub fn clear() {
190        LOGS.write().unwrap().clear();
191    }
192}
193
194pub trait Loggable {
195    fn log(self);
196    fn log_info(self);
197    fn log_error(self);
198    fn log_debug(self);
199}
200
201impl Loggable for &str {
202    fn log(self) {
203        Log::log(self);
204    }
205    fn log_info(self) {
206        Log::log_info(self);
207    }
208    fn log_error(self) {
209        Log::log_error(self);
210    }
211    fn log_debug(self) {
212        Log::log_debug(self);
213    }
214}
215
216impl Loggable for String {
217    fn log(self) {
218        self.as_str().log()
219    }
220    fn log_info(self) {
221        self.as_str().log_info()
222    }
223    fn log_error(self) {
224        self.as_str().log_error()
225    }
226    fn log_debug(self) {
227        self.as_str().log_debug()
228    }
229}
230
231/// Provides sugar methods for comparing values.
232pub trait EqUtils<T: PartialEq> {
233    /// Returns true if `self == other`.
234    fn eq_to(&self, other: &T) -> bool;
235
236    /// Returns true if `self != other`.
237    fn not_eq_to(&self, other: &T) -> bool;
238}
239
240impl<T: PartialEq> EqUtils<T> for T {
241    fn eq_to(&self, other: &T) -> bool {
242        self == other
243    }
244    fn not_eq_to(&self, other: &T) -> bool {
245        self != other
246    }
247}
248
249/// Extension methods for `Option<T>`.
250pub trait OptionUtils<T> {
251    /// Returns the value inside `Some`, or the fallback if `None`.
252    fn or_default_with(self, fallback: T) -> T;
253
254    /// Executes a closure if the `Option` is `Some`.
255    ///
256    /// Returns the same `Option` back.
257    #[must_use]
258    fn if_some<F: FnOnce(&T)>(self, f: F) -> Option<T>;
259}
260
261impl<T> OptionUtils<T> for Option<T> {
262    fn or_default_with(self, fallback: T) -> T {
263        self.unwrap_or(fallback)
264    }
265
266    fn if_some<F: FnOnce(&T)>(self, f: F) -> Option<T> {
267        if let Some(ref val) = self {
268            f(val);
269        }
270        self
271    }
272}
273
274/// Extra methods for string slices (`&str`).
275pub trait StrUtils {
276    /// Returns `true` if all strings in the iterator exist in the main string.
277    fn contains_all<'a, I>(&self, parts: I) -> bool
278    where
279        I: IntoIterator<Item = &'a str>;
280
281    /// Returns `true` if any of the strings in the iterator exist in the main string.
282    fn contains_any<'a, I>(&self, parts: I) -> bool
283    where
284        I: IntoIterator<Item = &'a str>;
285
286    /// Returns a new string with the first letter capitalized.
287    fn to_title_case(&self) -> String;
288}
289
290impl StrUtils for str {
291    fn contains_all<'a, I>(&self, parts: I) -> bool
292    where
293        I: IntoIterator<Item = &'a str>,
294    {
295        parts.into_iter().all(|part| self.contains(part))
296    }
297    fn contains_any<'a, I>(&self, parts: I) -> bool
298    where
299        I: IntoIterator<Item = &'a str>,
300    {
301        parts.into_iter().any(|part| self.contains(part))
302    }
303    fn to_title_case(&self) -> String {
304        let mut chars = self.chars();
305        match chars.next() {
306            None => String::new(),
307            Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
308        }
309    }
310}
311
312/// Reflection helpers: type name and memory size.
313pub trait MemUtils {
314    /// Returns the type name of `self`.
315    fn type_name(&self) -> &'static str;
316
317    /// Returns memory size in bytes of the type.
318    fn mem_size(&self) -> usize;
319
320    /// Prints type and memory size to stdout.
321    fn view(&self);
322}
323
324impl<T> MemUtils for T {
325    fn type_name(&self) -> &'static str {
326        type_name::<T>()
327    }
328    fn mem_size(&self) -> usize {
329        std::mem::size_of::<T>()
330    }
331    fn view(&self) {
332        println!(
333            "[view] Type: {}, Size: {} bytes",
334            self.type_name(),
335            self.mem_size()
336        );
337    }
338}
339
340pub trait ConvertUtils: Sized {
341    fn to<T: TryFrom<Self>>(self) -> Option<T>;
342    fn to_or<T: TryFrom<Self>>(self, fallback: T) -> T;
343    fn to_result<T: TryFrom<Self>>(self) -> Result<T, T::Error>;
344}
345
346impl<T> ConvertUtils for T {
347    fn to<U: TryFrom<T>>(self) -> Option<U> {
348        U::try_from(self).ok()
349    }
350
351    fn to_or<U: TryFrom<T>>(self, fallback: U) -> U {
352        self.to().unwrap_or(fallback)
353    }
354
355    fn to_result<U: TryFrom<T>>(self) -> Result<U, U::Error> {
356        U::try_from(self)
357    }
358}
359
360pub trait BoolUtils {
361    #[must_use]
362    fn not(&self) -> bool;
363    fn then_val<T>(&self, val: T) -> Option<T>;
364    fn if_true<T, F: FnOnce() -> T>(&self, f: F) -> Option<T>;
365    fn if_false<T, F: FnOnce() -> T>(&self, f: F) -> Option<T>;
366    fn toggle(&mut self);
367}
368
369impl BoolUtils for bool {
370    fn not(&self) -> bool {
371        !self
372    }
373    fn then_val<T>(&self, val: T) -> Option<T> {
374        if *self { Some(val) } else { None }
375    }
376    fn if_true<T, F: FnOnce() -> T>(&self, f: F) -> Option<T> {
377        if *self { Some(f()) } else { None }
378    }
379    fn if_false<T, F: FnOnce() -> T>(&self, f: F) -> Option<T> {
380        if self.not() { Some(f()) } else { None }
381    }
382    fn toggle(&mut self) {
383        *self = !*self;
384    }
385}
386
387/// Conditional vector push helpers.
388pub trait VecUtils<T> {
389    /// Pushes the value if `cond` is `true`.
390    fn push_if(&mut self, push: T, cond: bool);
391
392    /// Lazily evaluates and pushes the value if `cond` is `true`.
393    fn push_if_with<F: FnOnce() -> T>(&mut self, cond: bool, f: F);
394}
395
396impl<T> VecUtils<T> for Vec<T> {
397    fn push_if(&mut self, push: T, cond: bool) {
398        if cond {
399            self.push(push);
400        }
401    }
402    fn push_if_with<F: FnOnce() -> T>(&mut self, cond: bool, f: F) {
403        if cond {
404            self.push(f());
405        }
406    }
407}
408
409pub trait MapUtils<K, V> {
410    fn get_or<'a>(&'a self, key: &K, fallback: &'a V) -> &'a V;
411    fn insert_if(&mut self, key: K, value: V, cond: bool);
412}
413
414impl<K: Eq + Hash, V> MapUtils<K, V> for HashMap<K, V> {
415    fn get_or<'a>(&'a self, key: &K, fallback: &'a V) -> &'a V {
416        self.get(key).unwrap_or(fallback)
417    }
418
419    fn insert_if(&mut self, key: K, value: V, cond: bool) {
420        if cond {
421            self.insert(key, value);
422        }
423    }
424}
425
426pub trait ResultUtils<T, E> {
427    fn if_ok<F: FnOnce(&T)>(self, f: F) -> Self;
428    fn if_err<F: FnOnce(&E)>(self, f: F) -> Self;
429}
430
431impl<T, E: std::fmt::Debug> ResultUtils<T, E> for Result<T, E> {
432    fn if_ok<F: FnOnce(&T)>(self, f: F) -> Self {
433        if let Ok(ref val) = self {
434            f(val);
435        }
436        self
437    }
438
439    fn if_err<F: FnOnce(&E)>(self, f: F) -> Self {
440        if let Err(ref err) = self {
441            f(err);
442        }
443        self
444    }
445}
446
447/// Pretty-formatting for `Duration`.
448pub trait DurationUtils {
449    /// Returns a formatted string like `"1h 20m 5s"`.
450    fn pretty(&self) -> String;
451}
452
453impl DurationUtils for Duration {
454    fn pretty(&self) -> String {
455        let total_secs = self.as_secs();
456        let hours = total_secs / 3600;
457        let mins = (total_secs % 3600) / 60;
458        let secs = total_secs % 60;
459        format!("{}h {}m {}s", hours, mins, secs)
460    }
461}
462
463pub trait IteratorUtils: Iterator + Sized {
464    fn find_map_or<T, F: FnMut(Self::Item) -> Option<T>>(self, f: F, fallback: T) -> T;
465}
466
467impl<I: Iterator> IteratorUtils for I {
468    fn find_map_or<T, F: FnMut(Self::Item) -> Option<T>>(mut self, f: F, fallback: T) -> T {
469        self.find_map(f).unwrap_or(fallback)
470    }
471}
472
473pub trait IdentityUtils: Sized {
474    fn tap<F: FnOnce(&Self)>(self, f: F) -> Self;
475}
476
477impl<T> IdentityUtils for T {
478    fn tap<F: FnOnce(&Self)>(self, f: F) -> Self {
479        f(&self);
480        self
481    }
482}
483
484/// Helpers to panic or exit cleanly with messages.
485pub trait PanicUtils<T> {
486    /// If `None` or `Err`, logs the message and exits the program.
487    fn unwrap_or_exit(self, msg: &str) -> T;
488}
489
490impl<T> PanicUtils<T> for Option<T> {
491    fn unwrap_or_exit(self, msg: &str) -> T {
492        self.unwrap_or_else(|| {
493            eprintln!("[FATAL]: {}", msg);
494            std::process::exit(1);
495        })
496    }
497}
498
499impl<T, U> PanicUtils<T> for Result<T, U> {
500    fn unwrap_or_exit(self, msg: &str) -> T {
501        self.unwrap_or_else(|_| {
502            eprintln!("[FATAL]: {}", msg);
503            std::process::exit(1);
504        })
505    }
506}
507
508pub trait ClampUtils {
509    fn clamp_to(self, min: Self, max: Self) -> Self;
510}
511impl ClampUtils for i32 {
512    fn clamp_to(self, min: Self, max: Self) -> Self {
513        self.max(min).min(max)
514    }
515}
516
517pub trait NumberUtils {
518    #[must_use]
519    fn is_even(&self) -> bool;
520    #[must_use]
521    fn is_odd(&self) -> bool;
522}
523
524impl NumberUtils for i32 {
525    fn is_even(&self) -> bool {
526        self % 2 == 0
527    }
528    fn is_odd(&self) -> bool {
529        self % 2 != 0
530    }
531}