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}
31
32impl Operator {
33 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#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(untagged)]
49pub enum Condition {
50 Simple {
52 field: String,
53 op: Operator,
54 value: String,
55 },
56 And { and: Vec<Condition> },
58 Or { or: Vec<Condition> },
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(untagged)]
65pub enum RuleResult {
66 String(String),
67 Object(serde_json::Value),
68}
69
70#[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#[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
87pub struct ConfigEvaluator {
89 rules: ConfigRules,
90}
91
92impl ConfigEvaluator {
93 pub fn new(rules: ConfigRules) -> Result<Self, ConfigExprError> {
95 Self::validate_rules(&rules)?;
97 Ok(Self { rules })
98 }
99
100 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 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 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 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, }
150 }
151 }
152 }
153
154 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 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 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
221pub 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
230pub 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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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}