Files
attune/crates/common/src/utils.rs
2026-02-04 17:46:30 -06:00

300 lines
7.4 KiB
Rust

//! Utility functions for Attune services
//!
//! This module provides common utility functions used across all services.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// Pagination parameters
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pagination {
/// Page number (0-indexed)
#[serde(default)]
pub page: u32,
/// Number of items per page
#[serde(default = "default_page_size")]
pub page_size: u32,
}
fn default_page_size() -> u32 {
50
}
impl Default for Pagination {
fn default() -> Self {
Self {
page: 0,
page_size: default_page_size(),
}
}
}
impl Pagination {
/// Calculate the offset for SQL queries
pub fn offset(&self) -> u32 {
self.page * self.page_size
}
/// Get the limit for SQL queries
pub fn limit(&self) -> u32 {
self.page_size
}
/// Validate pagination parameters
pub fn validate(&self) -> crate::Result<()> {
if self.page_size == 0 {
return Err(crate::Error::validation("Page size must be greater than 0"));
}
if self.page_size > 1000 {
return Err(crate::Error::validation("Page size must not exceed 1000"));
}
Ok(())
}
}
/// Paginated response wrapper
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginatedResponse<T> {
/// The data items
pub data: Vec<T>,
/// Pagination metadata
pub pagination: PaginationMetadata,
}
/// Pagination metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationMetadata {
/// Current page number
pub page: u32,
/// Number of items per page
pub page_size: u32,
/// Total number of items
pub total: u64,
/// Total number of pages
pub total_pages: u32,
/// Whether there is a next page
pub has_next: bool,
/// Whether there is a previous page
pub has_prev: bool,
}
impl PaginationMetadata {
/// Create pagination metadata
pub fn new(pagination: &Pagination, total: u64) -> Self {
let total_pages = ((total as f64) / (pagination.page_size as f64)).ceil() as u32;
let has_next = pagination.page + 1 < total_pages;
let has_prev = pagination.page > 0;
Self {
page: pagination.page,
page_size: pagination.page_size,
total,
total_pages,
has_next,
has_prev,
}
}
}
/// Convert Duration to human-readable string
pub fn format_duration(duration: Duration) -> String {
let secs = duration.as_secs();
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m {}s", secs / 60, secs % 60)
} else if secs < 86400 {
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
} else {
format!("{}d {}h", secs / 86400, (secs % 86400) / 3600)
}
}
/// Format timestamp relative to now (e.g., "2 hours ago")
pub fn format_relative_time(timestamp: DateTime<Utc>) -> String {
let now = Utc::now();
let duration = now.signed_duration_since(timestamp);
if duration.num_seconds() < 0 {
return "in the future".to_string();
}
let secs = duration.num_seconds();
if secs < 60 {
format!("{} seconds ago", secs)
} else if secs < 3600 {
let mins = secs / 60;
if mins == 1 {
"1 minute ago".to_string()
} else {
format!("{} minutes ago", mins)
}
} else if secs < 86400 {
let hours = secs / 3600;
if hours == 1 {
"1 hour ago".to_string()
} else {
format!("{} hours ago", hours)
}
} else {
let days = secs / 86400;
if days == 1 {
"1 day ago".to_string()
} else {
format!("{} days ago", days)
}
}
}
/// Sanitize a reference string (lowercase, replace spaces with hyphens)
pub fn sanitize_ref(input: &str) -> String {
input
.to_lowercase()
.trim()
.chars()
.map(|c| if c.is_whitespace() { '-' } else { c })
.collect()
}
/// Generate a unique identifier
pub fn generate_id() -> String {
uuid::Uuid::new_v4().to_string()
}
/// Truncate a string to a maximum length
pub fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
/// Redact sensitive information from strings
pub fn redact_sensitive(s: &str) -> String {
if s.is_empty() {
return String::new();
}
let visible_chars = s.len().min(4);
let redacted_chars = s.len().saturating_sub(visible_chars);
if redacted_chars == 0 {
return "*".repeat(s.len());
}
format!(
"{}{}",
"*".repeat(redacted_chars),
&s[s.len() - visible_chars..]
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pagination_offset() {
let page = Pagination {
page: 0,
page_size: 10,
};
assert_eq!(page.offset(), 0);
assert_eq!(page.limit(), 10);
let page = Pagination {
page: 2,
page_size: 25,
};
assert_eq!(page.offset(), 50);
assert_eq!(page.limit(), 25);
}
#[test]
fn test_pagination_validation() {
let page = Pagination {
page: 0,
page_size: 0,
};
assert!(page.validate().is_err());
let page = Pagination {
page: 0,
page_size: 2000,
};
assert!(page.validate().is_err());
let page = Pagination {
page: 0,
page_size: 50,
};
assert!(page.validate().is_ok());
}
#[test]
fn test_pagination_metadata() {
let pagination = Pagination {
page: 1,
page_size: 10,
};
let metadata = PaginationMetadata::new(&pagination, 45);
assert_eq!(metadata.page, 1);
assert_eq!(metadata.page_size, 10);
assert_eq!(metadata.total, 45);
assert_eq!(metadata.total_pages, 5);
assert!(metadata.has_next);
assert!(metadata.has_prev);
}
#[test]
fn test_format_duration() {
assert_eq!(format_duration(Duration::from_secs(30)), "30s");
assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
assert_eq!(format_duration(Duration::from_secs(3661)), "1h 1m");
assert_eq!(format_duration(Duration::from_secs(86400)), "1d 0h");
}
#[test]
fn test_sanitize_ref() {
assert_eq!(sanitize_ref("My Action"), "my-action");
assert_eq!(sanitize_ref(" Test "), "test");
assert_eq!(sanitize_ref("UPPERCASE"), "uppercase");
}
#[test]
fn test_generate_id() {
let id1 = generate_id();
let id2 = generate_id();
assert_ne!(id1, id2);
assert_eq!(id1.len(), 36); // UUID v4 format
}
#[test]
fn test_truncate() {
assert_eq!(truncate("short", 10), "short");
assert_eq!(truncate("this is a long string", 10), "this is...");
assert_eq!(truncate("abc", 3), "abc");
assert_eq!(truncate("abcd", 3), "...");
}
#[test]
fn test_redact_sensitive() {
assert_eq!(redact_sensitive(""), "");
assert_eq!(redact_sensitive("abc"), "***");
assert_eq!(redact_sensitive("password123"), "*******d123");
assert_eq!(redact_sensitive("secret"), "**cret");
}
}