use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use toml::Value;
use crate::spec::{FieldKindRef, SchemaRef};
#[derive(Debug)]
pub struct UnknownKeyContext<'a> {
pub path: &'a str,
pub leaf: &'a str,
pub value: Option<&'a Value>,
pub file: Option<&'a Path>,
pub line: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnknownKeyDecision {
Reject,
Accept,
Collect,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CollectedUnknown {
pub path: String,
pub leaf: String,
pub value: Option<Value>,
pub file: Option<std::path::PathBuf>,
pub line: Option<usize>,
}
pub(crate) type UnknownKeyHook =
Arc<dyn Fn(&UnknownKeyContext<'_>) -> UnknownKeyDecision + Send + Sync>;
pub(crate) fn dotted_extension_callback(
path: String,
decision: UnknownKeyDecision,
) -> UnknownKeyHook {
Arc::new(move |ctx: &UnknownKeyContext<'_>| {
let in_subtree = path.is_empty()
|| ctx.path == path
|| ctx
.path
.strip_prefix(&path)
.is_some_and(|rest| rest.starts_with('.'));
if in_subtree && ctx.leaf.contains('.') {
decision
} else {
UnknownKeyDecision::Reject
}
})
}
#[derive(Debug, Default, Clone)]
pub(crate) struct StrictnessOverrides {
entries: HashMap<String, bool>,
}
impl StrictnessOverrides {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn insert(&mut self, path: impl Into<String>, strict: bool) {
self.entries.insert(path.into(), strict);
}
pub fn has_any_strict(&self) -> bool {
self.entries.values().any(|v| *v)
}
pub fn from_schema(schema: SchemaRef<'_>) -> Self {
let mut out = Self::new();
walk_schema_strict(schema, "", &mut out);
out
}
pub fn effective_strict(&self, path: &str, leaf: &str, default: bool) -> bool {
let mut cursor: &str = section_path_of(path, leaf);
loop {
if let Some(v) = self.entries.get(cursor) {
return *v;
}
if cursor.contains('[') {
let schema_form = strip_brackets(cursor);
if let Some(v) = self.entries.get(&schema_form) {
return *v;
}
}
if cursor.is_empty() {
return default;
}
cursor = parent_path(cursor);
}
}
}
fn walk_schema_strict(schema: SchemaRef<'_>, prefix: &str, out: &mut StrictnessOverrides) {
if let Some(value) = schema.strict() {
out.insert(prefix.to_string(), value);
}
for field in schema.fields() {
let dotted = if prefix.is_empty() {
field.name.to_string()
} else {
format!("{prefix}.{}", field.name)
};
match field.kind {
FieldKindRef::Leaf(_) => {
}
FieldKindRef::Nested { schema: nested }
| FieldKindRef::ArrayOf { schema: nested }
| FieldKindRef::MapOf { schema: nested } => {
walk_schema_strict(nested, &dotted, out);
}
}
}
}
fn parent_path(path: &str) -> &str {
let dot = path.rfind('.');
let bracket = path.rfind('[');
match (dot, bracket) {
(Some(d), Some(b)) => &path[..d.max(b)],
(Some(d), None) => &path[..d],
(None, Some(b)) => &path[..b],
(None, None) => "",
}
}
pub(crate) fn section_path_of<'a>(path: &'a str, leaf: &str) -> &'a str {
path.strip_suffix(leaf)
.map(|p| p.strip_suffix('.').unwrap_or(p))
.unwrap_or("")
}
fn strip_brackets(path: &str) -> String {
let mut out = String::with_capacity(path.len());
let mut in_brackets = false;
for ch in path.chars() {
match ch {
'[' => in_brackets = true,
']' => in_brackets = false,
_ if in_brackets => {}
_ => out.push(ch),
}
}
out
}
pub(crate) fn resolve_path_kind(schema: SchemaRef<'_>, dotted: &str) -> PathKind {
if dotted.is_empty() {
return PathKind::Section;
}
let mut current = schema;
let mut segments = dotted.split('.').peekable();
while let Some(seg) = segments.next() {
let mut found = None;
for field in current.fields() {
if field.name == seg {
found = Some(field);
break;
}
}
let Some(field) = found else {
return PathKind::Unknown;
};
match field.kind {
FieldKindRef::Leaf(_) => {
return if segments.peek().is_some() {
PathKind::Unknown
} else {
PathKind::Leaf
};
}
FieldKindRef::Nested { schema: nested }
| FieldKindRef::ArrayOf { schema: nested }
| FieldKindRef::MapOf { schema: nested } => {
if segments.peek().is_none() {
return PathKind::Section;
}
current = nested;
}
}
}
PathKind::Section
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PathKind {
Section,
Leaf,
Unknown,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parent_path_works() {
assert_eq!(parent_path("a.b.c"), "a.b");
assert_eq!(parent_path("a.b"), "a");
assert_eq!(parent_path("a"), "");
assert_eq!(parent_path(""), "");
}
#[test]
fn parent_path_handles_array_indices() {
assert_eq!(parent_path("plugins[0].name"), "plugins[0]");
assert_eq!(parent_path("plugins[0]"), "plugins");
assert_eq!(parent_path("plugins[10].a.b"), "plugins[10].a");
}
#[test]
fn cascade_walks_through_array_indices() {
let mut overrides = StrictnessOverrides::new();
overrides.insert("plugins", false);
assert!(!overrides.effective_strict("plugins[0].rogue", "rogue", true));
}
#[test]
fn cascade_returns_default_with_no_overrides() {
let overrides = StrictnessOverrides::new();
assert!(overrides.effective_strict("any.path.here", "here", true));
assert!(!overrides.effective_strict("any.path.here", "here", false));
}
#[test]
fn cascade_uses_nearest_ancestor() {
let mut overrides = StrictnessOverrides::new();
overrides.insert("a", true);
overrides.insert("a.b", false);
assert!(!overrides.effective_strict("a.b.c", "c", true));
assert!(overrides.effective_strict("a.x", "x", false));
}
#[test]
fn descendant_can_re_tighten() {
let mut overrides = StrictnessOverrides::new();
overrides.insert("plugins", false);
overrides.insert("plugins.audit", true);
assert!(!overrides.effective_strict("plugins.foo.bar", "bar", true));
assert!(overrides.effective_strict("plugins.audit.x", "x", false));
}
#[test]
fn root_override_applies_when_no_more_specific() {
let mut overrides = StrictnessOverrides::new();
overrides.insert("", false);
assert!(!overrides.effective_strict("anything", "anything", true));
}
#[test]
fn cascade_uses_section_path_not_dot_split_for_quoted_leaves() {
let mut overrides = StrictnessOverrides::new();
overrides.insert("diagnostics.rules.acme", true);
assert!(!overrides.effective_strict("diagnostics.rules.acme.task", "acme.task", false,));
}
#[test]
fn cascade_probes_bracket_stripped_form_at_each_step() {
let mut overrides = StrictnessOverrides::new();
overrides.insert("plugins.audit", false);
assert!(!overrides.effective_strict("plugins[0].audit.rogue", "rogue", true,));
}
#[test]
fn strip_brackets_removes_array_indices() {
assert_eq!(strip_brackets("plugins[0].audit"), "plugins.audit");
assert_eq!(strip_brackets("a[10].b[2].c"), "a.b.c");
assert_eq!(strip_brackets("a.b.c"), "a.b.c");
assert_eq!(strip_brackets(""), "");
}
#[test]
fn has_any_strict_reflects_override_values() {
let mut overrides = StrictnessOverrides::new();
assert!(!overrides.has_any_strict());
overrides.insert("a", false);
assert!(!overrides.has_any_strict());
overrides.insert("b", true);
assert!(overrides.has_any_strict());
}
}