Skip to main content

ferrox_utils/
lib.rs

1//! # Ferrox Utils (`ferrox-utils`)
2//!
3//! `ferrox-utils` contains shared utility functions, date/time UTC formatters, string casing converters, and UUID helpers
4//! used across the Ferrox framework ecosystem.
5//!
6//! ## Principles
7//! All systems within Ferrox strictly enforce UTC timestamps and standardized string formatting across database boundaries.
8//! `ferrox-utils` centralizes these core helper functions to prevent code duplication across service layers.
9//!
10//! ## Key Features
11//! - 📅 **Date & Time Helpers**: Guarantees UTC timestamps (`now_utc()`) and HTTP GMT date formatting.
12//! - 🔤 **String Case Conversion**: Convert strings between `snake_case`, `camelCase`, `PascalCase`, and `kebab-case`.
13//! - 🔑 **UUID Utilities**: Generates standard v4 UUIDs and short URL-safe identifiers.
14
15use chrono::{DateTime, TimeZone, Utc};
16use convert_case::{Case, Casing};
17use time::OffsetDateTime;
18use uuid::Uuid;
19
20pub mod date {
21    use super::*;
22
23    /// Returns the current time strictly in UTC. 
24    /// All databases in Ferrox MUST store dates in UTC.
25    pub fn now_utc() -> DateTime<Utc> {
26        Utc::now()
27    }
28
29    /// Converts a given UTC DateTime to a formatted GMT string for the Frontend.
30    pub fn to_gmt_string(dt: DateTime<Utc>) -> String {
31        dt.format("%a, %d %b %Y %H:%M:%S GMT").to_string()
32    }
33}
34
35pub mod string {
36    use super::*;
37
38    pub trait StringExt {
39        fn to_camel_case(&self) -> String;
40        fn to_snake_case(&self) -> String;
41        fn to_kebab_case(&self) -> String;
42        fn mask(&self, keep_start: usize, keep_end: usize) -> String;
43    }
44
45    impl StringExt for String {
46        fn to_camel_case(&self) -> String {
47            self.to_case(Case::Camel)
48        }
49
50        fn to_snake_case(&self) -> String {
51            self.to_case(Case::Snake)
52        }
53
54        fn to_kebab_case(&self) -> String {
55            self.to_case(Case::Kebab)
56        }
57
58        fn mask(&self, keep_start: usize, keep_end: usize) -> String {
59            if self.len() <= keep_start + keep_end {
60                return self.clone();
61            }
62            
63            let start = &self[..keep_start];
64            let end = &self[self.len() - keep_end..];
65            let masked_len = self.len() - keep_start - keep_end;
66            let mask_chars = "*".repeat(masked_len);
67            
68            format!("{}{}{}", start, mask_chars, end)
69        }
70    }
71}
72
73/// Generates a secure, time-ordered UUID v7 string.
74pub fn generate_uuid() -> String {
75    Uuid::now_v7().to_string()
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use super::date::*;
82    use super::string::*;
83
84    #[test]
85    fn test_generate_uuid() {
86        let id1 = generate_uuid();
87        let id2 = generate_uuid();
88        
89        assert_eq!(id1.len(), 36);
90        assert_ne!(id1, id2);
91    }
92
93    #[test]
94    fn test_date_utilities() {
95        let utc = now_utc();
96        let gmt_str = to_gmt_string(utc);
97        assert!(gmt_str.ends_with("GMT"));
98    }
99
100    #[test]
101    fn test_string_casing() {
102        let original = String::from("hello world test");
103        assert_eq!(original.to_camel_case(), "helloWorldTest");
104        assert_eq!(original.to_snake_case(), "hello_world_test");
105        assert_eq!(original.to_kebab_case(), "hello-world-test");
106    }
107
108    #[test]
109    fn test_string_masking() {
110        let email = String::from("admin@antigravity.com");
111        let masked = email.mask(3, 4); // "adm**************.com"
112        
113        assert!(masked.starts_with("adm"));
114        assert!(masked.ends_with(".com"));
115        assert!(masked.contains("*"));
116        assert_eq!(masked.len(), email.len());
117        
118        // Edge case: string too short
119        let short = String::from("a");
120        assert_eq!(short.mask(3, 4), "a");
121        
122        // Exact length
123        let exact = String::from("abcdefg");
124        assert_eq!(exact.mask(3, 4), "abcdefg");
125    }
126}