1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
8pub enum ConfigExprError {
9 #[error("Invalid operator: {0}")]
10 InvalidOperator(String),
11 #[error("Field not found: {0}")]
12 FieldNotFound(String),
13 #[error("Regex compilation error: {0}")]
14 RegexError(#[from] regex::Error),
15 #[error("JSON serialization error: {0}")]
16 JsonError(#[from] serde_json::Error),
17 #[error("Validation error: {0}")]
18 ValidationError(String),
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
23#[serde(rename_all = "lowercase")]
24pub enum Operator {
25 Equals,
26 Contains,
27 Prefix,
28 Suffix,
29 Regex,
30 #[serde(rename = "gt")]
31 GreaterThan,
32 #[serde(rename = "lt")]
33 LessThan,
34 #[serde(rename = "ge")]
35 GreaterThanOrEqual,
36 #[serde(rename = "le")]
37 LessThanOrEqual,
38}
39
40impl Operator {
41 pub fn is_valid(&self) -> bool {
43 matches!(
44 self,
45 Operator::Equals
46 | Operator::Contains
47 | Operator::Prefix
48 | Operator::Suffix
49 | Operator::Regex
50 | Operator::GreaterThan
51 | Operator::LessThan
52 | Operator::GreaterThanOrEqual
53 | Operator::LessThanOrEqual
54 )
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
60#[serde(untagged)]
61pub enum Condition {
62 Simple {
64 field: String,
65 op: Operator,
66 value: String,
67 },
68 And { and: Vec<Condition> },
70 Or { or: Vec<Condition> },
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
76#[serde(untagged)]
77pub enum RuleResult {
78 String(String),
79 Object(serde_json::Value),
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
84pub struct Rule {
85 #[serde(rename = "if")]
86 pub condition: Condition,
87 #[serde(rename = "then")]
88 pub result: RuleResult,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
93pub struct ConfigRules {
94 pub rules: Vec<Rule>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub fallback: Option<RuleResult>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
101pub struct ConfigEvaluator {
102 rules: ConfigRules,
103}
104
105impl ConfigEvaluator {
106 pub fn new(rules: ConfigRules) -> Result<Self, ConfigExprError> {
108 Self::validate_rules(&rules)?;
110 Ok(Self { rules })
111 }
112
113 pub fn from_json(json: &str) -> Result<Self, ConfigExprError> {
115 let rules: ConfigRules = serde_json::from_str(json)?;
116 Self::new(rules)
117 }
118
119 pub fn evaluate(&self, params: &HashMap<String, String>) -> Option<RuleResult> {
121 for rule in &self.rules.rules {
122 if self.evaluate_condition(&rule.condition, params) {
123 return Some(rule.result.clone());
124 }
125 }
126 self.rules.fallback.clone()
127 }
128
129 fn evaluate_condition(&self, condition: &Condition, params: &HashMap<String, String>) -> bool {
131 match condition {
132 Condition::Simple { field, op, value } => {
133 self.evaluate_simple_condition(field, op, value, params)
134 }
135 Condition::And { and } => and.iter().all(|cond| self.evaluate_condition(cond, params)),
136 Condition::Or { or } => or.iter().any(|cond| self.evaluate_condition(cond, params)),
137 }
138 }
139
140 fn evaluate_simple_condition(
142 &self,
143 field: &str,
144 op: &Operator,
145 value: &str,
146 params: &HashMap<String, String>,
147 ) -> bool {
148 let field_value = match params.get(field) {
149 Some(v) => v,
150 None => return false,
151 };
152
153 match op {
154 Operator::Equals => field_value == value,
155 Operator::Contains => field_value.contains(value),
156 Operator::Prefix => field_value.starts_with(value),
157 Operator::Suffix => field_value.ends_with(value),
158 Operator::Regex => {
159 match Regex::new(value) {
160 Ok(regex) => regex.is_match(field_value),
161 Err(_) => false, }
163 }
164 Operator::GreaterThan => self.compare_numbers(field_value, value, |a, b| a > b),
165 Operator::LessThan => self.compare_numbers(field_value, value, |a, b| a < b),
166 Operator::GreaterThanOrEqual => self.compare_numbers(field_value, value, |a, b| a >= b),
167 Operator::LessThanOrEqual => self.compare_numbers(field_value, value, |a, b| a <= b),
168 }
169 }
170
171 fn compare_numbers<F>(&self, field_value: &str, target_value: &str, compare_fn: F) -> bool
173 where
174 F: Fn(f64, f64) -> bool,
175 {
176 match (field_value.parse::<f64>(), target_value.parse::<f64>()) {
177 (Ok(field_num), Ok(target_num)) => compare_fn(field_num, target_num),
178 _ => false, }
180 }
181
182 fn validate_rules(rules: &ConfigRules) -> Result<(), ConfigExprError> {
184 if rules.rules.is_empty() {
185 return Err(ConfigExprError::ValidationError(
186 "Rules cannot be empty".to_string(),
187 ));
188 }
189
190 for (index, rule) in rules.rules.iter().enumerate() {
191 Self::validate_condition(&rule.condition, index)?;
192 }
193
194 Ok(())
195 }
196
197 fn validate_condition(condition: &Condition, rule_index: usize) -> Result<(), ConfigExprError> {
199 match condition {
200 Condition::Simple { field, op, value } => {
201 if field.is_empty() {
202 return Err(ConfigExprError::ValidationError(format!(
203 "Field name cannot be empty in rule {}",
204 rule_index
205 )));
206 }
207
208 if !op.is_valid() {
209 return Err(ConfigExprError::InvalidOperator(format!("{:?}", op)));
210 }
211
212 if matches!(op, Operator::Regex) {
214 Regex::new(value).map_err(|e| {
215 ConfigExprError::ValidationError(format!(
216 "Invalid regex '{}' in rule {}: {}",
217 value, rule_index, e
218 ))
219 })?;
220 }
221 }
222 Condition::And { and } => {
223 if and.is_empty() {
224 return Err(ConfigExprError::ValidationError(format!(
225 "AND condition cannot be empty in rule {}",
226 rule_index
227 )));
228 }
229 for cond in and {
230 Self::validate_condition(cond, rule_index)?;
231 }
232 }
233 Condition::Or { or } => {
234 if or.is_empty() {
235 return Err(ConfigExprError::ValidationError(format!(
236 "OR condition cannot be empty in rule {}",
237 rule_index
238 )));
239 }
240 for cond in or {
241 Self::validate_condition(cond, rule_index)?;
242 }
243 }
244 }
245 Ok(())
246 }
247}
248
249pub fn evaluate_json(
251 json: &str,
252 params: &HashMap<String, String>,
253) -> Result<Option<RuleResult>, ConfigExprError> {
254 let evaluator = ConfigEvaluator::from_json(json)?;
255 Ok(evaluator.evaluate(params))
256}
257
258pub fn validate_json(json: &str) -> Result<(), ConfigExprError> {
260 let rules: ConfigRules = serde_json::from_str(json)?;
261 ConfigEvaluator::validate_rules(&rules)
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_simple_condition() {
270 let json = r#"
271 {
272 "rules": [
273 {
274 "if": {
275 "field": "platform",
276 "op": "equals",
277 "value": "RTD"
278 },
279 "then": "chip_rtd"
280 }
281 ]
282 }
283 "#;
284
285 let mut params = HashMap::new();
286 params.insert("platform".to_string(), "RTD".to_string());
287
288 let result = evaluate_json(json, ¶ms).unwrap();
289 assert!(result.is_some());
290
291 if let Some(RuleResult::String(s)) = result {
292 assert_eq!(s, "chip_rtd");
293 } else {
294 panic!("Expected string result");
295 }
296 }
297
298 #[test]
299 fn test_and_condition() {
300 let json = r#"
301 {
302 "rules": [
303 {
304 "if": {
305 "and": [
306 { "field": "platform", "op": "contains", "value": "RTD" },
307 { "field": "region", "op": "equals", "value": "CN" }
308 ]
309 },
310 "then": "chip_rtd_cn"
311 }
312 ]
313 }
314 "#;
315
316 let mut params = HashMap::new();
317 params.insert("platform".to_string(), "RTD-2000".to_string());
318 params.insert("region".to_string(), "CN".to_string());
319
320 let result = evaluate_json(json, ¶ms).unwrap();
321 assert!(result.is_some());
322
323 if let Some(RuleResult::String(s)) = result {
324 assert_eq!(s, "chip_rtd_cn");
325 } else {
326 panic!("Expected string result");
327 }
328 }
329
330 #[test]
331 fn test_or_condition() {
332 let json = r#"
333 {
334 "rules": [
335 {
336 "if": {
337 "or": [
338 { "field": "platform", "op": "equals", "value": "MT9950" },
339 { "field": "platform", "op": "equals", "value": "MT9638" }
340 ]
341 },
342 "then": "chip_mt"
343 }
344 ]
345 }
346 "#;
347
348 let mut params = HashMap::new();
349 params.insert("platform".to_string(), "MT9950".to_string());
350
351 let result = evaluate_json(json, ¶ms).unwrap();
352 assert!(result.is_some());
353
354 if let Some(RuleResult::String(s)) = result {
355 assert_eq!(s, "chip_mt");
356 } else {
357 panic!("Expected string result");
358 }
359 }
360
361 #[test]
362 fn test_prefix_condition() {
363 let json = r#"
364 {
365 "rules": [
366 {
367 "if": {
368 "field": "platform",
369 "op": "prefix",
370 "value": "Hi"
371 },
372 "then": "chip_hi"
373 }
374 ]
375 }
376 "#;
377
378 let mut params = HashMap::new();
379 params.insert("platform".to_string(), "Hi3516".to_string());
380
381 let result = evaluate_json(json, ¶ms).unwrap();
382 assert!(result.is_some());
383
384 if let Some(RuleResult::String(s)) = result {
385 assert_eq!(s, "chip_hi");
386 } else {
387 panic!("Expected string result");
388 }
389 }
390
391 #[test]
392 fn test_json_object_result() {
393 let json = r#"
394 {
395 "rules": [
396 {
397 "if": {
398 "field": "platform",
399 "op": "equals",
400 "value": "RTD"
401 },
402 "then": {
403 "chip": "rtd",
404 "config": {
405 "memory": "2GB",
406 "cpu": "ARM"
407 }
408 }
409 }
410 ]
411 }
412 "#;
413
414 let mut params = HashMap::new();
415 params.insert("platform".to_string(), "RTD".to_string());
416
417 let result = evaluate_json(json, ¶ms).unwrap();
418 assert!(result.is_some());
419
420 if let Some(RuleResult::Object(obj)) = result {
421 assert_eq!(obj["chip"], "rtd");
422 assert_eq!(obj["config"]["memory"], "2GB");
423 } else {
424 panic!("Expected object result");
425 }
426 }
427
428 #[test]
429 fn test_fallback() {
430 let json = r#"
431 {
432 "rules": [
433 {
434 "if": {
435 "field": "platform",
436 "op": "equals",
437 "value": "RTD"
438 },
439 "then": "chip_rtd"
440 }
441 ],
442 "fallback": "default_chip"
443 }
444 "#;
445
446 let mut params = HashMap::new();
447 params.insert("platform".to_string(), "Unknown".to_string());
448
449 let result = evaluate_json(json, ¶ms).unwrap();
450 assert!(result.is_some());
451
452 if let Some(RuleResult::String(s)) = result {
453 assert_eq!(s, "default_chip");
454 } else {
455 panic!("Expected string result");
456 }
457 }
458
459 #[test]
460 fn test_no_match_no_fallback() {
461 let json = r#"
462 {
463 "rules": [
464 {
465 "if": {
466 "field": "platform",
467 "op": "equals",
468 "value": "RTD"
469 },
470 "then": "chip_rtd"
471 }
472 ]
473 }
474 "#;
475
476 let mut params = HashMap::new();
477 params.insert("platform".to_string(), "Unknown".to_string());
478
479 let result = evaluate_json(json, ¶ms).unwrap();
480 assert!(result.is_none());
481 }
482
483 #[test]
484 fn test_regex_condition() {
485 let json = r#"
486 {
487 "rules": [
488 {
489 "if": {
490 "field": "platform",
491 "op": "regex",
492 "value": "^Hi\\d+"
493 },
494 "then": "chip_hi"
495 }
496 ]
497 }
498 "#;
499
500 let mut params = HashMap::new();
501 params.insert("platform".to_string(), "Hi3516".to_string());
502
503 let result = evaluate_json(json, ¶ms).unwrap();
504 assert!(result.is_some());
505
506 if let Some(RuleResult::String(s)) = result {
507 assert_eq!(s, "chip_hi");
508 } else {
509 panic!("Expected string result");
510 }
511 }
512
513 #[test]
514 fn test_suffix_condition() {
515 let json = r#"
516 {
517 "rules": [
518 {
519 "if": {
520 "field": "platform",
521 "op": "suffix",
522 "value": "Pro"
523 },
524 "then": "chip_pro"
525 }
526 ]
527 }
528 "#;
529
530 let mut params = HashMap::new();
531 params.insert("platform".to_string(), "RTD-Pro".to_string());
532
533 let result = evaluate_json(json, ¶ms).unwrap();
534 assert!(result.is_some());
535
536 if let Some(RuleResult::String(s)) = result {
537 assert_eq!(s, "chip_pro");
538 } else {
539 panic!("Expected string result");
540 }
541 }
542
543 #[test]
544 fn test_validation_empty_rules() {
545 let json = r#"
546 {
547 "rules": []
548 }
549 "#;
550
551 let result = validate_json(json);
552 assert!(result.is_err());
553 assert!(result
554 .unwrap_err()
555 .to_string()
556 .contains("Rules cannot be empty"));
557 }
558
559 #[test]
560 fn test_validation_empty_field() {
561 let json = r#"
562 {
563 "rules": [
564 {
565 "if": {
566 "field": "",
567 "op": "equals",
568 "value": "RTD"
569 },
570 "then": "chip_rtd"
571 }
572 ]
573 }
574 "#;
575
576 let result = validate_json(json);
577 assert!(result.is_err());
578 assert!(result
579 .unwrap_err()
580 .to_string()
581 .contains("Field name cannot be empty"));
582 }
583
584 #[test]
585 fn test_validation_invalid_regex() {
586 let json = r#"
587 {
588 "rules": [
589 {
590 "if": {
591 "field": "platform",
592 "op": "regex",
593 "value": "[invalid"
594 },
595 "then": "chip_rtd"
596 }
597 ]
598 }
599 "#;
600
601 let result = validate_json(json);
602 assert!(result.is_err());
603 assert!(result.unwrap_err().to_string().contains("Invalid regex"));
604 }
605
606 #[test]
607 fn test_validation_empty_and_condition() {
608 let json = r#"
609 {
610 "rules": [
611 {
612 "if": {
613 "and": []
614 },
615 "then": "chip_rtd"
616 }
617 ]
618 }
619 "#;
620
621 let result = validate_json(json);
622 assert!(result.is_err());
623 assert!(result
624 .unwrap_err()
625 .to_string()
626 .contains("AND condition cannot be empty"));
627 }
628
629 #[test]
630 fn test_validation_empty_or_condition() {
631 let json = r#"
632 {
633 "rules": [
634 {
635 "if": {
636 "or": []
637 },
638 "then": "chip_rtd"
639 }
640 ]
641 }
642 "#;
643
644 let result = validate_json(json);
645 assert!(result.is_err());
646 assert!(result
647 .unwrap_err()
648 .to_string()
649 .contains("OR condition cannot be empty"));
650 }
651
652 #[test]
653 fn test_complex_nested_conditions() {
654 let json = r#"
655 {
656 "rules": [
657 {
658 "if": {
659 "and": [
660 {
661 "or": [
662 { "field": "platform", "op": "prefix", "value": "Hi" },
663 { "field": "platform", "op": "prefix", "value": "MT" }
664 ]
665 },
666 { "field": "region", "op": "equals", "value": "CN" }
667 ]
668 },
669 "then": "chip_cn"
670 }
671 ]
672 }
673 "#;
674
675 let mut params = HashMap::new();
676 params.insert("platform".to_string(), "Hi3516".to_string());
677 params.insert("region".to_string(), "CN".to_string());
678
679 let result = evaluate_json(json, ¶ms).unwrap();
680 assert!(result.is_some());
681
682 if let Some(RuleResult::String(s)) = result {
683 assert_eq!(s, "chip_cn");
684 } else {
685 panic!("Expected string result");
686 }
687 }
688
689 #[test]
690 fn test_greater_than_condition() {
691 let json = r#"
692 {
693 "rules": [
694 {
695 "if": {
696 "field": "score",
697 "op": "gt",
698 "value": "80"
699 },
700 "then": "high_score"
701 }
702 ]
703 }
704 "#;
705
706 let mut params = HashMap::new();
707 params.insert("score".to_string(), "85".to_string());
708
709 let result = evaluate_json(json, ¶ms).unwrap();
710 assert!(result.is_some());
711
712 if let Some(RuleResult::String(s)) = result {
713 assert_eq!(s, "high_score");
714 } else {
715 panic!("Expected string result");
716 }
717
718 let mut params = HashMap::new();
720 params.insert("score".to_string(), "75".to_string());
721
722 let result = evaluate_json(json, ¶ms).unwrap();
723 assert!(result.is_none());
724 }
725
726 #[test]
727 fn test_less_than_condition() {
728 let json = r#"
729 {
730 "rules": [
731 {
732 "if": {
733 "field": "age",
734 "op": "lt",
735 "value": "18"
736 },
737 "then": "minor"
738 }
739 ]
740 }
741 "#;
742
743 let mut params = HashMap::new();
744 params.insert("age".to_string(), "16".to_string());
745
746 let result = evaluate_json(json, ¶ms).unwrap();
747 assert!(result.is_some());
748
749 if let Some(RuleResult::String(s)) = result {
750 assert_eq!(s, "minor");
751 } else {
752 panic!("Expected string result");
753 }
754 }
755
756 #[test]
757 fn test_greater_than_or_equal_condition() {
758 let json = r#"
759 {
760 "rules": [
761 {
762 "if": {
763 "field": "level",
764 "op": "ge",
765 "value": "5"
766 },
767 "then": "advanced"
768 }
769 ]
770 }
771 "#;
772
773 let mut params = HashMap::new();
774 params.insert("level".to_string(), "5".to_string());
775
776 let result = evaluate_json(json, ¶ms).unwrap();
777 assert!(result.is_some());
778
779 if let Some(RuleResult::String(s)) = result {
780 assert_eq!(s, "advanced");
781 } else {
782 panic!("Expected string result");
783 }
784 }
785
786 #[test]
787 fn test_less_than_or_equal_condition() {
788 let json = r#"
789 {
790 "rules": [
791 {
792 "if": {
793 "field": "temperature",
794 "op": "le",
795 "value": "25.5"
796 },
797 "then": "cool"
798 }
799 ]
800 }
801 "#;
802
803 let mut params = HashMap::new();
804 params.insert("temperature".to_string(), "23.8".to_string());
805
806 let result = evaluate_json(json, ¶ms).unwrap();
807 assert!(result.is_some());
808
809 if let Some(RuleResult::String(s)) = result {
810 assert_eq!(s, "cool");
811 } else {
812 panic!("Expected string result");
813 }
814 }
815
816 #[test]
817 fn test_numeric_comparison_with_invalid_numbers() {
818 let json = r#"
819 {
820 "rules": [
821 {
822 "if": {
823 "field": "value",
824 "op": "gt",
825 "value": "10"
826 },
827 "then": "valid"
828 }
829 ],
830 "fallback": "invalid"
831 }
832 "#;
833
834 let mut params = HashMap::new();
836 params.insert("value".to_string(), "not_a_number".to_string());
837
838 let result = evaluate_json(json, ¶ms).unwrap();
839 assert!(result.is_some());
840
841 if let Some(RuleResult::String(s)) = result {
842 assert_eq!(s, "invalid"); } else {
844 panic!("Expected string result");
845 }
846 }
847
848 #[test]
849 fn test_numeric_comparison_with_decimal_numbers() {
850 let json = r#"
851 {
852 "rules": [
853 {
854 "if": {
855 "field": "price",
856 "op": "ge",
857 "value": "99.99"
858 },
859 "then": "expensive"
860 }
861 ]
862 }
863 "#;
864
865 let mut params = HashMap::new();
866 params.insert("price".to_string(), "100.50".to_string());
867
868 let result = evaluate_json(json, ¶ms).unwrap();
869 assert!(result.is_some());
870
871 if let Some(RuleResult::String(s)) = result {
872 assert_eq!(s, "expensive");
873 } else {
874 panic!("Expected string result");
875 }
876 }
877}