use serde::de::DeserializeOwned;
pub fn from_yaml_str<T: DeserializeOwned>(raw: &str) -> Result<T, String> {
let de = serde_yaml::Deserializer::from_str(raw);
let mut ignored: Vec<String> = Vec::new();
let value: T = serde_ignored::deserialize(de, |path| ignored.push(path.to_string()))
.map_err(|e| e.to_string())?;
reject_ignored(ignored)?;
Ok(value)
}
pub fn from_json_slice<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, String> {
let mut de = serde_json::Deserializer::from_slice(bytes);
let mut ignored: Vec<String> = Vec::new();
let value: T = serde_ignored::deserialize(&mut de, |path| ignored.push(path.to_string()))
.map_err(|e| e.to_string())?;
de.end().map_err(|e| e.to_string())?;
reject_ignored(ignored)?;
Ok(value)
}
fn reject_ignored(mut ignored: Vec<String>) -> Result<(), String> {
if ignored.is_empty() {
return Ok(());
}
ignored.sort();
ignored.dedup();
Err(format!(
"unknown field(s): {} — check for typos; fields must match the current schema",
ignored.join(", ")
))
}
#[cfg(test)]
mod tests {
use crate::manifest::Manifest;
const MINIMAL: &str = r#"
id: echo-test
version: 0.0.1
execute:
shell: powershell
script: "echo 'kanade'"
timeout: 30s
"#;
#[test]
fn strict_accepts_clean_manifest() {
let m: Manifest = super::from_yaml_str(MINIMAL).expect("clean yaml parses");
assert_eq!(m.id, "echo-test");
}
#[test]
fn strict_rejects_top_level_typo_with_path() {
let yaml = format!("{MINIMAL}staleness_polcy: warn\n");
let err = super::from_yaml_str::<Manifest>(&yaml).unwrap_err();
assert!(err.contains("staleness_polcy"), "{err}");
}
#[test]
fn strict_rejects_nested_typo_with_path() {
let yaml = r#"
id: echo-test
version: 0.0.1
execute:
shell: powershell
script_objectt: foo
timeout: 30s
"#;
let err = super::from_yaml_str::<Manifest>(yaml).unwrap_err();
assert!(err.contains("execute.script_objectt"), "{err}");
}
#[test]
fn tolerant_read_accepts_future_fields() {
let yaml = format!("{MINIMAL}field_from_the_future: 42\n");
let m: Manifest = serde_yaml::from_str(&yaml).expect("tolerant read");
assert_eq!(m.id, "echo-test");
}
#[test]
fn strict_json_rejects_typo() {
let json = serde_json::json!({
"id": "echo-test",
"version": "0.0.1",
"execute": { "shell": "powershell", "script": "echo", "timeout": "30s" },
"tyypo": true,
});
let err =
super::from_json_slice::<Manifest>(&serde_json::to_vec(&json).unwrap()).unwrap_err();
assert!(err.contains("tyypo"), "{err}");
}
}