pjson_rs/domain/services/
priority_service.rs

1//! Priority calculation and optimization service
2
3use crate::domain::{
4    DomainResult,
5    value_objects::{JsonPath, Priority},
6};
7// Temporary: use serde_json::Value until full migration to domain-specific JsonData
8use serde_json::Value as JsonValue;
9use std::collections::HashMap;
10
11/// Service for computing and optimizing priorities based on business rules
12#[derive(Debug, Clone)]
13pub struct PriorityService {
14    /// Field-based priority rules
15    field_rules: HashMap<String, Priority>,
16    /// Path-based priority rules (more specific)
17    path_rules: HashMap<String, Priority>,
18    /// Type-based priority rules
19    type_rules: HashMap<String, Priority>,
20    /// Default priority for unknown fields
21    default_priority: Priority,
22}
23
24impl PriorityService {
25    /// Create new priority service with default rules
26    pub fn new() -> Self {
27        let mut service = Self {
28            field_rules: HashMap::new(),
29            path_rules: HashMap::new(),
30            type_rules: HashMap::new(),
31            default_priority: Priority::MEDIUM,
32        };
33
34        // Configure default business rules
35        service.add_default_rules();
36        service
37    }
38
39    /// Create priority service with custom default
40    pub fn with_default_priority(default_priority: Priority) -> Self {
41        let mut service = Self::new();
42        service.default_priority = default_priority;
43        service
44    }
45
46    /// Add field-based priority rule
47    pub fn add_field_rule(&mut self, field_name: String, priority: Priority) {
48        self.field_rules.insert(field_name, priority);
49    }
50
51    /// Add path-based priority rule (more specific than field rules)
52    pub fn add_path_rule(&mut self, path_pattern: String, priority: Priority) {
53        self.path_rules.insert(path_pattern, priority);
54    }
55
56    /// Add type-based priority rule
57    pub fn add_type_rule(&mut self, type_name: String, priority: Priority) {
58        self.type_rules.insert(type_name, priority);
59    }
60
61    /// Calculate priority for a specific JSON path and value
62    pub fn calculate_priority(&self, path: &JsonPath, value: &JsonValue) -> Priority {
63        // 1. Check for exact path match (highest priority)
64        if let Some(&priority) = self.path_rules.get(path.as_str()) {
65            return priority;
66        }
67
68        // 2. Check for path pattern matches
69        for (pattern, &priority) in &self.path_rules {
70            if self.path_matches_pattern(path.as_str(), pattern) {
71                return priority;
72            }
73        }
74
75        // 3. Check for field name match
76        if let Some(field_name) = self.extract_field_name(path)
77            && let Some(&priority) = self.field_rules.get(&field_name)
78        {
79            return priority;
80        }
81
82        // 4. Check for type-based rules
83        let type_name = self.get_value_type_name(value);
84        if let Some(&priority) = self.type_rules.get(type_name) {
85            return priority;
86        }
87
88        // 5. Apply heuristic rules
89        self.apply_heuristic_priority(path, value)
90    }
91
92    /// Calculate priorities for all fields in a JSON object
93    pub fn calculate_priorities(
94        &self,
95        data: &JsonValue,
96    ) -> DomainResult<HashMap<JsonPath, Priority>> {
97        let mut priorities = HashMap::new();
98        self.calculate_priorities_recursive(data, &JsonPath::root(), &mut priorities)?;
99        Ok(priorities)
100    }
101
102    /// Update priority rules based on usage statistics
103    pub fn optimize_rules(&mut self, usage_stats: &UsageStatistics) {
104        // Increase priority for frequently accessed fields
105        for (field, access_count) in &usage_stats.field_access_counts {
106            if *access_count > usage_stats.average_access_count * 2 {
107                let current = self
108                    .field_rules
109                    .get(field)
110                    .copied()
111                    .unwrap_or(self.default_priority);
112                let optimized = current.increase_by(10);
113                self.field_rules.insert(field.clone(), optimized);
114            }
115        }
116
117        // Decrease priority for rarely accessed fields
118        for (field, access_count) in &usage_stats.field_access_counts {
119            if *access_count < usage_stats.average_access_count / 3 {
120                let current = self
121                    .field_rules
122                    .get(field)
123                    .copied()
124                    .unwrap_or(self.default_priority);
125                let optimized = current.decrease_by(5);
126                self.field_rules.insert(field.clone(), optimized);
127            }
128        }
129    }
130
131    /// Get priority rules summary
132    pub fn get_rules_summary(&self) -> PriorityRulesSummary {
133        PriorityRulesSummary {
134            field_rule_count: self.field_rules.len(),
135            path_rule_count: self.path_rules.len(),
136            type_rule_count: self.type_rules.len(),
137            default_priority: self.default_priority,
138        }
139    }
140
141    /// Private: Add default business priority rules
142    fn add_default_rules(&mut self) {
143        // Critical fields - identifiers and status
144        self.add_field_rule("id".to_string(), Priority::CRITICAL);
145        self.add_field_rule("uuid".to_string(), Priority::CRITICAL);
146        self.add_field_rule("status".to_string(), Priority::CRITICAL);
147        self.add_field_rule("state".to_string(), Priority::CRITICAL);
148        self.add_field_rule("error".to_string(), Priority::CRITICAL);
149
150        // High priority - user-visible content
151        self.add_field_rule("name".to_string(), Priority::HIGH);
152        self.add_field_rule("title".to_string(), Priority::HIGH);
153        self.add_field_rule("label".to_string(), Priority::HIGH);
154        self.add_field_rule("description".to_string(), Priority::HIGH);
155        self.add_field_rule("message".to_string(), Priority::HIGH);
156
157        // Medium priority - regular content
158        self.add_field_rule("content".to_string(), Priority::MEDIUM);
159        self.add_field_rule("body".to_string(), Priority::MEDIUM);
160        self.add_field_rule("value".to_string(), Priority::MEDIUM);
161        self.add_field_rule("data".to_string(), Priority::MEDIUM);
162
163        // Low priority - metadata
164        self.add_field_rule("created_at".to_string(), Priority::LOW);
165        self.add_field_rule("updated_at".to_string(), Priority::LOW);
166        self.add_field_rule("version".to_string(), Priority::LOW);
167        self.add_field_rule("metadata".to_string(), Priority::LOW);
168
169        // Background priority - analytics and debug info
170        self.add_field_rule("analytics".to_string(), Priority::BACKGROUND);
171        self.add_field_rule("debug".to_string(), Priority::BACKGROUND);
172        self.add_field_rule("trace".to_string(), Priority::BACKGROUND);
173        self.add_field_rule("logs".to_string(), Priority::BACKGROUND);
174
175        // Path-based rules (more specific)
176        self.add_path_rule("$.error".to_string(), Priority::CRITICAL);
177        self.add_path_rule("$.*.id".to_string(), Priority::CRITICAL);
178        self.add_path_rule("$.users[*].id".to_string(), Priority::CRITICAL);
179        self.add_path_rule("$.*.analytics".to_string(), Priority::BACKGROUND);
180
181        // Type-based rules
182        self.add_type_rule("array_large".to_string(), Priority::LOW); // Arrays > 100 items
183        self.add_type_rule("string_long".to_string(), Priority::LOW); // Strings > 1000 chars
184        self.add_type_rule("object_deep".to_string(), Priority::LOW); // Objects > 5 levels deep
185    }
186
187    /// Private: Check if path matches pattern (simple glob-like matching)
188    fn path_matches_pattern(&self, path: &str, pattern: &str) -> bool {
189        // Simple wildcard matching - could be improved with regex
190        if pattern.contains('*') {
191            let parts: Vec<&str> = pattern.split('*').collect();
192            if parts.is_empty() {
193                return true;
194            }
195
196            let mut pos = 0;
197            for (i, part) in parts.iter().enumerate() {
198                if part.is_empty() {
199                    continue;
200                }
201
202                if i == 0 {
203                    // First part must match from start
204                    if !path.starts_with(part) {
205                        return false;
206                    }
207                    pos = part.len();
208                } else if i == parts.len() - 1 {
209                    // Last part must match at end
210                    if !path[pos..].ends_with(part) {
211                        return false;
212                    }
213                } else {
214                    // Middle parts must exist somewhere after current position
215                    if let Some(found) = path[pos..].find(part) {
216                        pos += found + part.len();
217                    } else {
218                        return false;
219                    }
220                }
221            }
222            true
223        } else {
224            path == pattern
225        }
226    }
227
228    /// Private: Extract field name from JSON path
229    fn extract_field_name(&self, path: &JsonPath) -> Option<String> {
230        if let Some(segment) = path.last_segment() {
231            match segment {
232                crate::domain::value_objects::PathSegment::Key(key) => Some(key),
233                _ => None,
234            }
235        } else {
236            None
237        }
238    }
239
240    /// Private: Get type name for value
241    fn get_value_type_name(&self, value: &JsonValue) -> &'static str {
242        match value {
243            JsonValue::Null => "null",
244            JsonValue::Bool(_) => "boolean",
245            JsonValue::Number(_) => "number",
246            JsonValue::String(s) => {
247                if s.len() > 1000 {
248                    "string_long"
249                } else {
250                    "string"
251                }
252            }
253            JsonValue::Array(arr) => {
254                if arr.len() > 100 {
255                    "array_large"
256                } else {
257                    "array"
258                }
259            }
260            JsonValue::Object(obj) => {
261                if obj.len() > 20 {
262                    "object_large"
263                } else {
264                    "object"
265                }
266            }
267        }
268    }
269
270    /// Private: Apply heuristic-based priority calculation
271    fn apply_heuristic_priority(&self, path: &JsonPath, value: &JsonValue) -> Priority {
272        let mut priority = self.default_priority;
273
274        // Increase priority for shallow paths (more important)
275        let depth = path.depth();
276        if depth == 1 {
277            priority = priority.increase_by(20);
278        } else if depth == 2 {
279            priority = priority.increase_by(10);
280        } else if depth > 5 {
281            priority = priority.decrease_by(10);
282        }
283
284        // Adjust based on value characteristics
285        match value {
286            JsonValue::String(s) => {
287                if s.len() < 50 {
288                    // Short strings are likely important labels
289                    priority = priority.increase_by(5);
290                }
291            }
292            JsonValue::Array(arr) => {
293                if arr.len() > 10 {
294                    // Large arrays are less critical for initial display
295                    priority = priority.decrease_by(15);
296                }
297            }
298            JsonValue::Object(obj) => {
299                if obj.len() > 10 {
300                    // Complex objects are less critical for initial display
301                    priority = priority.decrease_by(10);
302                }
303            }
304            _ => {}
305        }
306
307        priority
308    }
309
310    /// Private: Recursively calculate priorities
311    fn calculate_priorities_recursive(
312        &self,
313        value: &JsonValue,
314        current_path: &JsonPath,
315        priorities: &mut HashMap<JsonPath, Priority>,
316    ) -> DomainResult<()> {
317        // Calculate priority for current path
318        let priority = self.calculate_priority(current_path, value);
319        priorities.insert(current_path.clone(), priority);
320
321        // Recurse into child values
322        match value {
323            JsonValue::Object(obj) => {
324                for (key, child_value) in obj.iter() {
325                    let child_path = current_path.append_key(key)?;
326                    self.calculate_priorities_recursive(child_value, &child_path, priorities)?;
327                }
328            }
329            JsonValue::Array(arr) => {
330                for (index, child_value) in arr.iter().enumerate() {
331                    let child_path = current_path.append_index(index);
332                    self.calculate_priorities_recursive(child_value, &child_path, priorities)?;
333                }
334            }
335            _ => {
336                // Leaf values - already calculated priority above
337            }
338        }
339
340        Ok(())
341    }
342}
343
344impl Default for PriorityService {
345    fn default() -> Self {
346        Self::new()
347    }
348}
349
350/// Usage statistics for priority optimization
351#[derive(Debug, Clone)]
352pub struct UsageStatistics {
353    pub field_access_counts: HashMap<String, u64>,
354    pub total_accesses: u64,
355    pub average_access_count: u64,
356}
357
358/// Summary of priority rules configuration
359#[derive(Debug, Clone)]
360pub struct PriorityRulesSummary {
361    pub field_rule_count: usize,
362    pub path_rule_count: usize,
363    pub type_rule_count: usize,
364    pub default_priority: Priority,
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_default_priority_rules() {
373        let service = PriorityService::new();
374        let path = JsonPath::new("$.id").unwrap();
375        let value = JsonValue::String("123".to_string());
376
377        let priority = service.calculate_priority(&path, &value);
378        assert_eq!(priority, Priority::CRITICAL);
379    }
380
381    #[test]
382    fn test_path_pattern_matching() {
383        let service = PriorityService::new();
384
385        // Test wildcard pattern
386        assert!(service.path_matches_pattern("$.users[0].id", "$.*.id"));
387        assert!(service.path_matches_pattern("$.posts[1].id", "$.*.id"));
388        assert!(!service.path_matches_pattern("$.users.name", "$.*.id"));
389    }
390
391    #[test]
392    fn test_custom_rules() {
393        let mut service = PriorityService::new();
394        service.add_field_rule("custom_field".to_string(), Priority::HIGH);
395
396        let path = JsonPath::new("$.custom_field").unwrap();
397        let value = JsonValue::String("test".to_string());
398
399        let priority = service.calculate_priority(&path, &value);
400        assert_eq!(priority, Priority::HIGH);
401    }
402
403    #[test]
404    fn test_heuristic_priority() {
405        let service = PriorityService::new();
406
407        // Shallow path should have higher priority
408        let shallow = JsonPath::new("$.name").unwrap();
409        let deep = JsonPath::new("$.user.profile.settings.theme.color").unwrap();
410        let value = JsonValue::String("test".to_string());
411
412        let shallow_priority = service.calculate_priority(&shallow, &value);
413        let deep_priority = service.calculate_priority(&deep, &value);
414
415        assert!(shallow_priority > deep_priority);
416    }
417
418    #[test]
419    fn test_calculate_all_priorities() {
420        let service = PriorityService::new();
421        let data = serde_json::json!({
422            "id": 123,
423            "name": "Test",
424            "details": {
425                "description": "A test object",
426                "metadata": {
427                    "created_at": "2023-01-01"
428                }
429            }
430        });
431
432        let priorities = service.calculate_priorities(&data).unwrap();
433
434        // Should have priorities for all paths
435        assert!(!priorities.is_empty());
436
437        // ID should be critical
438        let id_path = JsonPath::new("$.id").unwrap();
439        assert_eq!(priorities[&id_path], Priority::CRITICAL);
440
441        // Name should be high
442        let name_path = JsonPath::new("$.name").unwrap();
443        assert_eq!(priorities[&name_path], Priority::HIGH);
444    }
445}