1use 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 #[serde(rename = "type")]
15 pub kind: VariableType,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub default: Option<Value>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub allowed: Option<Vec<Value>>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub description: Option<String>,
25 #[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}