pjson_rs_domain/value_objects/
priority.rs

1//! Priority Value Object with compile-time safety
2//!
3//! Provides type-safe priority system with validation rules
4//! and compile-time constants for common priority levels.
5//!
6//! TODO: Remove serde derives once all serialization uses DTOs
7
8use crate::{DomainError, DomainResult};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11use std::num::NonZeroU8;
12
13/// Type-safe priority value (1-255 range)
14///
15/// This is a pure domain object. Serialization should be handled
16/// in the application layer via DTOs, but serde is temporarily kept
17/// for compatibility with existing code.
18///
19/// TODO: Remove Serialize, Deserialize derives once all serialization uses DTOs
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
21pub struct Priority(NonZeroU8);
22
23impl Priority {
24    /// Critical priority - for essential data (IDs, status, core metadata)
25    pub const CRITICAL: Self = Self::new_unchecked(100);
26
27    /// High priority - for important visible data (names, titles)
28    pub const HIGH: Self = Self::new_unchecked(80);
29
30    /// Medium priority - for regular content
31    pub const MEDIUM: Self = Self::new_unchecked(50);
32
33    /// Low priority - for supplementary data
34    pub const LOW: Self = Self::new_unchecked(25);
35
36    /// Background priority - for analytics, logs, etc.
37    pub const BACKGROUND: Self = Self::new_unchecked(10);
38
39    /// Create priority with validation
40    pub fn new(value: u8) -> DomainResult<Self> {
41        NonZeroU8::new(value)
42            .map(Self)
43            .ok_or_else(|| DomainError::InvalidPriority("Priority cannot be zero".to_string()))
44    }
45
46    /// Create priority without validation (for const contexts)
47    const fn new_unchecked(value: u8) -> Self {
48        // Safety: We control all usage sites to ensure value > 0
49        unsafe { Self(NonZeroU8::new_unchecked(value)) }
50    }
51
52    /// Get raw priority value
53    pub fn value(self) -> u8 {
54        self.0.get()
55    }
56
57    /// Increase priority by delta (saturating at max)
58    pub fn increase_by(self, delta: u8) -> Self {
59        let new_value = self.0.get().saturating_add(delta);
60        Self(NonZeroU8::new(new_value).unwrap_or(NonZeroU8::MAX))
61    }
62
63    /// Decrease priority by delta (saturating at min)
64    pub fn decrease_by(self, delta: u8) -> Self {
65        let new_value = self.0.get().saturating_sub(delta);
66        Self(NonZeroU8::new(new_value).unwrap_or(NonZeroU8::MIN))
67    }
68
69    /// Check if this is a critical priority
70    pub fn is_critical(self) -> bool {
71        self.0.get() >= Self::CRITICAL.0.get()
72    }
73
74    /// Check if this is high priority or above
75    pub fn is_high_or_above(self) -> bool {
76        self.0.get() >= Self::HIGH.0.get()
77    }
78
79    /// Create priority from percentage (0-100)
80    pub fn from_percentage(percent: f32) -> DomainResult<Self> {
81        if !(0.0..=100.0).contains(&percent) {
82            return Err(DomainError::InvalidPriority(format!(
83                "Percentage must be 0-100, got {percent}"
84            )));
85        }
86
87        let value = (percent * 2.55).round() as u8;
88        Self::new(value.max(1)) // Ensure non-zero
89    }
90
91    /// Convert to percentage (0-100)
92    pub fn to_percentage(self) -> f32 {
93        (self.0.get() as f32 / 255.0) * 100.0
94    }
95
96    /// Get priority value with fallback for compatibility
97    pub fn unwrap_or(self, _default: u8) -> u8 {
98        self.0.get()
99    }
100}
101
102impl fmt::Display for Priority {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match *self {
105            Self::CRITICAL => {
106                let val = self.0.get();
107                write!(f, "Critical({val})")
108            }
109            Self::HIGH => {
110                let val = self.0.get();
111                write!(f, "High({val})")
112            }
113            Self::MEDIUM => {
114                let val = self.0.get();
115                write!(f, "Medium({val})")
116            }
117            Self::LOW => {
118                let val = self.0.get();
119                write!(f, "Low({val})")
120            }
121            Self::BACKGROUND => {
122                let val = self.0.get();
123                write!(f, "Background({val})")
124            }
125            _ => {
126                let val = self.0.get();
127                write!(f, "Priority({val})")
128            }
129        }
130    }
131}
132
133impl From<Priority> for u8 {
134    fn from(priority: Priority) -> Self {
135        priority.0.get()
136    }
137}
138
139impl TryFrom<u8> for Priority {
140    type Error = DomainError;
141
142    fn try_from(value: u8) -> Result<Self, Self::Error> {
143        Self::new(value)
144    }
145}
146
147/// Priority validation rules (reserved for future use)
148#[allow(dead_code)]
149pub trait PriorityRule {
150    fn validate(&self, priority: Priority) -> bool;
151    fn name(&self) -> &'static str;
152}
153
154/// Rule: Priority must be at least minimum value (reserved for future use)
155#[allow(dead_code)]
156#[derive(Debug, Clone)]
157pub struct MinimumPriority(pub Priority);
158
159#[allow(dead_code)]
160impl PriorityRule for MinimumPriority {
161    fn validate(&self, priority: Priority) -> bool {
162        priority >= self.0
163    }
164
165    fn name(&self) -> &'static str {
166        "minimum_priority"
167    }
168}
169
170/// Rule: Priority must be within range (reserved for future use)
171#[allow(dead_code)]
172#[derive(Debug, Clone)]
173pub struct PriorityRange {
174    pub min: Priority,
175    pub max: Priority,
176}
177
178#[allow(dead_code)]
179impl PriorityRule for PriorityRange {
180    fn validate(&self, priority: Priority) -> bool {
181        priority >= self.min && priority <= self.max
182    }
183
184    fn name(&self) -> &'static str {
185        "priority_range"
186    }
187}
188
189/// Collection of priority rules for validation (reserved for future use)
190#[allow(dead_code)]
191pub struct PriorityRules {
192    rules: Vec<Box<dyn PriorityRule + Send + Sync>>,
193}
194
195#[allow(dead_code)]
196impl PriorityRules {
197    pub fn new() -> Self {
198        Self { rules: Vec::new() }
199    }
200
201    pub fn add_rule(mut self, rule: impl PriorityRule + Send + Sync + 'static) -> Self {
202        self.rules.push(Box::new(rule));
203        self
204    }
205
206    pub fn validate(&self, priority: Priority) -> DomainResult<()> {
207        for rule in &self.rules {
208            if !rule.validate(priority) {
209                return Err(DomainError::InvalidPriority(format!(
210                    "Priority {priority} violates rule: {}",
211                    rule.name()
212                )));
213            }
214        }
215        Ok(())
216    }
217}
218
219impl Default for PriorityRules {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_priority_constants() {
231        assert_eq!(Priority::CRITICAL.value(), 100);
232        assert_eq!(Priority::HIGH.value(), 80);
233        assert_eq!(Priority::MEDIUM.value(), 50);
234        assert_eq!(Priority::LOW.value(), 25);
235        assert_eq!(Priority::BACKGROUND.value(), 10);
236    }
237
238    #[test]
239    fn test_priority_validation() {
240        assert!(Priority::new(1).is_ok());
241        assert!(Priority::new(255).is_ok());
242        assert!(Priority::new(0).is_err());
243    }
244
245    #[test]
246    fn test_priority_ordering() {
247        assert!(Priority::CRITICAL > Priority::HIGH);
248        assert!(Priority::HIGH > Priority::MEDIUM);
249        assert!(Priority::MEDIUM > Priority::LOW);
250        assert!(Priority::LOW > Priority::BACKGROUND);
251    }
252
253    #[test]
254    fn test_priority_arithmetic() {
255        let p = Priority::MEDIUM;
256        assert_eq!(p.increase_by(10).value(), 60);
257        assert_eq!(p.decrease_by(10).value(), 40);
258
259        // Test saturation
260        let max_p = Priority::new(255).unwrap();
261        assert_eq!(max_p.increase_by(10).value(), 255);
262
263        let min_p = Priority::new(1).unwrap();
264        assert_eq!(min_p.decrease_by(10).value(), 1);
265    }
266
267    #[test]
268    fn test_priority_percentage() {
269        let p = Priority::from_percentage(50.0).unwrap();
270        assert!(p.to_percentage() >= 49.0 && p.to_percentage() <= 51.0);
271
272        assert!(Priority::from_percentage(101.0).is_err());
273        assert!(Priority::from_percentage(-1.0).is_err());
274    }
275
276    #[test]
277    fn test_priority_rules() {
278        let rules = PriorityRules::new()
279            .add_rule(MinimumPriority(Priority::LOW))
280            .add_rule(PriorityRange {
281                min: Priority::LOW,
282                max: Priority::CRITICAL,
283            });
284
285        assert!(rules.validate(Priority::MEDIUM).is_ok());
286        assert!(rules.validate(Priority::new(5).unwrap()).is_err());
287        assert!(rules.validate(Priority::new(200).unwrap()).is_err());
288    }
289
290    #[test]
291    fn test_priority_display() {
292        assert_eq!(Priority::CRITICAL.to_string(), "Critical(100)");
293        assert_eq!(Priority::HIGH.to_string(), "High(80)");
294        assert_eq!(Priority::new(42).unwrap().to_string(), "Priority(42)");
295    }
296}