Skip to main content

dataflow_rs/def/
keys.rs

1//! The known keys per level, and what an unknown key becomes.
2//!
3//! An unknown key moves into the owning node's `ext`: at a nested level under
4//! `loop.<key>`, `for_each.<key>`, `rollout.<key>` or `function.<key>`. It is a
5//! `LIKELY_TYPO` when it is a near miss of a known key at its level, otherwise
6//! an `UNKNOWN_KEY`. With the compat reader on, each takes its default
7//! severity, and the level's v3 spellings are near-miss candidates too; with
8//! it off, both are Rejected, nothing moves, and no v3 spelling is a
9//! candidate: a level's v3 key is then itself an unknown key. Nothing here
10//! looks inside `function.input`, so with the reader off the v3 spellings
11//! there stay for the function's config to refuse.
12
13use 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
20/// A workflow's keys.
21const 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
40/// A task's keys.
41const 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
54/// A group's keys.
55const GROUP_KEYS: &[&str] = &[
56    "id",
57    "name",
58    "description",
59    "condition",
60    "terminal",
61    "tasks",
62    "ext",
63];
64
65/// `loop`'s keys.
66const LOOP_KEYS: &[&str] = &[
67    "counter",
68    "init",
69    "increment",
70    "max",
71    "setup",
72    "over",
73    "as",
74    "scratch",
75];
76
77/// `for_each`'s keys.
78const FOR_EACH_KEYS: &[&str] = &["over", "as", "max_concurrency", "collect", "into"];
79
80/// `rollout`'s keys.
81const ROLLOUT_KEYS: &[&str] = &["bucket_start", "bucket_end"];
82
83/// `function`'s keys.
84const FUNCTION_KEYS: &[&str] = &["name", "input"];
85
86/// A workflow's v3 spellings, which the compat reader rewrites.
87const WORKFLOW_V3_KEYS: &[&str] = &["continue_on_error"];
88
89/// A task's v3 spellings, which the compat reader rewrites.
90pub(super) const TASK_V3_KEYS: &[&str] = &["continue_on_error", "halt_on"];
91
92/// A group's v3 spelling, which the compat reader moves into its `ext`.
93const GROUP_V3_KEYS: &[&str] = &["continue_on_error"];
94
95/// A workflow's v4 policy, which its v3 spelling becomes.
96pub(super) const WORKFLOW_POLICIES: &[&str] = &["on_failure"];
97
98/// A task's v4 policies, which its v3 spellings become.
99pub(super) const TASK_POLICIES: &[&str] = &["on_failure", "on_rejection"];
100
101/// The keys of a `map` input.
102const MAP_KEYS: &[&str] = &["mappings"];
103
104/// The keys of one of `map`'s mappings.
105const MAPPING_KEYS: &[&str] = &["path", "logic", "unset", "on_null", "mode"];
106
107/// The keys of a `validation` input.
108const VALIDATION_KEYS: &[&str] = &["rules"];
109
110/// The keys of one of `validation`'s rules.
111const RULE_KEYS: &[&str] = &["logic", "message"];
112
113/// The keys of a `filter` input.
114const FILTER_KEYS: &[&str] = &["condition", "on_reject"];
115
116/// The keys of a `log` input.
117const LOG_KEYS: &[&str] = &["level", "message", "fields"];
118
119/// The keys of a `parse_json` or `parse_xml` input.
120const PARSE_KEYS: &[&str] = &["source", "target"];
121
122/// The keys of a `publish_json` or `publish_xml` input.
123const PUBLISH_KEYS: &[&str] = &["source", "target", "pretty", "root_element"];
124
125/// What the compat reader knows of a built-in's input: its keys and,
126/// for an input holding an array of nested configs, that array's key and the
127/// keys of its elements. It moves any other key into the task's `ext`.
128///
129/// Each table equals its config's derived fields, which the built-in
130/// configs' tests check.
131#[derive(Clone, Copy, Debug)]
132pub(crate) struct BuiltinInput {
133    /// The input's keys.
134    pub(crate) keys: &'static [&'static str],
135    /// The array of nested configs, and their keys.
136    pub(crate) elements: Option<(&'static str, &'static [&'static str])>,
137}
138
139/// The input of the built-in called `name`; `None` for any other name.
140pub(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
153/// One level's table: its known keys, and the v3 spellings the compat reader
154/// takes there.
155pub(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    /// Whether `key` is one of the level's known keys.
166    pub(super) fn knows(&self, key: &str) -> bool {
167        self.known.contains(&key)
168    }
169
170    /// The level's known keys, for the tests that hold the table to the
171    /// walk, the serializer and the types.
172    #[cfg(test)]
173    pub(super) fn known(&self) -> &'static [&'static str] {
174        self.known
175    }
176}
177
178/// A workflow.
179pub(super) const WORKFLOW: Level = Level {
180    known: WORKFLOW_KEYS,
181    v3: WORKFLOW_V3_KEYS,
182};
183/// A task.
184pub(super) const TASK: Level = Level {
185    known: TASK_KEYS,
186    v3: TASK_V3_KEYS,
187};
188/// A group.
189pub(super) const GROUP: Level = Level {
190    known: GROUP_KEYS,
191    v3: GROUP_V3_KEYS,
192};
193/// `loop`.
194pub(super) const LOOP: Level = Level::new(LOOP_KEYS);
195/// `for_each`.
196pub(super) const FOR_EACH: Level = Level::new(FOR_EACH_KEYS);
197/// `rollout`.
198pub(super) const ROLLOUT: Level = Level::new(ROLLOUT_KEYS);
199/// `function`.
200pub(super) const FUNCTION: Level = Level::new(FUNCTION_KEYS);
201
202/// The known keys at each level of a definition, as the reader, the walk and
203/// the serializer read them. A tool that shows definitions — the debugger
204/// reads it through the wasm package — holds its own view to these rather
205/// than keeping a copy that drifts when a key is added.
206#[derive(Clone, Copy, Debug, Serialize)]
207#[non_exhaustive]
208pub struct DefinitionKeys {
209    /// A workflow's keys.
210    pub workflow: &'static [&'static str],
211    /// A task's keys.
212    pub task: &'static [&'static str],
213    /// A group's keys.
214    pub group: &'static [&'static str],
215    /// A workflow `loop`'s keys.
216    pub r#loop: &'static [&'static str],
217    /// A task `for_each`'s keys.
218    pub for_each: &'static [&'static str],
219    /// A workflow `rollout`'s keys.
220    pub rollout: &'static [&'static str],
221    /// A function reference's keys.
222    pub function: &'static [&'static str],
223}
224
225/// The known keys at each level of a definition.
226#[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
239/// A node's `ext` as it is built: the authored object's entries, then the
240/// keys moved into it (by the compat reader, then by the walk), each
241/// remembered with the path it came from.
242pub(super) struct Ext {
243    map: Map<String, Value>,
244    moved: HashMap<String, AuthoredPath>,
245}
246
247impl Ext {
248    /// The authored `ext`'s entries. One that is not an object is reported
249    /// with the node's other keys, and contributes nothing.
250    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    /// Keeps `value`, moved from the key `key` at `from`, as `name`. The one
258    /// collision rule for every move, the reader's and the walk's: `Err` with
259    /// the message when `ext` already holds `name`, naming where that entry
260    /// came from.
261    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    /// Reports each key of `obj` that is neither known at `level` nor in
293    /// `skip`, in the object's key order, and moves it into `ext` as
294    /// `<prefix><key>` when the compat reader is on.
295    ///
296    /// `skip` names keys refused by a rule of their own, which are not also
297    /// unknown. Under compat, a near miss of one of the level's v3 spellings
298    /// is a `LIKELY_TYPO` naming it; the reader has already taken the
299    /// spelling itself.
300    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        // Built on the first unknown key: the known keys, then (under compat)
310        // the v3 spellings, so a tie goes to the v4 key.
311        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}