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