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 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub pattern: Option<String>,
32 #[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 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
121fn full_match(pattern: &str) -> Result<regex::Regex, regex::Error> {
123 regex::Regex::new(&format!("^(?:{pattern})$"))
124}
125
126pub 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
136pub 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 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}