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, 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)]
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)]
76#[serde(untagged)]
77pub enum RuleResult {
78 String(String),
79 Object(serde_json::Value),
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
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)]
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)]
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 Ok(());
186 }
190
191 for (index, rule) in rules.rules.iter().enumerate() {
192 Self::validate_condition(&rule.condition, index)?;
193 }
194
195 Ok(())
196 }
197
198 fn validate_condition(condition: &Condition, rule_index: usize) -> Result<(), ConfigExprError> {
200 match condition {
201 Condition::Simple { field, op, value } => {
202 if field.is_empty() {
203 return Err(ConfigExprError::ValidationError(format!(
204 "Field name cannot be empty in rule {}",
205 rule_index
206 )));
207 }
208
209 if !op.is_valid() {
210 return Err(ConfigExprError::InvalidOperator(format!("{:?}", op)));
211 }
212
213 if matches!(op, Operator::Regex) {
215 Regex::new(value).map_err(|e| {
216 ConfigExprError::ValidationError(format!(
217 "Invalid regex '{}' in rule {}: {}",
218 value, rule_index, e
219 ))
220 })?;
221 }
222 }
223 Condition::And { and } => {
224 if and.is_empty() {
225 return Err(ConfigExprError::ValidationError(format!(
226 "AND condition cannot be empty in rule {}",
227 rule_index
228 )));
229 }
230 for cond in and {
231 Self::validate_condition(cond, rule_index)?;
232 }
233 }
234 Condition::Or { or } => {
235 if or.is_empty() {
236 return Err(ConfigExprError::ValidationError(format!(
237 "OR condition cannot be empty in rule {}",
238 rule_index
239 )));
240 }
241 for cond in or {
242 Self::validate_condition(cond, rule_index)?;
243 }
244 }
245 }
246 Ok(())
247 }
248}
249
250pub fn evaluate_json(
252 json: &str,
253 params: &HashMap<String, String>,
254) -> Result<Option<RuleResult>, ConfigExprError> {
255 let evaluator = ConfigEvaluator::from_json(json)?;
256 Ok(evaluator.evaluate(params))
257}
258
259pub fn validate_json(json: &str) -> Result<(), ConfigExprError> {
261 let rules: ConfigRules = serde_json::from_str(json)?;
262 ConfigEvaluator::validate_rules(&rules)
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_simple_condition() {
271 let json = r#"
272 {
273 "rules": [
274 {
275 "if": {
276 "field": "platform",
277 "op": "equals",
278 "value": "RTD"
279 },
280 "then": "chip_rtd"
281 }
282 ]
283 }
284 "#;
285
286 let mut params = HashMap::new();
287 params.insert("platform".to_string(), "RTD".to_string());
288
289 let result = evaluate_json(json, ¶ms).unwrap();
290 assert!(result.is_some());
291
292 if let Some(RuleResult::String(s)) = result {
293 assert_eq!(s, "chip_rtd");
294 } else {
295 panic!("Expected string result");
296 }
297 }
298
299 #[test]
300 fn test_and_condition() {
301 let json = r#"
302 {
303 "rules": [
304 {
305 "if": {
306 "and": [
307 { "field": "platform", "op": "contains", "value": "RTD" },
308 { "field": "region", "op": "equals", "value": "CN" }
309 ]
310 },
311 "then": "chip_rtd_cn"
312 }
313 ]
314 }
315 "#;
316
317 let mut params = HashMap::new();
318 params.insert("platform".to_string(), "RTD-2000".to_string());
319 params.insert("region".to_string(), "CN".to_string());
320
321 let result = evaluate_json(json, ¶ms).unwrap();
322 assert!(result.is_some());
323
324 if let Some(RuleResult::String(s)) = result {
325 assert_eq!(s, "chip_rtd_cn");
326 } else {
327 panic!("Expected string result");
328 }
329 }
330
331 #[test]
332 fn test_or_condition() {
333 let json = r#"
334 {
335 "rules": [
336 {
337 "if": {
338 "or": [
339 { "field": "platform", "op": "equals", "value": "MT9950" },
340 { "field": "platform", "op": "equals", "value": "MT9638" }
341 ]
342 },
343 "then": "chip_mt"
344 }
345 ]
346 }
347 "#;
348
349 let mut params = HashMap::new();
350 params.insert("platform".to_string(), "MT9950".to_string());
351
352 let result = evaluate_json(json, ¶ms).unwrap();
353 assert!(result.is_some());
354
355 if let Some(RuleResult::String(s)) = result {
356 assert_eq!(s, "chip_mt");
357 } else {
358 panic!("Expected string result");
359 }
360 }
361
362 #[test]
363 fn test_prefix_condition() {
364 let json = r#"
365 {
366 "rules": [
367 {
368 "if": {
369 "field": "platform",
370 "op": "prefix",
371 "value": "Hi"
372 },
373 "then": "chip_hi"
374 }
375 ]
376 }
377 "#;
378
379 let mut params = HashMap::new();
380 params.insert("platform".to_string(), "Hi3516".to_string());
381
382 let result = evaluate_json(json, ¶ms).unwrap();
383 assert!(result.is_some());
384
385 if let Some(RuleResult::String(s)) = result {
386 assert_eq!(s, "chip_hi");
387 } else {
388 panic!("Expected string result");
389 }
390 }
391
392 #[test]
393 fn test_json_object_result() {
394 let json = r#"
395 {
396 "rules": [
397 {
398 "if": {
399 "field": "platform",
400 "op": "equals",
401 "value": "RTD"
402 },
403 "then": {
404 "chip": "rtd",
405 "config": {
406 "memory": "2GB",
407 "cpu": "ARM"
408 }
409 }
410 }
411 ]
412 }
413 "#;
414
415 let mut params = HashMap::new();
416 params.insert("platform".to_string(), "RTD".to_string());
417
418 let result = evaluate_json(json, ¶ms).unwrap();
419 assert!(result.is_some());
420
421 if let Some(RuleResult::Object(obj)) = result {
422 assert_eq!(obj["chip"], "rtd");
423 assert_eq!(obj["config"]["memory"], "2GB");
424 } else {
425 panic!("Expected object result");
426 }
427 }
428
429 #[test]
430 fn test_fallback() {
431 let json = r#"
432 {
433 "rules": [
434 {
435 "if": {
436 "field": "platform",
437 "op": "equals",
438 "value": "RTD"
439 },
440 "then": "chip_rtd"
441 }
442 ],
443 "fallback": "default_chip"
444 }
445 "#;
446
447 let mut params = HashMap::new();
448 params.insert("platform".to_string(), "Unknown".to_string());
449
450 let result = evaluate_json(json, ¶ms).unwrap();
451 assert!(result.is_some());
452
453 if let Some(RuleResult::String(s)) = result {
454 assert_eq!(s, "default_chip");
455 } else {
456 panic!("Expected string result");
457 }
458 }
459
460 #[test]
461 fn test_no_match_no_fallback() {
462 let json = r#"
463 {
464 "rules": [
465 {
466 "if": {
467 "field": "platform",
468 "op": "equals",
469 "value": "RTD"
470 },
471 "then": "chip_rtd"
472 }
473 ]
474 }
475 "#;
476
477 let mut params = HashMap::new();
478 params.insert("platform".to_string(), "Unknown".to_string());
479
480 let result = evaluate_json(json, ¶ms).unwrap();
481 assert!(result.is_none());
482 }
483
484 #[test]
485 fn test_regex_condition() {
486 let json = r#"
487 {
488 "rules": [
489 {
490 "if": {
491 "field": "platform",
492 "op": "regex",
493 "value": "^Hi\\d+"
494 },
495 "then": "chip_hi"
496 }
497 ]
498 }
499 "#;
500
501 let mut params = HashMap::new();
502 params.insert("platform".to_string(), "Hi3516".to_string());
503
504 let result = evaluate_json(json, ¶ms).unwrap();
505 assert!(result.is_some());
506
507 if let Some(RuleResult::String(s)) = result {
508 assert_eq!(s, "chip_hi");
509 } else {
510 panic!("Expected string result");
511 }
512 }
513
514 #[test]
515 fn test_suffix_condition() {
516 let json = r#"
517 {
518 "rules": [
519 {
520 "if": {
521 "field": "platform",
522 "op": "suffix",
523 "value": "Pro"
524 },
525 "then": "chip_pro"
526 }
527 ]
528 }
529 "#;
530
531 let mut params = HashMap::new();
532 params.insert("platform".to_string(), "RTD-Pro".to_string());
533
534 let result = evaluate_json(json, ¶ms).unwrap();
535 assert!(result.is_some());
536
537 if let Some(RuleResult::String(s)) = result {
538 assert_eq!(s, "chip_pro");
539 } else {
540 panic!("Expected string result");
541 }
542 }
543
544 #[test]
545 fn test_validation_empty_rules() {
546 let json = r#"
547 {
548 "rules": []
549 }
550 "#;
551
552 let result = validate_json(json);
553 assert!(result.is_err());
554 assert!(result
555 .unwrap_err()
556 .to_string()
557 .contains("Rules cannot be empty"));
558 }
559
560 #[test]
561 fn test_validation_empty_field() {
562 let json = r#"
563 {
564 "rules": [
565 {
566 "if": {
567 "field": "",
568 "op": "equals",
569 "value": "RTD"
570 },
571 "then": "chip_rtd"
572 }
573 ]
574 }
575 "#;
576
577 let result = validate_json(json);
578 assert!(result.is_err());
579 assert!(result
580 .unwrap_err()
581 .to_string()
582 .contains("Field name cannot be empty"));
583 }
584
585 #[test]
586 fn test_validation_invalid_regex() {
587 let json = r#"
588 {
589 "rules": [
590 {
591 "if": {
592 "field": "platform",
593 "op": "regex",
594 "value": "[invalid"
595 },
596 "then": "chip_rtd"
597 }
598 ]
599 }
600 "#;
601
602 let result = validate_json(json);
603 assert!(result.is_err());
604 assert!(result.unwrap_err().to_string().contains("Invalid regex"));
605 }
606
607 #[test]
608 fn test_validation_empty_and_condition() {
609 let json = r#"
610 {
611 "rules": [
612 {
613 "if": {
614 "and": []
615 },
616 "then": "chip_rtd"
617 }
618 ]
619 }
620 "#;
621
622 let result = validate_json(json);
623 assert!(result.is_err());
624 assert!(result
625 .unwrap_err()
626 .to_string()
627 .contains("AND condition cannot be empty"));
628 }
629
630 #[test]
631 fn test_validation_empty_or_condition() {
632 let json = r#"
633 {
634 "rules": [
635 {
636 "if": {
637 "or": []
638 },
639 "then": "chip_rtd"
640 }
641 ]
642 }
643 "#;
644
645 let result = validate_json(json);
646 assert!(result.is_err());
647 assert!(result
648 .unwrap_err()
649 .to_string()
650 .contains("OR condition cannot be empty"));
651 }
652
653 #[test]
654 fn test_complex_nested_conditions() {
655 let json = r#"
656 {
657 "rules": [
658 {
659 "if": {
660 "and": [
661 {
662 "or": [
663 { "field": "platform", "op": "prefix", "value": "Hi" },
664 { "field": "platform", "op": "prefix", "value": "MT" }
665 ]
666 },
667 { "field": "region", "op": "equals", "value": "CN" }
668 ]
669 },
670 "then": "chip_cn"
671 }
672 ]
673 }
674 "#;
675
676 let mut params = HashMap::new();
677 params.insert("platform".to_string(), "Hi3516".to_string());
678 params.insert("region".to_string(), "CN".to_string());
679
680 let result = evaluate_json(json, ¶ms).unwrap();
681 assert!(result.is_some());
682
683 if let Some(RuleResult::String(s)) = result {
684 assert_eq!(s, "chip_cn");
685 } else {
686 panic!("Expected string result");
687 }
688 }
689
690 #[test]
691 fn test_greater_than_condition() {
692 let json = r#"
693 {
694 "rules": [
695 {
696 "if": {
697 "field": "score",
698 "op": "gt",
699 "value": "80"
700 },
701 "then": "high_score"
702 }
703 ]
704 }
705 "#;
706
707 let mut params = HashMap::new();
708 params.insert("score".to_string(), "85".to_string());
709
710 let result = evaluate_json(json, ¶ms).unwrap();
711 assert!(result.is_some());
712
713 if let Some(RuleResult::String(s)) = result {
714 assert_eq!(s, "high_score");
715 } else {
716 panic!("Expected string result");
717 }
718
719 let mut params = HashMap::new();
721 params.insert("score".to_string(), "75".to_string());
722
723 let result = evaluate_json(json, ¶ms).unwrap();
724 assert!(result.is_none());
725 }
726
727 #[test]
728 fn test_less_than_condition() {
729 let json = r#"
730 {
731 "rules": [
732 {
733 "if": {
734 "field": "age",
735 "op": "lt",
736 "value": "18"
737 },
738 "then": "minor"
739 }
740 ]
741 }
742 "#;
743
744 let mut params = HashMap::new();
745 params.insert("age".to_string(), "16".to_string());
746
747 let result = evaluate_json(json, ¶ms).unwrap();
748 assert!(result.is_some());
749
750 if let Some(RuleResult::String(s)) = result {
751 assert_eq!(s, "minor");
752 } else {
753 panic!("Expected string result");
754 }
755 }
756
757 #[test]
758 fn test_greater_than_or_equal_condition() {
759 let json = r#"
760 {
761 "rules": [
762 {
763 "if": {
764 "field": "level",
765 "op": "ge",
766 "value": "5"
767 },
768 "then": "advanced"
769 }
770 ]
771 }
772 "#;
773
774 let mut params = HashMap::new();
775 params.insert("level".to_string(), "5".to_string());
776
777 let result = evaluate_json(json, ¶ms).unwrap();
778 assert!(result.is_some());
779
780 if let Some(RuleResult::String(s)) = result {
781 assert_eq!(s, "advanced");
782 } else {
783 panic!("Expected string result");
784 }
785 }
786
787 #[test]
788 fn test_less_than_or_equal_condition() {
789 let json = r#"
790 {
791 "rules": [
792 {
793 "if": {
794 "field": "temperature",
795 "op": "le",
796 "value": "25.5"
797 },
798 "then": "cool"
799 }
800 ]
801 }
802 "#;
803
804 let mut params = HashMap::new();
805 params.insert("temperature".to_string(), "23.8".to_string());
806
807 let result = evaluate_json(json, ¶ms).unwrap();
808 assert!(result.is_some());
809
810 if let Some(RuleResult::String(s)) = result {
811 assert_eq!(s, "cool");
812 } else {
813 panic!("Expected string result");
814 }
815 }
816
817 #[test]
818 fn test_numeric_comparison_with_invalid_numbers() {
819 let json = r#"
820 {
821 "rules": [
822 {
823 "if": {
824 "field": "value",
825 "op": "gt",
826 "value": "10"
827 },
828 "then": "valid"
829 }
830 ],
831 "fallback": "invalid"
832 }
833 "#;
834
835 let mut params = HashMap::new();
837 params.insert("value".to_string(), "not_a_number".to_string());
838
839 let result = evaluate_json(json, ¶ms).unwrap();
840 assert!(result.is_some());
841
842 if let Some(RuleResult::String(s)) = result {
843 assert_eq!(s, "invalid"); } else {
845 panic!("Expected string result");
846 }
847 }
848
849 #[test]
850 fn test_numeric_comparison_with_decimal_numbers() {
851 let json = r#"
852 {
853 "rules": [
854 {
855 "if": {
856 "field": "price",
857 "op": "ge",
858 "value": "99.99"
859 },
860 "then": "expensive"
861 }
862 ]
863 }
864 "#;
865
866 let mut params = HashMap::new();
867 params.insert("price".to_string(), "100.50".to_string());
868
869 let result = evaluate_json(json, ¶ms).unwrap();
870 assert!(result.is_some());
871
872 if let Some(RuleResult::String(s)) = result {
873 assert_eq!(s, "expensive");
874 } else {
875 panic!("Expected string result");
876 }
877 }
878}