use serde_yaml::Value as YamlValue;
use std::collections::HashSet;
pub const REFERENCE_FILES: &[&str] = &[
"config.yaml",
"channels.yaml",
"providers.yaml",
"agent.yaml",
"workflow.yaml",
"tools.yaml",
"cron.yaml",
"mcp.yaml",
"a2a.yaml",
"skills.yaml",
"context.yaml",
"memory.yaml",
];
pub fn reference_yaml(filename: &str) -> Option<&'static str> {
match filename {
"config.yaml" => Some(include_str!("../medic/config.yaml")),
"channels.yaml" => Some(include_str!("../medic/channels.yaml")),
"providers.yaml" => Some(include_str!("../medic/providers.yaml")),
"agent.yaml" => Some(include_str!("../medic/agent.yaml")),
"workflow.yaml" => Some(include_str!("../medic/workflow.yaml")),
"tools.yaml" => Some(include_str!("../medic/tools.yaml")),
"cron.yaml" => Some(include_str!("../medic/cron.yaml")),
"mcp.yaml" => Some(include_str!("../medic/mcp.yaml")),
"a2a.yaml" => Some(include_str!("../medic/a2a.yaml")),
"skills.yaml" => Some(include_str!("../medic/skills.yaml")),
"context.yaml" => Some(include_str!("../medic/context.yaml")),
"memory.yaml" => Some(include_str!("../medic/memory.yaml")),
_ => None,
}
}
pub fn disallowed_top_level_keys(user: &YamlValue, reference: &YamlValue) -> Vec<String> {
let allowed: HashSet<String> = reference
.as_mapping()
.map(|m| {
m.keys()
.filter_map(|k| k.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
user.as_mapping()
.map(|m| {
m.keys()
.filter_map(|k| k.as_str().map(String::from))
.filter(|k| !allowed.contains(k))
.collect()
})
.unwrap_or_default()
}
pub fn allowed_key_paths_shallow(reference: &YamlValue) -> HashSet<String> {
let mut out = HashSet::new();
let map = match reference.as_mapping() {
Some(m) => m,
None => return out,
};
for (k, v) in map {
let top = k.as_str().unwrap_or("").to_string();
out.insert(top.clone());
if let Some(nested) = v.as_mapping() {
for (k2, _) in nested {
let second = k2.as_str().unwrap_or("").to_string();
out.insert(format!("{}.{}", top, second));
}
}
}
out
}
pub fn extra_top_level_keys(user: &YamlValue, reference: &YamlValue) -> Vec<String> {
disallowed_top_level_keys(user, reference)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reference_yaml_returns_some_for_known_files() {
assert!(reference_yaml("config.yaml").is_some());
assert!(reference_yaml("channels.yaml").is_some());
assert!(reference_yaml("workflow.yaml").is_some());
assert!(reference_yaml("unknown.yaml").is_none());
}
#[test]
fn disallowed_keys_detects_extra() {
let ref_yaml = "a: 1\nb: 2";
let user_yaml = "a: 1\nb: 2\nc: 3";
let ref_val: YamlValue = serde_yaml::from_str(ref_yaml).unwrap();
let user_val: YamlValue = serde_yaml::from_str(user_yaml).unwrap();
let disallowed = disallowed_top_level_keys(&user_val, &ref_val);
assert_eq!(disallowed, vec!["c"]);
}
#[test]
fn disallowed_keys_empty_when_subset() {
let ref_yaml = "a: 1\nb: 2\nc: 3";
let user_yaml = "a: 1";
let ref_val: YamlValue = serde_yaml::from_str(ref_yaml).unwrap();
let user_val: YamlValue = serde_yaml::from_str(user_yaml).unwrap();
let disallowed = disallowed_top_level_keys(&user_val, &ref_val);
assert!(disallowed.is_empty());
}
}