use std::collections::BTreeMap;
use super::component_problem;
use super::layout::{RelPath, TAG_TYPES};
use super::manifest::Manifest;
use super::target::Target;
use crate::error::DeployFinding;
pub const DEFAULT_EXCLUDED_TAG_PROVIDERS: [&str; 1] = ["System"];
pub fn entry_problem(name: &str) -> Option<&'static str> {
if !name.is_empty() && name.trim().is_empty() {
Some("must not be blank")
} else if name.trim() != name {
Some("must not have leading or trailing whitespace")
} else if name.chars().any(char::is_control) {
Some("must not contain a control character")
} else {
component_problem(name)
}
}
pub(crate) fn list_problems(list: &[String]) -> Vec<String> {
let mut problems = Vec::new();
for (i, name) in list.iter().enumerate() {
if let Some(why) = entry_problem(name) {
problems.push(format!("exclude_tag_providers[{i}] {name:?}: {why}"));
}
if list[..i]
.iter()
.any(|earlier| earlier.eq_ignore_ascii_case(name))
{
problems.push(format!(
"exclude_tag_providers[{i}] {name:?}: duplicate tag provider"
));
}
}
problems
}
pub fn effective(target: &Target, manifest: Option<&Manifest>) -> Vec<String> {
let mut by_fold: BTreeMap<String, String> = BTreeMap::new();
let listed = DEFAULT_EXCLUDED_TAG_PROVIDERS
.iter()
.map(|p| (*p).to_string())
.chain(target.exclude_tag_providers.iter().cloned())
.chain(
manifest
.into_iter()
.flat_map(|m| m.exclude_tag_providers.iter().cloned()),
);
for name in listed {
let fold = name.to_ascii_lowercase();
match by_fold.get_mut(&fold) {
Some(kept) if name.as_bytes() < kept.as_bytes() => *kept = name,
Some(_) => {}
None => {
by_fold.insert(fold, name);
}
}
}
let mut names: Vec<String> = by_fold.into_values().collect();
names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
names
}
pub fn is_excluded(provider: &str, excluded: &[String]) -> bool {
excluded.iter().any(|e| e.eq_ignore_ascii_case(provider))
}
pub fn excluded_provider<'a>(path: &'a RelPath, excluded: &[String]) -> Option<&'a str> {
let mut parts = path.components();
let (module, ty, provider) = (parts.next()?, parts.next()?, parts.next()?);
(module == "ignition" && TAG_TYPES.contains(&ty) && is_excluded(provider, excluded))
.then_some(provider)
}
pub fn contradictions(
excluded: &[String],
manifest: &Manifest,
manifest_file: &str,
) -> Vec<DeployFinding> {
manifest
.tag_providers
.iter()
.enumerate()
.filter(|(_, provider)| is_excluded(provider, excluded))
.map(|(i, provider)| DeployFinding {
file: manifest_file.to_string(),
line: None,
column: None,
message: format!(
"tag_providers[{i}] {provider:?}: the tag provider is excluded (by exclude_tag_providers or the built-in System default); an excluded provider cannot be declared"
),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn target(extra: &str) -> Target {
let yaml = format!(
"schema_version: 1\ngateway: g\nmode: dev\nnodes:\n master:\n profile: dev\n{extra}"
);
Target::parse("targets/dev.yaml", yaml.as_bytes()).expect("target")
}
fn manifest(extra: &str) -> Manifest {
let yaml = format!("schema_version: 1\ntypes: []\n{extra}");
Manifest::parse("gateways/g/manifest.yaml", yaml.as_bytes()).expect("manifest")
}
#[test]
fn exclude_default_is_system_alone() {
assert_eq!(DEFAULT_EXCLUDED_TAG_PROVIDERS, ["System"]);
assert_eq!(effective(&target(""), None), ["System"]);
assert_eq!(effective(&target(""), Some(&manifest(""))), ["System"]);
}
#[test]
fn exclude_effective_unions_default_target_and_manifest() {
let t = target("exclude_tag_providers: [\"MQTT Engine\"]\n");
let m = manifest("exclude_tag_providers: [\"Sparkplug B\", system]\n");
assert_eq!(
effective(&t, Some(&m)),
["MQTT Engine", "Sparkplug B", "System"]
);
let both = manifest("exclude_tag_providers: [\"MQTT Engine\"]\n");
assert_eq!(effective(&t, Some(&both)), ["MQTT Engine", "System"]);
}
#[test]
fn exclude_is_excluded_folds_ascii_case() {
let excluded = vec!["MQTT Engine".to_string(), "System".to_string()];
assert!(is_excluded("system", &excluded));
assert!(is_excluded("mqtt ENGINE", &excluded));
assert!(!is_excluded("default", &excluded));
assert!(!is_excluded("MQTT", &excluded));
}
#[test]
fn exclude_contradictions_flag_declared_excluded_providers() {
let m = manifest("tag_providers: [default, system]\n");
let found = contradictions(
&effective(&target(""), Some(&m)),
&m,
"gateways/g/manifest.yaml",
);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].file, "gateways/g/manifest.yaml");
assert_eq!((found[0].line, found[0].column), (None, None));
assert!(
found[0].message.starts_with("tag_providers[1] \"system\"")
&& found[0].message.contains("excluded"),
"{}",
found[0].message
);
let t = target("exclude_tag_providers: [\"MQTT Engine\"]\n");
let m = manifest("tag_providers: [\"MQTT Engine\", default]\n");
let found = contradictions(&effective(&t, Some(&m)), &m, "gateways/g/manifest.yaml");
assert_eq!(found.len(), 1, "{found:?}");
assert!(
found[0]
.message
.starts_with("tag_providers[0] \"MQTT Engine\""),
"{}",
found[0].message
);
let m = manifest("tag_providers: [default]\n");
assert!(contradictions(&effective(&t, Some(&m)), &m, "x").is_empty());
}
#[test]
fn exclude_excluded_provider_matches_the_four_tag_types_only() {
let excluded = vec!["MQTT Engine".to_string(), "System".to_string()];
let of = |p: &str| {
let path = RelPath::new(p).expect("path");
excluded_provider(&path, &excluded).map(str::to_string)
};
for (path, want) in [
(
"ignition/tag-provider/MQTT Engine/config.json",
Some("MQTT Engine"),
),
(
"ignition/tag-group/System/Default/config.json",
Some("System"),
),
(
"ignition/tag-type-definition/system/T/tags.json",
Some("system"),
),
(
"ignition/tag-definition/mqtt engine/Edge Nodes/tags.json",
Some("mqtt engine"),
),
("ignition/tag-provider/default/config.json", None),
("ignition/database-translator/MQTT Engine/config.json", None),
("com.example/tag-provider/MQTT Engine/config.json", None),
] {
assert_eq!(of(path).as_deref(), want, "{path}");
}
}
#[test]
fn exclude_entry_problem_refuses_blank_padded_and_unsafe_names() {
assert_eq!(entry_problem("MQTT Engine"), None);
assert_eq!(entry_problem("System"), None);
for bad in [
"",
" ",
" MQTT Engine",
"MQTT Engine\t",
"a/b",
".x",
"..",
] {
assert!(entry_problem(bad).is_some(), "{bad:?}");
}
}
#[test]
fn exclude_entry_problem_refuses_control_characters() {
for bad in [
"MQTT\nEngine",
"MQTT\rEngine",
"MQTT\u{1b}[31mEngine",
"a\u{7f}b",
"a\u{85}b",
"a\u{0}b",
] {
assert_eq!(
entry_problem(bad),
Some("must not contain a control character"),
"{bad:?}"
);
}
assert_eq!(entry_problem("\n"), Some("must not be blank"));
assert_eq!(
entry_problem("MQTT Engine\n"),
Some("must not have leading or trailing whitespace")
);
assert_eq!(entry_problem("MQTT Engine (caf\u{e9})"), None);
}
}