1use rskit_errors::{AppError, AppResult};
2use serde_json::Value;
3
4use crate::codec::Codec;
5
6#[derive(Debug, Clone, Copy, Default)]
22pub struct YamlCodec;
23
24impl Codec for YamlCodec {
25 fn name(&self) -> &'static str {
26 "yaml"
27 }
28
29 fn encode_value(&self, value: &Value) -> AppResult<String> {
30 if !value.is_object() {
31 return Err(AppError::invalid_input(
32 "codec",
33 "failed to serialize value as YAML: top level must be a mapping",
34 ));
35 }
36 serde_norway::to_string(value).map_err(|err| {
37 AppError::invalid_input("codec", "failed to serialize value as YAML").with_cause(err)
38 })
39 }
40
41 fn decode_value(&self, contents: &str) -> AppResult<Value> {
42 let value = serde_norway::from_str::<Value>(contents).map_err(|err| {
43 AppError::invalid_input("codec", "failed to parse YAML").with_cause(err)
44 })?;
45 if !value.is_object() {
46 return Err(AppError::invalid_input(
47 "codec",
48 "YAML top level must be a mapping",
49 ));
50 }
51 Ok(value)
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::decode;
59 use serde::Deserialize;
60
61 #[test]
62 fn round_trips_table() {
63 let codec = YamlCodec;
64 let value: Value = serde_json::json!({
65 "name": "svc",
66 "tags": ["alpha", "beta", "gamma"],
67 "nested": { "enabled": true, "retries": 3 }
68 });
69
70 let encoded = codec.encode_value(&value).unwrap();
71 let decoded = codec.decode_value(&encoded).unwrap();
72
73 assert_eq!(decoded, value);
74 assert_eq!(codec.name(), "yaml");
75 }
76
77 #[test]
78 fn rejects_malformed_input() {
79 let err = YamlCodec.decode_value("key: [unclosed").unwrap_err();
80 assert!(err.to_string().contains("parse"));
81 }
82
83 #[test]
84 fn rejects_non_mapping_top_level_on_decode() {
85 for doc in ["- a\n- b", "42", ""] {
87 let err = YamlCodec.decode_value(doc).unwrap_err();
88 assert!(err.to_string().contains("mapping"), "doc: {doc:?}");
89 }
90 }
91
92 #[test]
93 fn rejects_non_mapping_top_level_on_encode() {
94 let err = YamlCodec.encode_value(&Value::Null).unwrap_err();
96 let message = err.to_string();
97 assert!(message.contains("serialize"));
98 assert!(message.contains("mapping"), "names the contract: {message}");
101 }
102
103 #[test]
104 fn decode_honors_deny_unknown_fields() {
105 #[derive(Debug, Deserialize)]
106 #[serde(deny_unknown_fields)]
107 struct Settings {
108 #[expect(dead_code, reason = "only the field set is under test")]
109 name: String,
110 }
111
112 let err = decode::<Settings>(&YamlCodec, "name: svc\nunknown: 1\n").unwrap_err();
113 assert!(err.to_string().contains("deserialize"));
114 }
115}