Skip to main content

backbone_core/
utils.rs

1//! Backbone Framework Utilities
2//!
3//! Common utility functions and helpers for the Backbone framework.
4//! These utilities are designed to be reused across modules.
5
6use chrono::{DateTime, Utc, Duration};
7use serde::{Deserialize, Serialize};
8
9// ============================================================
10// Timestamp Utilities
11// ============================================================
12
13/// Convert prost_types::Timestamp to chrono::DateTime<Utc>
14#[cfg(feature = "prost")]
15pub fn prost_timestamp_to_datetime(ts: &prost_types::Timestamp) -> Option<DateTime<Utc>> {
16    DateTime::from_timestamp(ts.seconds, ts.nanos as u32)
17}
18
19/// Convert chrono::DateTime<Utc> to prost_types::Timestamp
20#[cfg(feature = "prost")]
21pub fn datetime_to_prost_timestamp(dt: DateTime<Utc>) -> prost_types::Timestamp {
22    prost_types::Timestamp {
23        seconds: dt.timestamp(),
24        nanos: dt.timestamp_subsec_nanos() as i32,
25    }
26}
27
28/// Get current timestamp
29pub fn now() -> DateTime<Utc> {
30    Utc::now()
31}
32
33/// Check if a timestamp is in the past
34pub fn is_past(dt: DateTime<Utc>) -> bool {
35    dt < Utc::now()
36}
37
38/// Check if a timestamp is in the future
39pub fn is_future(dt: DateTime<Utc>) -> bool {
40    dt > Utc::now()
41}
42
43/// Get timestamp for N days ago
44pub fn days_ago(days: i64) -> DateTime<Utc> {
45    Utc::now() - Duration::days(days)
46}
47
48/// Get timestamp for N days from now
49pub fn days_from_now(days: i64) -> DateTime<Utc> {
50    Utc::now() + Duration::days(days)
51}
52
53/// Get timestamp for N hours ago
54pub fn hours_ago(hours: i64) -> DateTime<Utc> {
55    Utc::now() - Duration::hours(hours)
56}
57
58/// Get timestamp for N hours from now
59pub fn hours_from_now(hours: i64) -> DateTime<Utc> {
60    Utc::now() + Duration::hours(hours)
61}
62
63/// Get timestamp for N minutes from now
64pub fn minutes_from_now(minutes: i64) -> DateTime<Utc> {
65    Utc::now() + Duration::minutes(minutes)
66}
67
68// ============================================================
69// ID Utilities
70// ============================================================
71
72use uuid::Uuid;
73
74/// Generate a new UUID v4
75pub fn new_id() -> Uuid {
76    Uuid::new_v4()
77}
78
79/// Generate a new UUID v4 as string
80pub fn new_id_string() -> String {
81    Uuid::new_v4().to_string()
82}
83
84/// Parse UUID from string
85pub fn parse_id(s: &str) -> Option<Uuid> {
86    Uuid::parse_str(s).ok()
87}
88
89/// Validate UUID format
90pub fn is_valid_uuid(s: &str) -> bool {
91    Uuid::parse_str(s).is_ok()
92}
93
94// ============================================================
95// Pagination Utilities
96// ============================================================
97
98/// Standard pagination parameters
99#[derive(Debug, Clone, Default, Serialize, Deserialize)]
100pub struct PaginationParams {
101    pub page: u32,
102    pub limit: u32,
103    pub offset: u32,
104}
105
106impl PaginationParams {
107    /// Create pagination params with defaults
108    pub fn new(page: u32, limit: u32) -> Self {
109        let page = if page == 0 { 1 } else { page };
110        let limit = limit.clamp(1, 100); // Max 100 per page
111        let offset = (page - 1) * limit;
112
113        Self { page, limit, offset }
114    }
115
116    /// Default pagination (page 1, 20 items)
117    pub fn default_pagination() -> Self {
118        Self::new(1, 20)
119    }
120
121    /// Calculate total pages
122    pub fn total_pages(&self, total_items: u64) -> u32 {
123        ((total_items as f64) / (self.limit as f64)).ceil() as u32
124    }
125}
126
127/// Pagination metadata for responses
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct PaginationMeta {
130    pub total: u64,
131    pub page: u32,
132    pub limit: u32,
133    pub total_pages: u32,
134    pub has_next: bool,
135    pub has_prev: bool,
136}
137
138impl PaginationMeta {
139    pub fn new(total: u64, page: u32, limit: u32) -> Self {
140        let total_pages = ((total as f64) / (limit as f64)).ceil() as u32;
141        Self {
142            total,
143            page,
144            limit,
145            total_pages,
146            has_next: page < total_pages,
147            has_prev: page > 1,
148        }
149    }
150}
151
152// ============================================================
153// String Utilities
154// ============================================================
155
156/// Trim and lowercase a string
157pub fn normalize_string(s: &str) -> String {
158    s.trim().to_lowercase()
159}
160
161/// Validate email format (basic)
162pub fn is_valid_email(email: &str) -> bool {
163    let email = email.trim();
164    if email.is_empty() || email.len() > 254 {
165        return false;
166    }
167
168    let parts: Vec<&str> = email.split('@').collect();
169    if parts.len() != 2 {
170        return false;
171    }
172
173    let local = parts[0];
174    let domain = parts[1];
175
176    !local.is_empty()
177        && !domain.is_empty()
178        && domain.contains('.')
179        && !domain.starts_with('.')
180        && !domain.ends_with('.')
181}
182
183/// Validate username format
184pub fn is_valid_username(username: &str) -> bool {
185    let username = username.trim();
186    if username.len() < 3 || username.len() > 50 {
187        return false;
188    }
189
190    username.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
191}
192
193// ============================================================
194// Error Utilities
195// ============================================================
196
197/// Common error types for backbone operations
198#[derive(Debug, Clone, thiserror::Error)]
199pub enum BackboneError {
200    #[error("Entity not found: {entity_type} with id {id}")]
201    NotFound { entity_type: String, id: String },
202
203    #[error("Validation error on field '{field}': {message}")]
204    Validation { field: String, message: String },
205
206    #[error("Conflict: {message}")]
207    Conflict { message: String },
208
209    #[error("Unauthorized: {message}")]
210    Unauthorized { message: String },
211
212    #[error("Forbidden: {message}")]
213    Forbidden { message: String },
214
215    #[error("Internal error: {message}")]
216    Internal { message: String },
217
218    #[error("Database error: {message}")]
219    Database { message: String },
220}
221
222impl BackboneError {
223    pub fn not_found(entity_type: impl Into<String>, id: impl Into<String>) -> Self {
224        Self::NotFound {
225            entity_type: entity_type.into(),
226            id: id.into(),
227        }
228    }
229
230    pub fn validation(field: impl Into<String>, message: impl Into<String>) -> Self {
231        Self::Validation {
232            field: field.into(),
233            message: message.into(),
234        }
235    }
236
237    pub fn conflict(message: impl Into<String>) -> Self {
238        Self::Conflict {
239            message: message.into(),
240        }
241    }
242
243    pub fn internal(message: impl Into<String>) -> Self {
244        Self::Internal {
245            message: message.into(),
246        }
247    }
248
249    pub fn database(message: impl Into<String>) -> Self {
250        Self::Database {
251            message: message.into(),
252        }
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_pagination_params() {
262        let params = PaginationParams::new(2, 25);
263        assert_eq!(params.page, 2);
264        assert_eq!(params.limit, 25);
265        assert_eq!(params.offset, 25);
266    }
267
268    #[test]
269    fn test_pagination_meta() {
270        let meta = PaginationMeta::new(100, 2, 20);
271        assert_eq!(meta.total_pages, 5);
272        assert!(meta.has_next);
273        assert!(meta.has_prev);
274    }
275
276    #[test]
277    fn test_email_validation() {
278        assert!(is_valid_email("test@example.com"));
279        assert!(is_valid_email("user.name@domain.co.uk"));
280        assert!(!is_valid_email("invalid"));
281        assert!(!is_valid_email("@domain.com"));
282        assert!(!is_valid_email("user@"));
283    }
284
285    #[test]
286    fn test_username_validation() {
287        assert!(is_valid_username("john_doe"));
288        assert!(is_valid_username("user-123"));
289        assert!(!is_valid_username("ab")); // too short
290        assert!(!is_valid_username("user name")); // space
291    }
292
293    #[test]
294    fn test_uuid_generation() {
295        let id = new_id();
296        assert!(is_valid_uuid(&id.to_string()));
297    }
298}