1use super::parse::Walk;
14use crate::base::typo::unknown_key;
15use crate::base::{AuthoredPath, IssueCode, Severity};
16use serde::Serialize;
17use serde_json::{Map, Value};
18use std::collections::HashMap;
19
20const WORKFLOW_KEYS: &[&str] = &[
22 "id",
23 "name",
24 "description",
25 "priority",
26 "condition",
27 "tasks",
28 "on_failure",
29 "channel",
30 "status",
31 "version",
32 "rollout",
33 "loop",
34 "tags",
35 "created_at",
36 "updated_at",
37 "ext",
38];
39
40const TASK_KEYS: &[&str] = &[
42 "id",
43 "name",
44 "description",
45 "condition",
46 "function",
47 "on_failure",
48 "on_rejection",
49 "terminal",
50 "for_each",
51 "ext",
52];
53
54const GROUP_KEYS: &[&str] = &[
56 "id",
57 "name",
58 "description",
59 "condition",
60 "terminal",
61 "tasks",
62 "ext",
63];
64
65const LOOP_KEYS: &[&str] = &[
67 "counter",
68 "init",
69 "increment",
70 "max",
71 "setup",
72 "over",
73 "as",
74 "scratch",
75];
76
77const FOR_EACH_KEYS: &[&str] = &["over", "as", "max_concurrency", "collect", "into"];
79
80const ROLLOUT_KEYS: &[&str] = &["bucket_start", "bucket_end"];
82
83const FUNCTION_KEYS: &[&str] = &["name", "input"];
85
86const WORKFLOW_V3_KEYS: &[&str] = &["continue_on_error"];
88
89pub(super) const TASK_V3_KEYS: &[&str] = &["continue_on_error", "halt_on"];
91
92const GROUP_V3_KEYS: &[&str] = &["continue_on_error"];
94
95pub(super) const WORKFLOW_POLICIES: &[&str] = &["on_failure"];
97
98pub(super) const TASK_POLICIES: &[&str] = &["on_failure", "on_rejection"];
100
101const MAP_KEYS: &[&str] = &["mappings"];
103
104const MAPPING_KEYS: &[&str] = &["path", "logic", "unset", "on_null", "mode"];
106
107const VALIDATION_KEYS: &[&str] = &["rules"];
109
110const RULE_KEYS: &[&str] = &["logic", "message"];
112
113const FILTER_KEYS: &[&str] = &["condition", "on_reject"];
115
116const LOG_KEYS: &[&str] = &["level", "message", "fields"];
118
119const PARSE_KEYS: &[&str] = &["source", "target"];
121
122const PUBLISH_KEYS: &[&str] = &["source", "target", "pretty", "root_element"];
124
125#[derive(Clone, Copy, Debug)]
132pub(crate) struct BuiltinInput {
133 pub(crate) keys: &'static [&'static str],
135 pub(crate) elements: Option<(&'static str, &'static [&'static str])>,
137}
138
139pub(crate) fn builtin_input(name: &str) -> Option<BuiltinInput> {
141 let (keys, elements) = match name {
142 "map" => (MAP_KEYS, Some(("mappings", MAPPING_KEYS))),
143 super::VALIDATION => (VALIDATION_KEYS, Some(("rules", RULE_KEYS))),
144 "filter" => (FILTER_KEYS, None),
145 "log" => (LOG_KEYS, None),
146 "parse_json" | "parse_xml" => (PARSE_KEYS, None),
147 "publish_json" | "publish_xml" => (PUBLISH_KEYS, None),
148 _ => return None,
149 };
150 Some(BuiltinInput { keys, elements })
151}
152
153pub(super) struct Level {
156 known: &'static [&'static str],
157 v3: &'static [&'static str],
158}
159
160impl Level {
161 const fn new(known: &'static [&'static str]) -> Self {
162 Self { known, v3: &[] }
163 }
164
165 pub(super) fn knows(&self, key: &str) -> bool {
167 self.known.contains(&key)
168 }
169
170 #[cfg(test)]
173 pub(super) fn known(&self) -> &'static [&'static str] {
174 self.known
175 }
176}
177
178pub(super) const WORKFLOW: Level = Level {
180 known: WORKFLOW_KEYS,
181 v3: WORKFLOW_V3_KEYS,
182};
183pub(super) const TASK: Level = Level {
185 known: TASK_KEYS,
186 v3: TASK_V3_KEYS,
187};
188pub(super) const GROUP: Level = Level {
190 known: GROUP_KEYS,
191 v3: GROUP_V3_KEYS,
192};
193pub(super) const LOOP: Level = Level::new(LOOP_KEYS);
195pub(super) const FOR_EACH: Level = Level::new(FOR_EACH_KEYS);
197pub(super) const ROLLOUT: Level = Level::new(ROLLOUT_KEYS);
199pub(super) const FUNCTION: Level = Level::new(FUNCTION_KEYS);
201
202#[derive(Clone, Copy, Debug, Serialize)]
207#[non_exhaustive]
208pub struct DefinitionKeys {
209 pub workflow: &'static [&'static str],
211 pub task: &'static [&'static str],
213 pub group: &'static [&'static str],
215 pub r#loop: &'static [&'static str],
217 pub for_each: &'static [&'static str],
219 pub rollout: &'static [&'static str],
221 pub function: &'static [&'static str],
223}
224
225#[must_use]
227pub fn definition_keys() -> DefinitionKeys {
228 DefinitionKeys {
229 workflow: WORKFLOW_KEYS,
230 task: TASK_KEYS,
231 group: GROUP_KEYS,
232 r#loop: LOOP_KEYS,
233 for_each: FOR_EACH_KEYS,
234 rollout: ROLLOUT_KEYS,
235 function: FUNCTION_KEYS,
236 }
237}
238
239pub(super) struct Ext {
243 map: Map<String, Value>,
244 moved: HashMap<String, AuthoredPath>,
245}
246
247impl Ext {
248 pub(super) fn authored(raw: Option<&Value>) -> Self {
251 Self {
252 map: raw.and_then(Value::as_object).cloned().unwrap_or_default(),
253 moved: HashMap::new(),
254 }
255 }
256
257 pub(super) fn keep(
262 &mut self,
263 key: &str,
264 name: String,
265 value: Value,
266 from: AuthoredPath,
267 ) -> Result<(), String> {
268 if self.map.contains_key(&name) {
269 return Err(match self.moved.get(&name) {
270 Some(first) => format!(
271 "`{key}` cannot be kept in `ext` as `{name}`: the key at `{first}` is \
272 already kept there"
273 ),
274 None => {
275 format!(
276 "`{key}` appears both on the node and in its authored `ext`, as `{name}`"
277 )
278 }
279 });
280 }
281 self.map.insert(name.clone(), value);
282 self.moved.insert(name, from);
283 Ok(())
284 }
285
286 pub(super) fn into_map(self) -> Map<String, Value> {
287 self.map
288 }
289}
290
291impl Walk<'_> {
292 pub(super) fn unknown_keys(
301 &mut self,
302 obj: &Map<String, Value>,
303 level: &Level,
304 skip: &[&str],
305 at: &AuthoredPath,
306 prefix: &str,
307 ext: &mut Ext,
308 ) {
309 let mut candidates: Option<Vec<&str>> = None;
312 for (key, value) in obj {
313 if level.knows(key) || skip.contains(&key.as_str()) {
314 continue;
315 }
316 let path = at.join(key.as_str());
317 let candidates = candidates.get_or_insert_with(|| {
318 let v3 = if self.compat { level.v3 } else { &[] };
319 level.known.iter().chain(v3).copied().collect()
320 });
321 let (code, message) = unknown_key(key, candidates);
322 if !self.compat {
323 self.report_as(code, Severity::Rejected, path, message);
324 continue;
325 }
326 let name = format!("{prefix}{key}");
327 let kept = format!("{message} (kept in `ext` as `{name}`)");
328 match ext.keep(key, name, value.clone(), path.clone()) {
329 Ok(()) => self.report(code, path, kept),
330 Err(collision) => self.report(IssueCode::ExtCollision, path, collision),
331 }
332 }
333 }
334}