re-uploading work
This commit is contained in:
299
crates/common/src/utils.rs
Normal file
299
crates/common/src/utils.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
//! 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user