clia_config_expr/
lib.rs

1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use thiserror::Error;
5
6/// 配置表达式错误类型
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/// 操作符枚举
22#[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}
31
32impl Operator {
33    /// 验证操作符是否有效
34    pub fn is_valid(&self) -> bool {
35        matches!(
36            self,
37            Operator::Equals
38                | Operator::Contains
39                | Operator::Prefix
40                | Operator::Suffix
41                | Operator::Regex
42        )
43    }
44}
45
46/// 条件表达式
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(untagged)]
49pub enum Condition {
50    /// 简单条件:字段比较
51    Simple {
52        field: String,
53        op: Operator,
54        value: String,
55    },
56    /// AND 条件:所有子条件都必须满足
57    And { and: Vec<Condition> },
58    /// OR 条件:至少一个子条件满足
59    Or { or: Vec<Condition> },
60}
61
62/// 规则的返回值,支持字符串或JSON对象
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(untagged)]
65pub enum RuleResult {
66    String(String),
67    Object(serde_json::Value),
68}
69
70/// 单个规则定义
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Rule {
73    #[serde(rename = "if")]
74    pub condition: Condition,
75    #[serde(rename = "then")]
76    pub result: RuleResult,
77}
78
79/// 配置规则集
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ConfigRules {
82    pub rules: Vec<Rule>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub fallback: Option<RuleResult>,
85}
86
87/// 配置表达式评估器
88pub struct ConfigEvaluator {
89    rules: ConfigRules,
90}
91
92impl ConfigEvaluator {
93    /// 创建新的评估器
94    pub fn new(rules: ConfigRules) -> Result<Self, ConfigExprError> {
95        // 验证规则集
96        Self::validate_rules(&rules)?;
97        Ok(Self { rules })
98    }
99
100    /// 从JSON字符串创建评估器
101    pub fn from_json(json: &str) -> Result<Self, ConfigExprError> {
102        let rules: ConfigRules = serde_json::from_str(json)?;
103        Self::new(rules)
104    }
105
106    /// 评估请求参数,返回匹配的结果
107    pub fn evaluate(&self, params: &HashMap<String, String>) -> Option<RuleResult> {
108        for rule in &self.rules.rules {
109            if self.evaluate_condition(&rule.condition, params) {
110                return Some(rule.result.clone());
111            }
112        }
113        self.rules.fallback.clone()
114    }
115
116    /// 评估单个条件
117    fn evaluate_condition(&self, condition: &Condition, params: &HashMap<String, String>) -> bool {
118        match condition {
119            Condition::Simple { field, op, value } => {
120                self.evaluate_simple_condition(field, op, value, params)
121            }
122            Condition::And { and } => and.iter().all(|cond| self.evaluate_condition(cond, params)),
123            Condition::Or { or } => or.iter().any(|cond| self.evaluate_condition(cond, params)),
124        }
125    }
126
127    /// 评估简单条件
128    fn evaluate_simple_condition(
129        &self,
130        field: &str,
131        op: &Operator,
132        value: &str,
133        params: &HashMap<String, String>,
134    ) -> bool {
135        let field_value = match params.get(field) {
136            Some(v) => v,
137            None => return false,
138        };
139
140        match op {
141            Operator::Equals => field_value == value,
142            Operator::Contains => field_value.contains(value),
143            Operator::Prefix => field_value.starts_with(value),
144            Operator::Suffix => field_value.ends_with(value),
145            Operator::Regex => {
146                match Regex::new(value) {
147                    Ok(regex) => regex.is_match(field_value),
148                    Err(_) => false, // 正则表达式无效时返回false
149                }
150            }
151        }
152    }
153
154    /// 验证规则集是否合法
155    fn validate_rules(rules: &ConfigRules) -> Result<(), ConfigExprError> {
156        if rules.rules.is_empty() {
157            return Err(ConfigExprError::ValidationError(
158                "Rules cannot be empty".to_string(),
159            ));
160        }
161
162        for (index, rule) in rules.rules.iter().enumerate() {
163            Self::validate_condition(&rule.condition, index)?;
164        }
165
166        Ok(())
167    }
168
169    /// 验证条件是否合法
170    fn validate_condition(condition: &Condition, rule_index: usize) -> Result<(), ConfigExprError> {
171        match condition {
172            Condition::Simple { field, op, value } => {
173                if field.is_empty() {
174                    return Err(ConfigExprError::ValidationError(format!(
175                        "Field name cannot be empty in rule {}",
176                        rule_index
177                    )));
178                }
179
180                if !op.is_valid() {
181                    return Err(ConfigExprError::InvalidOperator(format!("{:?}", op)));
182                }
183
184                // 验证正则表达式
185                if matches!(op, Operator::Regex) {
186                    Regex::new(value).map_err(|e| {
187                        ConfigExprError::ValidationError(format!(
188                            "Invalid regex '{}' in rule {}: {}",
189                            value, rule_index, e
190                        ))
191                    })?;
192                }
193            }
194            Condition::And { and } => {
195                if and.is_empty() {
196                    return Err(ConfigExprError::ValidationError(format!(
197                        "AND condition cannot be empty in rule {}",
198                        rule_index
199                    )));
200                }
201                for cond in and {
202                    Self::validate_condition(cond, rule_index)?;
203                }
204            }
205            Condition::Or { or } => {
206                if or.is_empty() {
207                    return Err(ConfigExprError::ValidationError(format!(
208                        "OR condition cannot be empty in rule {}",
209                        rule_index
210                    )));
211                }
212                for cond in or {
213                    Self::validate_condition(cond, rule_index)?;
214                }
215            }
216        }
217        Ok(())
218    }
219}
220
221/// 便利方法:直接从JSON字符串评估
222pub fn evaluate_json(
223    json: &str,
224    params: &HashMap<String, String>,
225) -> Result<Option<RuleResult>, ConfigExprError> {
226    let evaluator = ConfigEvaluator::from_json(json)?;
227    Ok(evaluator.evaluate(params))
228}
229
230/// 便利方法:验证JSON规则是否合法
231pub fn validate_json(json: &str) -> Result<(), ConfigExprError> {
232    let rules: ConfigRules = serde_json::from_str(json)?;
233    ConfigEvaluator::validate_rules(&rules)
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_simple_condition() {
242        let json = r#"
243        {
244            "rules": [
245                {
246                    "if": {
247                        "field": "platform",
248                        "op": "equals",
249                        "value": "RTD"
250                    },
251                    "then": "chip_rtd"
252                }
253            ]
254        }
255        "#;
256
257        let mut params = HashMap::new();
258        params.insert("platform".to_string(), "RTD".to_string());
259
260        let result = evaluate_json(json, &params).unwrap();
261        assert!(result.is_some());
262
263        if let Some(RuleResult::String(s)) = result {
264            assert_eq!(s, "chip_rtd");
265        } else {
266            panic!("Expected string result");
267        }
268    }
269
270    #[test]
271    fn test_and_condition() {
272        let json = r#"
273        {
274            "rules": [
275                {
276                    "if": {
277                        "and": [
278                            { "field": "platform", "op": "contains", "value": "RTD" },
279                            { "field": "region", "op": "equals", "value": "CN" }
280                        ]
281                    },
282                    "then": "chip_rtd_cn"
283                }
284            ]
285        }
286        "#;
287
288        let mut params = HashMap::new();
289        params.insert("platform".to_string(), "RTD-2000".to_string());
290        params.insert("region".to_string(), "CN".to_string());
291
292        let result = evaluate_json(json, &params).unwrap();
293        assert!(result.is_some());
294
295        if let Some(RuleResult::String(s)) = result {
296            assert_eq!(s, "chip_rtd_cn");
297        } else {
298            panic!("Expected string result");
299        }
300    }
301
302    #[test]
303    fn test_or_condition() {
304        let json = r#"
305        {
306            "rules": [
307                {
308                    "if": {
309                        "or": [
310                            { "field": "platform", "op": "equals", "value": "MT9950" },
311                            { "field": "platform", "op": "equals", "value": "MT9638" }
312                        ]
313                    },
314                    "then": "chip_mt"
315                }
316            ]
317        }
318        "#;
319
320        let mut params = HashMap::new();
321        params.insert("platform".to_string(), "MT9950".to_string());
322
323        let result = evaluate_json(json, &params).unwrap();
324        assert!(result.is_some());
325
326        if let Some(RuleResult::String(s)) = result {
327            assert_eq!(s, "chip_mt");
328        } else {
329            panic!("Expected string result");
330        }
331    }
332
333    #[test]
334    fn test_prefix_condition() {
335        let json = r#"
336        {
337            "rules": [
338                {
339                    "if": {
340                        "field": "platform",
341                        "op": "prefix",
342                        "value": "Hi"
343                    },
344                    "then": "chip_hi"
345                }
346            ]
347        }
348        "#;
349
350        let mut params = HashMap::new();
351        params.insert("platform".to_string(), "Hi3516".to_string());
352
353        let result = evaluate_json(json, &params).unwrap();
354        assert!(result.is_some());
355
356        if let Some(RuleResult::String(s)) = result {
357            assert_eq!(s, "chip_hi");
358        } else {
359            panic!("Expected string result");
360        }
361    }
362
363    #[test]
364    fn test_json_object_result() {
365        let json = r#"
366        {
367            "rules": [
368                {
369                    "if": {
370                        "field": "platform",
371                        "op": "equals",
372                        "value": "RTD"
373                    },
374                    "then": {
375                        "chip": "rtd",
376                        "config": {
377                            "memory": "2GB",
378                            "cpu": "ARM"
379                        }
380                    }
381                }
382            ]
383        }
384        "#;
385
386        let mut params = HashMap::new();
387        params.insert("platform".to_string(), "RTD".to_string());
388
389        let result = evaluate_json(json, &params).unwrap();
390        assert!(result.is_some());
391
392        if let Some(RuleResult::Object(obj)) = result {
393            assert_eq!(obj["chip"], "rtd");
394            assert_eq!(obj["config"]["memory"], "2GB");
395        } else {
396            panic!("Expected object result");
397        }
398    }
399
400    #[test]
401    fn test_fallback() {
402        let json = r#"
403        {
404            "rules": [
405                {
406                    "if": {
407                        "field": "platform",
408                        "op": "equals",
409                        "value": "RTD"
410                    },
411                    "then": "chip_rtd"
412                }
413            ],
414            "fallback": "default_chip"
415        }
416        "#;
417
418        let mut params = HashMap::new();
419        params.insert("platform".to_string(), "Unknown".to_string());
420
421        let result = evaluate_json(json, &params).unwrap();
422        assert!(result.is_some());
423
424        if let Some(RuleResult::String(s)) = result {
425            assert_eq!(s, "default_chip");
426        } else {
427            panic!("Expected string result");
428        }
429    }
430
431    #[test]
432    fn test_no_match_no_fallback() {
433        let json = r#"
434        {
435            "rules": [
436                {
437                    "if": {
438                        "field": "platform",
439                        "op": "equals",
440                        "value": "RTD"
441                    },
442                    "then": "chip_rtd"
443                }
444            ]
445        }
446        "#;
447
448        let mut params = HashMap::new();
449        params.insert("platform".to_string(), "Unknown".to_string());
450
451        let result = evaluate_json(json, &params).unwrap();
452        assert!(result.is_none());
453    }
454
455    #[test]
456    fn test_regex_condition() {
457        let json = r#"
458        {
459            "rules": [
460                {
461                    "if": {
462                        "field": "platform",
463                        "op": "regex",
464                        "value": "^Hi\\d+"
465                    },
466                    "then": "chip_hi"
467                }
468            ]
469        }
470        "#;
471
472        let mut params = HashMap::new();
473        params.insert("platform".to_string(), "Hi3516".to_string());
474
475        let result = evaluate_json(json, &params).unwrap();
476        assert!(result.is_some());
477
478        if let Some(RuleResult::String(s)) = result {
479            assert_eq!(s, "chip_hi");
480        } else {
481            panic!("Expected string result");
482        }
483    }
484
485    #[test]
486    fn test_suffix_condition() {
487        let json = r#"
488        {
489            "rules": [
490                {
491                    "if": {
492                        "field": "platform",
493                        "op": "suffix",
494                        "value": "Pro"
495                    },
496                    "then": "chip_pro"
497                }
498            ]
499        }
500        "#;
501
502        let mut params = HashMap::new();
503        params.insert("platform".to_string(), "RTD-Pro".to_string());
504
505        let result = evaluate_json(json, &params).unwrap();
506        assert!(result.is_some());
507
508        if let Some(RuleResult::String(s)) = result {
509            assert_eq!(s, "chip_pro");
510        } else {
511            panic!("Expected string result");
512        }
513    }
514
515    #[test]
516    fn test_validation_empty_rules() {
517        let json = r#"
518        {
519            "rules": []
520        }
521        "#;
522
523        let result = validate_json(json);
524        assert!(result.is_err());
525        assert!(
526            result
527                .unwrap_err()
528                .to_string()
529                .contains("Rules cannot be empty")
530        );
531    }
532
533    #[test]
534    fn test_validation_empty_field() {
535        let json = r#"
536        {
537            "rules": [
538                {
539                    "if": {
540                        "field": "",
541                        "op": "equals",
542                        "value": "RTD"
543                    },
544                    "then": "chip_rtd"
545                }
546            ]
547        }
548        "#;
549
550        let result = validate_json(json);
551        assert!(result.is_err());
552        assert!(
553            result
554                .unwrap_err()
555                .to_string()
556                .contains("Field name cannot be empty")
557        );
558    }
559
560    #[test]
561    fn test_validation_invalid_regex() {
562        let json = r#"
563        {
564            "rules": [
565                {
566                    "if": {
567                        "field": "platform",
568                        "op": "regex",
569                        "value": "[invalid"
570                    },
571                    "then": "chip_rtd"
572                }
573            ]
574        }
575        "#;
576
577        let result = validate_json(json);
578        assert!(result.is_err());
579        assert!(result.unwrap_err().to_string().contains("Invalid regex"));
580    }
581
582    #[test]
583    fn test_validation_empty_and_condition() {
584        let json = r#"
585        {
586            "rules": [
587                {
588                    "if": {
589                        "and": []
590                    },
591                    "then": "chip_rtd"
592                }
593            ]
594        }
595        "#;
596
597        let result = validate_json(json);
598        assert!(result.is_err());
599        assert!(
600            result
601                .unwrap_err()
602                .to_string()
603                .contains("AND condition cannot be empty")
604        );
605    }
606
607    #[test]
608    fn test_validation_empty_or_condition() {
609        let json = r#"
610        {
611            "rules": [
612                {
613                    "if": {
614                        "or": []
615                    },
616                    "then": "chip_rtd"
617                }
618            ]
619        }
620        "#;
621
622        let result = validate_json(json);
623        assert!(result.is_err());
624        assert!(
625            result
626                .unwrap_err()
627                .to_string()
628                .contains("OR condition cannot be empty")
629        );
630    }
631
632    #[test]
633    fn test_complex_nested_conditions() {
634        let json = r#"
635        {
636            "rules": [
637                {
638                    "if": {
639                        "and": [
640                            {
641                                "or": [
642                                    { "field": "platform", "op": "prefix", "value": "Hi" },
643                                    { "field": "platform", "op": "prefix", "value": "MT" }
644                                ]
645                            },
646                            { "field": "region", "op": "equals", "value": "CN" }
647                        ]
648                    },
649                    "then": "chip_cn"
650                }
651            ]
652        }
653        "#;
654
655        let mut params = HashMap::new();
656        params.insert("platform".to_string(), "Hi3516".to_string());
657        params.insert("region".to_string(), "CN".to_string());
658
659        let result = evaluate_json(json, &params).unwrap();
660        assert!(result.is_some());
661
662        if let Some(RuleResult::String(s)) = result {
663            assert_eq!(s, "chip_cn");
664        } else {
665            panic!("Expected string result");
666        }
667    }
668}