use super::parse::Walk;
use crate::base::typo::unknown_key;
use crate::base::{AuthoredPath, IssueCode, Severity};
use serde::Serialize;
use serde_json::{Map, Value};
use std::collections::HashMap;
const WORKFLOW_KEYS: &[&str] = &[
"id",
"name",
"description",
"priority",
"condition",
"tasks",
"on_failure",
"channel",
"status",
"version",
"rollout",
"loop",
"tags",
"created_at",
"updated_at",
"ext",
];
const TASK_KEYS: &[&str] = &[
"id",
"name",
"description",
"condition",
"function",
"on_failure",
"on_rejection",
"terminal",
"for_each",
"ext",
];
const GROUP_KEYS: &[&str] = &[
"id",
"name",
"description",
"condition",
"terminal",
"tasks",
"ext",
];
const LOOP_KEYS: &[&str] = &[
"counter",
"init",
"increment",
"max",
"setup",
"over",
"as",
"scratch",
];
const FOR_EACH_KEYS: &[&str] = &["over", "as", "max_concurrency", "collect", "into"];
const ROLLOUT_KEYS: &[&str] = &["bucket_start", "bucket_end"];
const FUNCTION_KEYS: &[&str] = &["name", "input"];
const WORKFLOW_V3_KEYS: &[&str] = &["continue_on_error"];
pub(super) const TASK_V3_KEYS: &[&str] = &["continue_on_error", "halt_on"];
const GROUP_V3_KEYS: &[&str] = &["continue_on_error"];
pub(super) const WORKFLOW_POLICIES: &[&str] = &["on_failure"];
pub(super) const TASK_POLICIES: &[&str] = &["on_failure", "on_rejection"];
const MAP_KEYS: &[&str] = &["mappings"];
const MAPPING_KEYS: &[&str] = &["path", "logic", "unset", "on_null", "mode"];
const VALIDATION_KEYS: &[&str] = &["rules"];
const RULE_KEYS: &[&str] = &["logic", "message"];
const FILTER_KEYS: &[&str] = &["condition", "on_reject"];
const LOG_KEYS: &[&str] = &["level", "message", "fields"];
const PARSE_KEYS: &[&str] = &["source", "target"];
const PUBLISH_KEYS: &[&str] = &["source", "target", "pretty", "root_element"];
#[derive(Clone, Copy, Debug)]
pub(crate) struct BuiltinInput {
pub(crate) keys: &'static [&'static str],
pub(crate) elements: Option<(&'static str, &'static [&'static str])>,
}
pub(crate) fn builtin_input(name: &str) -> Option<BuiltinInput> {
let (keys, elements) = match name {
"map" => (MAP_KEYS, Some(("mappings", MAPPING_KEYS))),
super::VALIDATION => (VALIDATION_KEYS, Some(("rules", RULE_KEYS))),
"filter" => (FILTER_KEYS, None),
"log" => (LOG_KEYS, None),
"parse_json" | "parse_xml" => (PARSE_KEYS, None),
"publish_json" | "publish_xml" => (PUBLISH_KEYS, None),
_ => return None,
};
Some(BuiltinInput { keys, elements })
}
pub(super) struct Level {
known: &'static [&'static str],
v3: &'static [&'static str],
}
impl Level {
const fn new(known: &'static [&'static str]) -> Self {
Self { known, v3: &[] }
}
pub(super) fn knows(&self, key: &str) -> bool {
self.known.contains(&key)
}
#[cfg(test)]
pub(super) fn known(&self) -> &'static [&'static str] {
self.known
}
}
pub(super) const WORKFLOW: Level = Level {
known: WORKFLOW_KEYS,
v3: WORKFLOW_V3_KEYS,
};
pub(super) const TASK: Level = Level {
known: TASK_KEYS,
v3: TASK_V3_KEYS,
};
pub(super) const GROUP: Level = Level {
known: GROUP_KEYS,
v3: GROUP_V3_KEYS,
};
pub(super) const LOOP: Level = Level::new(LOOP_KEYS);
pub(super) const FOR_EACH: Level = Level::new(FOR_EACH_KEYS);
pub(super) const ROLLOUT: Level = Level::new(ROLLOUT_KEYS);
pub(super) const FUNCTION: Level = Level::new(FUNCTION_KEYS);
#[derive(Clone, Copy, Debug, Serialize)]
#[non_exhaustive]
pub struct DefinitionKeys {
pub workflow: &'static [&'static str],
pub task: &'static [&'static str],
pub group: &'static [&'static str],
pub r#loop: &'static [&'static str],
pub for_each: &'static [&'static str],
pub rollout: &'static [&'static str],
pub function: &'static [&'static str],
}
#[must_use]
pub fn definition_keys() -> DefinitionKeys {
DefinitionKeys {
workflow: WORKFLOW_KEYS,
task: TASK_KEYS,
group: GROUP_KEYS,
r#loop: LOOP_KEYS,
for_each: FOR_EACH_KEYS,
rollout: ROLLOUT_KEYS,
function: FUNCTION_KEYS,
}
}
pub(super) struct Ext {
map: Map<String, Value>,
moved: HashMap<String, AuthoredPath>,
}
impl Ext {
pub(super) fn authored(raw: Option<&Value>) -> Self {
Self {
map: raw.and_then(Value::as_object).cloned().unwrap_or_default(),
moved: HashMap::new(),
}
}
pub(super) fn keep(
&mut self,
key: &str,
name: String,
value: Value,
from: AuthoredPath,
) -> Result<(), String> {
if self.map.contains_key(&name) {
return Err(match self.moved.get(&name) {
Some(first) => format!(
"`{key}` cannot be kept in `ext` as `{name}`: the key at `{first}` is \
already kept there"
),
None => {
format!(
"`{key}` appears both on the node and in its authored `ext`, as `{name}`"
)
}
});
}
self.map.insert(name.clone(), value);
self.moved.insert(name, from);
Ok(())
}
pub(super) fn into_map(self) -> Map<String, Value> {
self.map
}
}
impl Walk<'_> {
pub(super) fn unknown_keys(
&mut self,
obj: &Map<String, Value>,
level: &Level,
skip: &[&str],
at: &AuthoredPath,
prefix: &str,
ext: &mut Ext,
) {
let mut candidates: Option<Vec<&str>> = None;
for (key, value) in obj {
if level.knows(key) || skip.contains(&key.as_str()) {
continue;
}
let path = at.join(key.as_str());
let candidates = candidates.get_or_insert_with(|| {
let v3 = if self.compat { level.v3 } else { &[] };
level.known.iter().chain(v3).copied().collect()
});
let (code, message) = unknown_key(key, candidates);
if !self.compat {
self.report_as(code, Severity::Rejected, path, message);
continue;
}
let name = format!("{prefix}{key}");
let kept = format!("{message} (kept in `ext` as `{name}`)");
match ext.keep(key, name, value.clone(), path.clone()) {
Ok(()) => self.report(code, path, kept),
Err(collision) => self.report(IssueCode::ExtCollision, path, collision),
}
}
}
}