Skip to main content

sz_orm_config/
config_validator.rs

1//! 配置校验器:对配置值进行类型/范围/格式校验
2//!
3//! 提供声明式配置校验:定义 schema → 校验配置 → 输出校验结果。
4//! 支持类型校验(int/float/bool/string)、范围校验(min/max)、
5//! 前缀/后缀/包含校验、必填校验、枚举值校验。
6
7use std::collections::HashMap;
8
9/// 校验规则
10#[derive(Debug, Clone, PartialEq)]
11pub enum ValidationRule {
12    /// 必填(值不能为空)
13    Required,
14    /// 整数类型
15    Integer,
16    /// 浮点类型
17    Float,
18    /// 布尔类型
19    Boolean,
20    /// 字符串类型
21    String,
22    /// 整数范围 [min, max]
23    IntRange(i64, i64),
24    /// 浮点范围 [min, max]
25    FloatRange(f64, f64),
26    /// 字符串长度范围 [min, max]
27    LengthRange(usize, usize),
28    /// 枚举值(必须是列表中的一个)
29    Enum(Vec<String>),
30    /// 前缀匹配
31    Prefix(String),
32    /// 后缀匹配
33    Suffix(String),
34    /// 包含子串
35    Contains(String),
36}
37
38/// 字段校验定义
39#[derive(Debug, Clone)]
40pub struct FieldSchema {
41    /// 字段名
42    pub name: String,
43    /// 校验规则列表(全部必须通过)
44    pub rules: Vec<ValidationRule>,
45    /// 默认值(可选)
46    pub default: Option<String>,
47    /// 描述
48    pub description: String,
49}
50
51impl FieldSchema {
52    /// 创建字段 schema
53    pub fn new(name: &str) -> Self {
54        Self {
55            name: name.to_string(),
56            rules: Vec::new(),
57            default: None,
58            description: String::new(),
59        }
60    }
61
62    /// 添加规则(链式)
63    pub fn with_rule(mut self, rule: ValidationRule) -> Self {
64        self.rules.push(rule);
65        self
66    }
67
68    /// 设置默认值(链式)
69    pub fn with_default(mut self, value: &str) -> Self {
70        self.default = Some(value.to_string());
71        self
72    }
73
74    /// 设置描述(链式)
75    pub fn with_description(mut self, desc: &str) -> Self {
76        self.description = desc.to_string();
77        self
78    }
79
80    /// 校验单个值
81    pub fn validate(&self, value: Option<&str>) -> ValidationResult {
82        let value = match value {
83            Some(v) => v,
84            None => match &self.default {
85                Some(d) => d.as_str(),
86                None => {
87                    if self.rules.contains(&ValidationRule::Required) {
88                        return ValidationResult::failed(&self.name, "required field is missing");
89                    }
90                    return ValidationResult::passed(&self.name);
91                }
92            },
93        };
94
95        if value.is_empty() && self.rules.contains(&ValidationRule::Required) {
96            return ValidationResult::failed(&self.name, "required field is empty");
97        }
98
99        for rule in &self.rules {
100            if let Some(msg) = check_rule(rule, value) {
101                return ValidationResult::failed(&self.name, &msg);
102            }
103        }
104
105        ValidationResult::passed(&self.name)
106    }
107}
108
109/// 配置 schema:多字段校验定义
110#[derive(Debug, Clone, Default)]
111pub struct ConfigSchema {
112    fields: Vec<FieldSchema>,
113}
114
115impl ConfigSchema {
116    /// 创建空 schema
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    /// 添加字段定义(链式)
122    pub fn add_field(mut self, field: FieldSchema) -> Self {
123        self.fields.push(field);
124        self
125    }
126
127    /// 字段数
128    pub fn field_count(&self) -> usize {
129        self.fields.len()
130    }
131
132    /// 校验配置 HashMap
133    pub fn validate(&self, config: &HashMap<String, String>) -> ConfigValidationReport {
134        let mut results = Vec::with_capacity(self.fields.len());
135        for field in &self.fields {
136            let value = config.get(&field.name).map(|s| s.as_str());
137            results.push(field.validate(value));
138        }
139        ConfigValidationReport { results }
140    }
141
142    /// 校验并填充默认值(返回新 config)
143    pub fn validate_and_fill_defaults(
144        &self,
145        config: &HashMap<String, String>,
146    ) -> (HashMap<String, String>, ConfigValidationReport) {
147        let mut filled = config.clone();
148        for field in &self.fields {
149            if !filled.contains_key(&field.name) {
150                if let Some(default) = &field.default {
151                    filled.insert(field.name.clone(), default.clone());
152                }
153            }
154        }
155        let report = self.validate(&filled);
156        (filled, report)
157    }
158
159    /// 从环境变量读取配置并校验
160    ///
161    /// 对每个字段,先查 env var,再查默认值,最后校验。
162    pub fn validate_env(&self) -> ConfigValidationReport {
163        let mut env_config: HashMap<String, String> = HashMap::new();
164        for field in &self.fields {
165            if let Ok(val) = std::env::var(&field.name) {
166                env_config.insert(field.name.clone(), val);
167            }
168        }
169        let (filled, _) = self.validate_and_fill_defaults(&env_config);
170        self.validate(&filled)
171    }
172
173    /// 返回所有字段名
174    pub fn field_names(&self) -> Vec<&str> {
175        self.fields.iter().map(|f| f.name.as_str()).collect()
176    }
177}
178
179/// 单字段校验结果
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct ValidationResult {
182    /// 字段名
183    pub field: String,
184    /// 是否通过
185    pub passed: bool,
186    /// 失败消息(通过时为空)
187    pub message: String,
188}
189
190impl ValidationResult {
191    fn passed(field: &str) -> Self {
192        Self {
193            field: field.to_string(),
194            passed: true,
195            message: String::new(),
196        }
197    }
198
199    fn failed(field: &str, message: &str) -> Self {
200        Self {
201            field: field.to_string(),
202            passed: false,
203            message: message.to_string(),
204        }
205    }
206}
207
208/// 配置校验报告
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct ConfigValidationReport {
211    results: Vec<ValidationResult>,
212}
213
214impl ConfigValidationReport {
215    /// 是否全部通过
216    pub fn is_valid(&self) -> bool {
217        self.results.iter().all(|r| r.passed)
218    }
219
220    /// 失败数量
221    pub fn failure_count(&self) -> usize {
222        self.results.iter().filter(|r| !r.passed).count()
223    }
224
225    /// 通过数量
226    pub fn pass_count(&self) -> usize {
227        self.results.iter().filter(|r| r.passed).count()
228    }
229
230    /// 总字段数
231    pub fn total(&self) -> usize {
232        self.results.len()
233    }
234
235    /// 失败字段列表
236    pub fn failures(&self) -> Vec<&ValidationResult> {
237        self.results.iter().filter(|r| !r.passed).collect()
238    }
239
240    /// 通过字段列表
241    pub fn passed_fields(&self) -> Vec<&ValidationResult> {
242        self.results.iter().filter(|r| r.passed).collect()
243    }
244
245    /// 汇总字符串
246    pub fn to_summary(&self) -> String {
247        if self.is_valid() {
248            format!("Config validation: all {} field(s) passed", self.total())
249        } else {
250            let mut out = format!(
251                "Config validation: {}/{} field(s) failed\n",
252                self.failure_count(),
253                self.total()
254            );
255            for f in self.failures() {
256                out.push_str(&format!("  X {}: {}\n", f.field, f.message));
257            }
258            out
259        }
260    }
261
262    /// 合并另一个报告,返回新报告
263    pub fn merge(&self, other: &ConfigValidationReport) -> ConfigValidationReport {
264        let mut combined = self.results.clone();
265        combined.extend(other.results.iter().cloned());
266        ConfigValidationReport { results: combined }
267    }
268
269    /// 按字段名过滤结果
270    pub fn filter_by_field(&self, field: &str) -> Vec<&ValidationResult> {
271        self.results.iter().filter(|r| r.field == field).collect()
272    }
273
274    /// 返回所有校验结果的引用
275    pub fn all_results(&self) -> &[ValidationResult] {
276        &self.results
277    }
278}
279
280/// 校验单个规则(返回 None 表示通过,Some(msg) 表示失败)
281fn check_rule(rule: &ValidationRule, value: &str) -> Option<String> {
282    match rule {
283        ValidationRule::Required => {
284            if value.is_empty() {
285                Some("required field is empty".to_string())
286            } else {
287                None
288            }
289        }
290        ValidationRule::Integer => {
291            if value.parse::<i64>().is_err() {
292                Some(format!("expected integer, got '{value}'"))
293            } else {
294                None
295            }
296        }
297        ValidationRule::Float => {
298            if value.parse::<f64>().is_err() {
299                Some(format!("expected float, got '{value}'"))
300            } else {
301                None
302            }
303        }
304        ValidationRule::Boolean => {
305            if value.parse::<bool>().is_err() {
306                Some(format!("expected boolean (true/false), got '{value}'"))
307            } else {
308                None
309            }
310        }
311        ValidationRule::String => None,
312        ValidationRule::IntRange(min, max) => match value.parse::<i64>() {
313            Ok(n) if (*min..=*max).contains(&n) => None,
314            Ok(n) => Some(format!("integer {n} out of range [{min}, {max}]")),
315            Err(_) => Some(format!("expected integer for range check, got '{value}'")),
316        },
317        ValidationRule::FloatRange(min, max) => match value.parse::<f64>() {
318            Ok(n) if (*min..=*max).contains(&n) => None,
319            Ok(n) => Some(format!("float {n} out of range [{min}, {max}]")),
320            Err(_) => Some(format!("expected float for range check, got '{value}'")),
321        },
322        ValidationRule::LengthRange(min, max) => {
323            let len = value.chars().count();
324            if (*min..=*max).contains(&len) {
325                None
326            } else {
327                Some(format!("length {len} out of range [{min}, {max}]"))
328            }
329        }
330        ValidationRule::Enum(allowed) => {
331            if allowed.iter().any(|a| a == value) {
332                None
333            } else {
334                Some(format!(
335                    "value '{value}' not in allowed list: {:?}",
336                    allowed
337                ))
338            }
339        }
340        ValidationRule::Prefix(prefix) => {
341            if value.starts_with(prefix) {
342                None
343            } else {
344                Some(format!("value '{value}' does not start with '{prefix}'"))
345            }
346        }
347        ValidationRule::Suffix(suffix) => {
348            if value.ends_with(suffix) {
349                None
350            } else {
351                Some(format!("value '{value}' does not end with '{suffix}'"))
352            }
353        }
354        ValidationRule::Contains(substring) => {
355            if value.contains(substring) {
356                None
357            } else {
358                Some(format!("value '{value}' does not contain '{substring}'"))
359            }
360        }
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    fn config(items: &[(&str, &str)]) -> HashMap<String, String> {
369        items
370            .iter()
371            .map(|(k, v)| (k.to_string(), v.to_string()))
372            .collect()
373    }
374
375    #[test]
376    fn validate_required_present() {
377        let schema = FieldSchema::new("port").with_rule(ValidationRule::Required);
378        let result = schema.validate(Some("8080"));
379        assert!(result.passed);
380    }
381
382    #[test]
383    fn validate_required_missing() {
384        let schema = FieldSchema::new("port").with_rule(ValidationRule::Required);
385        let result = schema.validate(None);
386        assert!(!result.passed);
387        assert!(result.message.contains("missing"));
388    }
389
390    #[test]
391    fn validate_required_empty() {
392        let schema = FieldSchema::new("port").with_rule(ValidationRule::Required);
393        let result = schema.validate(Some(""));
394        assert!(!result.passed);
395    }
396
397    #[test]
398    fn validate_integer_pass() {
399        let schema = FieldSchema::new("port").with_rule(ValidationRule::Integer);
400        assert!(schema.validate(Some("8080")).passed);
401    }
402
403    #[test]
404    fn validate_integer_fail() {
405        let schema = FieldSchema::new("port").with_rule(ValidationRule::Integer);
406        assert!(!schema.validate(Some("abc")).passed);
407    }
408
409    #[test]
410    fn validate_float_pass() {
411        let schema = FieldSchema::new("ratio").with_rule(ValidationRule::Float);
412        assert!(schema.validate(Some("0.95")).passed);
413    }
414
415    #[test]
416    fn validate_boolean_pass() {
417        let schema = FieldSchema::new("enabled").with_rule(ValidationRule::Boolean);
418        assert!(schema.validate(Some("true")).passed);
419        assert!(schema.validate(Some("false")).passed);
420    }
421
422    #[test]
423    fn validate_boolean_fail() {
424        let schema = FieldSchema::new("enabled").with_rule(ValidationRule::Boolean);
425        assert!(!schema.validate(Some("yes")).passed);
426    }
427
428    #[test]
429    fn validate_int_range_pass() {
430        let schema = FieldSchema::new("port").with_rule(ValidationRule::IntRange(1, 65535));
431        assert!(schema.validate(Some("8080")).passed);
432    }
433
434    #[test]
435    fn validate_int_range_fail() {
436        let schema = FieldSchema::new("port").with_rule(ValidationRule::IntRange(1, 65535));
437        assert!(!schema.validate(Some("99999")).passed);
438    }
439
440    #[test]
441    fn validate_length_range() {
442        let schema = FieldSchema::new("name").with_rule(ValidationRule::LengthRange(3, 10));
443        assert!(schema.validate(Some("hello")).passed);
444        assert!(!schema.validate(Some("hi")).passed);
445        assert!(!schema.validate(Some("this_is_too_long")).passed);
446    }
447
448    #[test]
449    fn validate_enum_pass() {
450        let schema = FieldSchema::new("level").with_rule(ValidationRule::Enum(vec![
451            "debug".to_string(),
452            "info".to_string(),
453            "warn".to_string(),
454        ]));
455        assert!(schema.validate(Some("info")).passed);
456    }
457
458    #[test]
459    fn validate_enum_fail() {
460        let schema = FieldSchema::new("level").with_rule(ValidationRule::Enum(vec![
461            "debug".to_string(),
462            "info".to_string(),
463        ]));
464        assert!(!schema.validate(Some("trace")).passed);
465    }
466
467    #[test]
468    fn validate_prefix() {
469        let schema = FieldSchema::new("url").with_rule(ValidationRule::Prefix("http".to_string()));
470        assert!(schema.validate(Some("http://example.com")).passed);
471        assert!(!schema.validate(Some("ftp://x")).passed);
472    }
473
474    #[test]
475    fn validate_suffix() {
476        let schema = FieldSchema::new("file").with_rule(ValidationRule::Suffix(".rs".to_string()));
477        assert!(schema.validate(Some("main.rs")).passed);
478        assert!(!schema.validate(Some("main.go")).passed);
479    }
480
481    #[test]
482    fn validate_contains() {
483        let schema =
484            FieldSchema::new("conn").with_rule(ValidationRule::Contains("://".to_string()));
485        assert!(schema.validate(Some("mysql://localhost")).passed);
486        assert!(!schema.validate(Some("localhost")).passed);
487    }
488
489    #[test]
490    fn validate_with_default() {
491        let schema = FieldSchema::new("port")
492            .with_rule(ValidationRule::Integer)
493            .with_default("8080");
494        let result = schema.validate(None);
495        assert!(result.passed);
496    }
497
498    #[test]
499    fn schema_validate_all_pass() {
500        let schema = ConfigSchema::new()
501            .add_field(FieldSchema::new("host").with_rule(ValidationRule::Required))
502            .add_field(FieldSchema::new("port").with_rule(ValidationRule::IntRange(1, 65535)));
503        let report = schema.validate(&config(&[("host", "localhost"), ("port", "8080")]));
504        assert!(report.is_valid());
505        assert_eq!(report.pass_count(), 2);
506        assert_eq!(report.failure_count(), 0);
507    }
508
509    #[test]
510    fn schema_validate_with_failures() {
511        let schema = ConfigSchema::new()
512            .add_field(FieldSchema::new("host").with_rule(ValidationRule::Required))
513            .add_field(FieldSchema::new("port").with_rule(ValidationRule::IntRange(1, 65535)));
514        let report = schema.validate(&config(&[("port", "99999")]));
515        assert!(!report.is_valid());
516        assert_eq!(report.failure_count(), 2);
517    }
518
519    #[test]
520    fn schema_fill_defaults() {
521        let schema = ConfigSchema::new().add_field(FieldSchema::new("port").with_default("8080"));
522        let (filled, report) = schema.validate_and_fill_defaults(&HashMap::new());
523        assert_eq!(filled.get("port").map(|s| s.as_str()), Some("8080"));
524        assert!(report.is_valid());
525    }
526
527    #[test]
528    fn report_to_summary_valid() {
529        let schema = ConfigSchema::new()
530            .add_field(FieldSchema::new("x").with_rule(ValidationRule::Required));
531        let report = schema.validate(&config(&[("x", "1")]));
532        assert!(report.to_summary().contains("passed"));
533    }
534
535    #[test]
536    fn report_to_summary_invalid() {
537        let schema = ConfigSchema::new()
538            .add_field(FieldSchema::new("x").with_rule(ValidationRule::Required));
539        let report = schema.validate(&HashMap::new());
540        let summary = report.to_summary();
541        assert!(summary.contains("failed"));
542    }
543
544    #[test]
545    fn report_total_and_counts() {
546        let schema = ConfigSchema::new()
547            .add_field(FieldSchema::new("a").with_rule(ValidationRule::Required))
548            .add_field(FieldSchema::new("b"));
549        let report = schema.validate(&config(&[("a", "1")]));
550        assert_eq!(report.total(), 2);
551        assert_eq!(report.pass_count(), 2);
552        assert_eq!(report.failure_count(), 0);
553    }
554
555    #[test]
556    fn field_schema_builder_chain() {
557        let schema = FieldSchema::new("url")
558            .with_rule(ValidationRule::Required)
559            .with_rule(ValidationRule::Prefix("http".to_string()))
560            .with_default("http://localhost")
561            .with_description("Service URL");
562        assert_eq!(schema.name, "url");
563        assert_eq!(schema.rules.len(), 2);
564        assert_eq!(schema.default, Some("http://localhost".to_string()));
565        assert_eq!(schema.description, "Service URL");
566    }
567
568    #[test]
569    fn config_schema_field_count() {
570        let schema = ConfigSchema::new()
571            .add_field(FieldSchema::new("a"))
572            .add_field(FieldSchema::new("b"))
573            .add_field(FieldSchema::new("c"));
574        assert_eq!(schema.field_count(), 3);
575    }
576
577    #[test]
578    fn validate_float_range() {
579        let schema = FieldSchema::new("ratio").with_rule(ValidationRule::FloatRange(0.0, 1.0));
580        assert!(schema.validate(Some("0.5")).passed);
581        assert!(!schema.validate(Some("1.5")).passed);
582    }
583
584    #[test]
585    fn validate_multiple_rules_all_pass() {
586        let schema = FieldSchema::new("port")
587            .with_rule(ValidationRule::Required)
588            .with_rule(ValidationRule::Integer)
589            .with_rule(ValidationRule::IntRange(1, 65535));
590        assert!(schema.validate(Some("8080")).passed);
591    }
592
593    #[test]
594    fn validate_multiple_rules_one_fails() {
595        let schema = FieldSchema::new("port")
596            .with_rule(ValidationRule::Required)
597            .with_rule(ValidationRule::Integer)
598            .with_rule(ValidationRule::IntRange(1, 1024));
599        let result = schema.validate(Some("8080"));
600        assert!(!result.passed);
601        assert!(result.message.contains("range"));
602    }
603
604    #[test]
605    fn report_merge_combines_results() {
606        let r1 = ValidationResult::passed("a");
607        let r2 = ValidationResult::failed("b", "err");
608        let rep1 = ConfigValidationReport { results: vec![r1] };
609        let rep2 = ConfigValidationReport { results: vec![r2] };
610        let merged = rep1.merge(&rep2);
611        assert_eq!(merged.total(), 2);
612        assert_eq!(merged.failure_count(), 1);
613    }
614
615    #[test]
616    fn report_filter_by_field() {
617        let r1 = ValidationResult::passed("port");
618        let r2 = ValidationResult::failed("host", "err");
619        let rep = ConfigValidationReport {
620            results: vec![r1, r2],
621        };
622        assert_eq!(rep.filter_by_field("port").len(), 1);
623        assert_eq!(rep.filter_by_field("missing").len(), 0);
624    }
625
626    #[test]
627    fn schema_field_names() {
628        let schema = ConfigSchema::new()
629            .add_field(FieldSchema::new("host"))
630            .add_field(FieldSchema::new("port"));
631        assert_eq!(schema.field_names(), vec!["host", "port"]);
632    }
633
634    #[test]
635    fn report_all_results() {
636        let rep = ConfigValidationReport {
637            results: vec![ValidationResult::passed("a")],
638        };
639        assert_eq!(rep.all_results().len(), 1);
640    }
641}