Skip to main content

dre_core/
variables.rs

1//! Optional variable declarations, separate from `vars` so existing map values keep their meaning.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6use std::collections::BTreeMap;
7
8pub type Definitions = BTreeMap<String, Definition>;
9
10#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
11#[serde(deny_unknown_fields)]
12pub struct Definition {
13    /// The value's type. Dates are real calendar dates in YYYY-MM-DD form.
14    #[serde(rename = "type")]
15    pub kind: VariableType,
16    /// Used when no project, folder, report, Set, schedule or CLI value supplies this variable.
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub default: Option<Value>,
19    /// Optional exact values accepted for this variable; required for the enum type.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub allowed: Option<Vec<Value>>,
22    /// What this input means, including its units or reporting period.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub description: Option<String>,
25    /// Whether execution requires a value. A default satisfies this requirement.
26    #[serde(default)]
27    pub required: bool,
28    /// For `string` variables: a regular expression the whole value must match, e.g.
29    /// `^[A-Z]{3}[0-9]{4}$` for a customer code. Anchors are implied.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub pattern: Option<String>,
32    /// For `string` variables: the most characters a value may have.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub max_length: Option<u32>,
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
38#[serde(rename_all = "snake_case")]
39pub enum VariableType {
40    String,
41    #[serde(alias = "int")]
42    Integer,
43    Number,
44    Boolean,
45    Date,
46    #[serde(alias = "list")]
47    Array,
48    #[serde(alias = "map")]
49    Object,
50    Enum,
51}
52
53impl Definition {
54    /// Problems with the declaration itself: `pattern`/`max_length` on a non-string, or a
55    /// pattern that isn't a valid regular expression.
56    pub fn problems(&self, name: &str) -> Vec<String> {
57        let mut problems = Vec::new();
58        let string = matches!(self.kind, VariableType::String);
59        for (key, set) in [
60            ("pattern", self.pattern.is_some()),
61            ("max_length", self.max_length.is_some()),
62        ] {
63            if set && !string {
64                problems.push(format!("variable `{name}`: `{key}` applies only to type string"));
65            }
66        }
67        if let Some(pattern) = &self.pattern
68            && let Err(error) = full_match(pattern)
69        {
70            problems.push(format!("variable `{name}`: invalid `pattern`: {error}"));
71        }
72        problems
73    }
74
75    pub fn check(&self, name: &str, value: &Value) -> Result<(), String> {
76        let valid = match self.kind {
77            VariableType::String => value.is_string(),
78            VariableType::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
79            VariableType::Number => value.is_number(),
80            VariableType::Boolean => value.is_boolean(),
81            VariableType::Date => value
82                .as_str()
83                .is_some_and(|s| s.len() == 10 && chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").is_ok()),
84            VariableType::Array => value.is_array(),
85            VariableType::Object => value.is_object(),
86            VariableType::Enum => !value.is_null() && self.allowed.as_ref().is_some_and(|v| !v.is_empty()),
87        };
88        if !valid {
89            return Err(format!(
90                "variable `{name}` must have type {}",
91                serde_json::to_value(&self.kind).unwrap().as_str().unwrap()
92            ));
93        }
94        if self
95            .allowed
96            .as_ref()
97            .is_some_and(|allowed| !allowed.contains(value))
98        {
99            return Err(format!("variable `{name}` is not one of its allowed values"));
100        }
101        if let Some(text) = value.as_str() {
102            if let Some(max) = self.max_length
103                && text.chars().count() > max as usize
104            {
105                return Err(format!(
106                    "variable `{name}` is longer than its max_length of {max}"
107                ));
108            }
109            if let Some(pattern) = &self.pattern
110                && !full_match(pattern).is_ok_and(|re| re.is_match(text))
111            {
112                return Err(format!(
113                    "variable `{name}` does not match its pattern `{pattern}`"
114                ));
115            }
116        }
117        Ok(())
118    }
119}
120
121/// `pattern` compiled to match a whole value.
122fn full_match(pattern: &str) -> Result<regex::Regex, regex::Error> {
123    regex::Regex::new(&format!("^(?:{pattern})$"))
124}
125
126/// Check the values a run will use: types, allowed values, patterns, and that every required
127/// variable has one.
128pub fn check_run_values(
129    definitions: &Definitions,
130    vars: &Map<String, Value>,
131    cli: &BTreeMap<String, String>,
132) -> Vec<String> {
133    check(definitions, vars, cli, true)
134}
135
136/// Check the values that are set (by `validate`): a missing required value isn't an error yet,
137/// the run may still be given one.
138pub fn check_given_values(
139    definitions: &Definitions,
140    vars: &Map<String, Value>,
141    cli: &BTreeMap<String, String>,
142) -> Vec<String> {
143    check(definitions, vars, cli, false)
144}
145
146fn check(
147    definitions: &Definitions,
148    vars: &Map<String, Value>,
149    cli: &BTreeMap<String, String>,
150    required: bool,
151) -> Vec<String> {
152    let mut errors = Vec::new();
153    for (name, definition) in definitions {
154        let value = cli
155            .get(name)
156            .map(|v| crate::render::cli_var_value(v))
157            .or_else(|| vars.get(name).cloned());
158        match value {
159            Some(value) => {
160                if let Err(error) = definition.check(name, &value) {
161                    errors.push(error);
162                }
163            }
164            None if required && definition.required => errors.push(format!(
165                "required variable `{name}` is missing; pass --var {name}=VALUE"
166            )),
167            None => {}
168        }
169    }
170    errors
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use serde_json::json;
177
178    fn string(pattern: Option<&str>, max_length: Option<u32>) -> Definition {
179        Definition {
180            kind: VariableType::String,
181            default: None,
182            allowed: None,
183            description: None,
184            required: false,
185            pattern: pattern.map(str::to_owned),
186            max_length,
187        }
188    }
189
190    #[test]
191    fn pattern_matches_the_whole_value() {
192        let code = string(Some("[A-Z]{3}[0-9]{2}"), None);
193        assert!(code.check("code", &json!("ABC12")).is_ok());
194        assert!(code.check("code", &json!("ABC12' or 1=1 --")).is_err());
195        assert!(code.check("code", &json!("xABC12")).is_err());
196        // An alternation can't escape the implied anchors.
197        let either = string(Some("a|b"), None);
198        assert!(either.check("v", &json!("ab")).is_err());
199    }
200
201    #[test]
202    fn max_length_counts_characters() {
203        let short = string(None, Some(3));
204        assert!(short.check("v", &json!("éée")).is_ok());
205        assert!(short.check("v", &json!("abcd")).is_err());
206    }
207
208    #[test]
209    fn constraints_only_apply_to_strings_and_must_compile() {
210        let mut number = string(Some("1"), Some(1));
211        number.kind = VariableType::Number;
212        assert_eq!(number.problems("n").len(), 2);
213        assert_eq!(string(Some("("), None).problems("s").len(), 1);
214    }
215}