Skip to main content

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