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}
29
30#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
31#[serde(rename_all = "snake_case")]
32pub enum VariableType {
33    String,
34    #[serde(alias = "int")]
35    Integer,
36    Number,
37    Boolean,
38    Date,
39    #[serde(alias = "list")]
40    Array,
41    #[serde(alias = "map")]
42    Object,
43    Enum,
44}
45
46impl Definition {
47    pub fn check(&self, name: &str, value: &Value) -> Result<(), String> {
48        let valid = match self.kind {
49            VariableType::String => value.is_string(),
50            VariableType::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
51            VariableType::Number => value.is_number(),
52            VariableType::Boolean => value.is_boolean(),
53            VariableType::Date => value
54                .as_str()
55                .is_some_and(|s| s.len() == 10 && chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").is_ok()),
56            VariableType::Array => value.is_array(),
57            VariableType::Object => value.is_object(),
58            VariableType::Enum => !value.is_null() && self.allowed.as_ref().is_some_and(|v| !v.is_empty()),
59        };
60        if !valid {
61            return Err(format!(
62                "variable `{name}` must have type {}",
63                serde_json::to_value(&self.kind).unwrap().as_str().unwrap()
64            ));
65        }
66        if self
67            .allowed
68            .as_ref()
69            .is_some_and(|allowed| !allowed.contains(value))
70        {
71            return Err(format!("variable `{name}` is not one of its allowed values"));
72        }
73        Ok(())
74    }
75}
76
77pub fn check_values(
78    definitions: &Definitions,
79    vars: &Map<String, Value>,
80    cli: &BTreeMap<String, String>,
81    required: bool,
82) -> Vec<String> {
83    let mut errors = Vec::new();
84    for (name, definition) in definitions {
85        let value = cli
86            .get(name)
87            .map(|v| crate::render::cli_var_value(v))
88            .or_else(|| vars.get(name).cloned());
89        match value {
90            Some(value) => {
91                if let Err(error) = definition.check(name, &value) {
92                    errors.push(error);
93                }
94            }
95            None if required && definition.required => errors.push(format!(
96                "required variable `{name}` is missing; pass --var {name}=VALUE"
97            )),
98            None => {}
99        }
100    }
101    errors
102}