clia_config_expr/
lib.rs

1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use thiserror::Error;
5
6/// Configuration expression error types
7#[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/// Operator enumeration
22#[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    /// Validate if the operator is valid
42    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/// Condition expression
59#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
60#[serde(untagged)]
61pub enum Condition {
62    /// Simple condition: field comparison
63    Simple {
64        field: String,
65        op: Operator,
66        value: String,
67    },
68    /// AND condition: all sub-conditions must be satisfied
69    And { and: Vec<Condition> },
70    /// OR condition: at least one sub-condition must be satisfied
71    Or { or: Vec<Condition> },
72}
73
74/// Rule return value, supports string or JSON object
75#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
76#[serde(untagged)]
77pub enum RuleResult {
78    String(String),
79    Object(serde_json::Value),
80}
81
82/// Single rule definition
83#[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/// Configuration rule set
92#[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/// Configuration expression evaluator
100#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
101pub struct ConfigEvaluator {
102    rules: ConfigRules,
103}
104
105impl ConfigEvaluator {
106    /// Create a new evaluator
107    pub fn new(rules: ConfigRules) -> Result<Self, ConfigExprError> {
108        // Validate rule set
109        Self::validate_rules(&rules)?;
110        Ok(Self { rules })
111    }
112
113    /// Create evaluator from JSON string
114    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    /// Evaluate request parameters and return matching result
120    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    /// Evaluate a single condition
130    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    /// Evaluate simple condition
141    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, // Return false if regex is invalid
162                }
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    /// Compare two strings as numbers
172    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, // Return false if any value cannot be parsed as a number
179        }
180    }
181
182    /// Validate if the rule set is valid
183    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    /// Validate if the condition is valid
198    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                // 验证正则表达式
213                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
249/// Convenience method: directly evaluate from JSON string
250pub 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
258/// Convenience method: validate if JSON rules are valid
259pub 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, &params).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, &params).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, &params).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, &params).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, &params).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, &params).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, &params).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, &params).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, &params).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, &params).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, &params).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        // 测试不满足条件的情况
719        let mut params = HashMap::new();
720        params.insert("score".to_string(), "75".to_string());
721
722        let result = evaluate_json(json, &params).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, &params).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, &params).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, &params).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        // 测试无效数字
835        let mut params = HashMap::new();
836        params.insert("value".to_string(), "not_a_number".to_string());
837
838        let result = evaluate_json(json, &params).unwrap();
839        assert!(result.is_some());
840
841        if let Some(RuleResult::String(s)) = result {
842            assert_eq!(s, "invalid"); // 应该使用fallback
843        } 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, &params).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}