1use serde::de::DeserializeOwned;
20
21pub fn from_yaml_str<T: DeserializeOwned>(raw: &str) -> Result<T, String> {
24 let de = serde_yaml::Deserializer::from_str(raw);
25 let mut ignored: Vec<String> = Vec::new();
26 let value: T = serde_ignored::deserialize(de, |path| ignored.push(path.to_string()))
27 .map_err(|e| e.to_string())?;
28 reject_ignored(ignored)?;
29 Ok(value)
30}
31
32pub fn from_json_slice<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, String> {
34 let mut de = serde_json::Deserializer::from_slice(bytes);
35 let mut ignored: Vec<String> = Vec::new();
36 let value: T = serde_ignored::deserialize(&mut de, |path| ignored.push(path.to_string()))
37 .map_err(|e| e.to_string())?;
38 de.end().map_err(|e| e.to_string())?;
43 reject_ignored(ignored)?;
44 Ok(value)
45}
46
47fn reject_ignored(mut ignored: Vec<String>) -> Result<(), String> {
48 if ignored.is_empty() {
49 return Ok(());
50 }
51 ignored.sort();
52 ignored.dedup();
53 Err(format!(
54 "unknown field(s): {} — check for typos; fields must match the current schema",
55 ignored.join(", ")
56 ))
57}
58
59#[cfg(test)]
60mod tests {
61 use crate::manifest::Manifest;
62
63 const MINIMAL: &str = r#"
64id: echo-test
65version: 0.0.1
66execute:
67 shell: powershell
68 script: "echo 'kanade'"
69 timeout: 30s
70"#;
71
72 #[test]
73 fn strict_accepts_clean_manifest() {
74 let m: Manifest = super::from_yaml_str(MINIMAL).expect("clean yaml parses");
75 assert_eq!(m.id, "echo-test");
76 }
77
78 #[test]
79 fn strict_rejects_top_level_typo_with_path() {
80 let yaml = format!("{MINIMAL}staleness_polcy: warn\n");
81 let err = super::from_yaml_str::<Manifest>(&yaml).unwrap_err();
82 assert!(err.contains("staleness_polcy"), "{err}");
83 }
84
85 #[test]
86 fn strict_rejects_nested_typo_with_path() {
87 let yaml = r#"
88id: echo-test
89version: 0.0.1
90execute:
91 shell: powershell
92 script_objectt: foo
93 timeout: 30s
94"#;
95 let err = super::from_yaml_str::<Manifest>(yaml).unwrap_err();
96 assert!(err.contains("execute.script_objectt"), "{err}");
98 }
99
100 #[test]
101 fn tolerant_read_accepts_future_fields() {
102 let yaml = format!("{MINIMAL}field_from_the_future: 42\n");
106 let m: Manifest = serde_yaml::from_str(&yaml).expect("tolerant read");
107 assert_eq!(m.id, "echo-test");
108 }
109
110 #[test]
111 fn strict_json_rejects_typo() {
112 let json = serde_json::json!({
113 "id": "echo-test",
114 "version": "0.0.1",
115 "execute": { "shell": "powershell", "script": "echo", "timeout": "30s" },
116 "tyypo": true,
117 });
118 let err =
119 super::from_json_slice::<Manifest>(&serde_json::to_vec(&json).unwrap()).unwrap_err();
120 assert!(err.contains("tyypo"), "{err}");
121 }
122}