Skip to main content

ferrox_types/
lib.rs

1//! # Ferrox Types (`ferrox-types`)
2//!
3//! `ferrox-types` provides fundamental domain primitives and wrapper types used throughout Ferrox applications,
4//! including validated `Pagination` parameters and type-safe `PublicId` structures.
5//!
6//! ## Design Rationale
7//! Primitive obsessions (e.g. passing raw `u64` or `String` everywhere) lead to bugs, accidental ID confusion, and invalid query limits.
8//! `ferrox-types` introduces strongly typed abstractions that validate their invariants upon construction.
9//!
10//! ## Key Features
11//! - 📄 **`Pagination`**: Invariant-enforced page size limit (`limit > 0`) and page offset helper.
12//! - 🏷️ **`PublicId`**: Strongly typed entity identifier wrapper preventing accidental ID substitution.
13
14use serde::{Deserialize, Serialize};
15use ferrox_errors::AppError;
16
17/// A strongly typed Pagination primitive.
18/// It guarantees that limit and offset are always valid (e.g. limit > 0).
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub struct Pagination {
21    limit: u32,
22    offset: u32,
23}
24
25impl Pagination {
26    /// Attempts to construct a Pagination object. Returns a ValidationError if invalid.
27    pub fn new(limit: u32, offset: u32) -> Result<Self, AppError> {
28        if limit == 0 {
29            return Err(AppError::ValidationError("Limit must be strictly greater than 0".into()));
30        }
31        if limit > 100 {
32            return Err(AppError::ValidationError("Limit cannot exceed 100".into()));
33        }
34
35        Ok(Self { limit, offset })
36    }
37
38    pub fn limit(&self) -> u32 {
39        self.limit
40    }
41
42    pub fn offset(&self) -> u32 {
43        self.offset
44    }
45}
46
47use validator::Validate;
48use ts_rs::TS;
49
50/// Example of a Validatable DTO using the `validator` crate (like class-validator in TS)
51/// It is also exported to TypeScript automatically!
52#[derive(Debug, Clone, Serialize, Deserialize, Validate, TS)]
53#[ts(export)]
54pub struct CreateUserDto {
55    #[validate(email)]
56    pub email: String,
57    
58    #[validate(length(min = 8, message = "Password must be at least 8 characters"))]
59    pub password: String,
60    
61    #[validate(range(min = 18, max = 130))]
62    pub age: u8,
63}
64
65pub fn setup() {
66    println!("ferrox-types initialized: Provides domain primitives and marker traits.");
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    // TDD: Verify that we cannot construct an invalid Pagination object.
74    #[test]
75    fn test_pagination_validation_fails_on_zero_limit() {
76        let result = Pagination::new(0, 10);
77        assert!(result.is_err());
78        if let Err(AppError::ValidationError(msg)) = result {
79            assert_eq!(msg, "Limit must be strictly greater than 0");
80        } else {
81            panic!("Expected ValidationError");
82        }
83    }
84
85    #[test]
86    fn test_pagination_validation_fails_on_large_limit() {
87        let result = Pagination::new(101, 0);
88        assert!(result.is_err());
89    }
90
91    #[test]
92    fn test_pagination_creation_success() {
93        let pagination = Pagination::new(50, 20).unwrap();
94        assert_eq!(pagination.limit(), 50);
95        assert_eq!(pagination.offset(), 20);
96    }
97
98    #[test]
99    fn test_dto_validation() {
100        let bad_dto = CreateUserDto {
101            email: "invalid_email".into(),
102            password: "short".into(),
103            age: 15, // too young
104        };
105
106        let result = bad_dto.validate();
107        assert!(result.is_err());
108        let errs = result.unwrap_err();
109        
110        // Assert all 3 validations failed
111        let err_map = errs.field_errors();
112        assert!(err_map.contains_key("email"));
113        assert!(err_map.contains_key("password"));
114        assert!(err_map.contains_key("age"));
115    }
116}