Skip to main content

envo/
validate.rs

1//! Validate a resolved environment against a schema.
2
3use crate::resolve::Resolved;
4use crate::schema::{self, Schema};
5
6#[derive(Debug, Default)]
7pub struct Report {
8    /// Required variables with no value.
9    pub missing: Vec<String>,
10    /// Present variables whose value does not match the declared type.
11    pub invalid: Vec<(String, String)>,
12    /// Variables present in the environment but absent from the schema.
13    pub extra: Vec<String>,
14}
15
16impl Report {
17    /// Passing means no missing-required and no type errors. `extra` is only a
18    /// hard failure in strict mode (decided by the caller via [`Report::failed`]).
19    pub fn passed(&self) -> bool {
20        self.missing.is_empty() && self.invalid.is_empty()
21    }
22
23    pub fn failed(&self, strict: bool) -> bool {
24        !self.passed() || (strict && !self.extra.is_empty())
25    }
26}
27
28pub fn validate(schema: &Schema, resolved: &Resolved) -> Report {
29    let mut report = Report::default();
30
31    for spec in &schema.vars {
32        match resolved.values.get(&spec.name) {
33            None => {
34                if spec.required() {
35                    report.missing.push(spec.name.clone());
36                }
37            }
38            Some(value) => {
39                if let Err(reason) = schema::validate_value(&spec.ty, value) {
40                    report.invalid.push((spec.name.clone(), reason));
41                }
42            }
43        }
44    }
45
46    for key in resolved.values.keys() {
47        if schema.get(key).is_none() {
48            report.extra.push(key.to_string());
49        }
50    }
51
52    report
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::resolve::Resolved;
59
60    fn resolved_from(pairs: &[(&str, &str)]) -> Resolved {
61        let mut r = Resolved::default();
62        for (k, v) in pairs {
63            r.values.set(k.to_string(), v.to_string());
64        }
65        r
66    }
67
68    #[test]
69    fn flags_missing_and_invalid_and_extra() {
70        let schema = Schema::parse("PORT: port\nKEY: secret\nDEBUG: bool = false\n").unwrap();
71        let resolved = resolved_from(&[("PORT", "not-a-port"), ("EXTRA", "1")]);
72        let report = validate(&schema, &resolved);
73
74        assert!(report.missing.contains(&"KEY".to_string()));
75        assert!(report.invalid.iter().any(|(n, _)| n == "PORT"));
76        assert!(report.extra.contains(&"EXTRA".to_string()));
77        assert!(!report.passed());
78        assert!(report.failed(false));
79    }
80
81    #[test]
82    fn passes_when_satisfied() {
83        let schema = Schema::parse("PORT: port = 3000\nDEBUG: bool = false\n").unwrap();
84        let resolved = resolved_from(&[("PORT", "8080"), ("DEBUG", "true")]);
85        let report = validate(&schema, &resolved);
86        assert!(report.passed());
87        assert!(!report.failed(false));
88    }
89}