agent_first_data/document/format/
yaml.rs1use crate::document::{DocumentError, DocumentResult, Value};
6use noyalib::{
7 DuplicateKeyPolicy, Mapping as YamlMapping, ParserConfig, Value as YamlValue,
8 cst::parse_document, from_str_with_config, to_string,
9};
10
11pub fn load(content: &str) -> DocumentResult<Value> {
12 let parser_config = ParserConfig::new()
13 .duplicate_key_policy(DuplicateKeyPolicy::Error)
14 .lossless_u64_integers(true);
15 from_str_with_config::<YamlValue>(content, &parser_config)
16 .map(value_to_our_value)
17 .map_err(|e| DocumentError::ParseError {
18 format: "YAML".to_string(),
19 detail: e.to_string(),
20 })
21}
22
23pub fn set_preserving(content: &str, path: &str, value: &Value) -> DocumentResult<String> {
27 let segments = crate::document::parse_path(path)?;
28 let yaml_path = cst_path(&segments, "set")?;
29 if !matches!(
30 value,
31 Value::Null
32 | Value::Bool(_)
33 | Value::Integer(_)
34 | Value::Unsigned(_)
35 | Value::Float(_)
36 | Value::Number(_)
37 | Value::String(_)
38 ) {
39 return Err(DocumentError::UnsupportedOperation {
40 format: "YAML".to_string(),
41 operation: "set".to_string(),
42 detail: "collection mutation requires a dedicated CST fragment editor".to_string(),
43 });
44 }
45 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
46 format: "YAML".to_string(),
47 detail: error.to_string(),
48 })?;
49 let exists = load(content)
52 .ok()
53 .is_some_and(|loaded| crate::document::get_path_ref(&loaded, path, &[]).is_ok());
54 if exists {
55 document
56 .set_value(&yaml_path, &to_noyalib_value(value)?)
57 .map_err(|error| DocumentError::UnsupportedOperation {
58 format: "YAML".to_string(),
59 operation: "set".to_string(),
60 detail: error.to_string(),
61 })?;
62 } else {
63 let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
64 if last.parse::<usize>().is_ok() {
65 return Err(DocumentError::UnsupportedOperation {
66 format: "YAML".to_string(),
67 operation: "set".to_string(),
68 detail: "cannot create a new sequence index; the element must already exist"
69 .to_string(),
70 });
71 }
72 let parent_path = if parents.is_empty() {
73 String::new()
74 } else {
75 cst_path(parents, "set")?
76 };
77 let fragment = to_string(&to_noyalib_value(value)?).map_err(|error| {
78 DocumentError::UnsupportedOperation {
79 format: "YAML".to_string(),
80 operation: "set".to_string(),
81 detail: error.to_string(),
82 }
83 })?;
84 document
85 .insert_entry(&parent_path, last, fragment.trim_end())
86 .map_err(|error| DocumentError::UnsupportedOperation {
87 format: "YAML".to_string(),
88 operation: "set".to_string(),
89 detail: error.to_string(),
90 })?;
91 }
92 document
93 .validate()
94 .map_err(|error| DocumentError::ParseError {
95 format: "YAML".to_string(),
96 detail: error.to_string(),
97 })?;
98 Ok(document.to_string())
99}
100
101pub fn unset_preserving(content: &str, path: &str) -> DocumentResult<String> {
103 let segments = crate::document::parse_path(path)?;
104 let yaml_path = cst_path(&segments, "unset")?;
105 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
106 format: "YAML".to_string(),
107 detail: error.to_string(),
108 })?;
109 document
110 .remove(&yaml_path)
111 .map_err(|error| DocumentError::UnsupportedOperation {
112 format: "YAML".to_string(),
113 operation: "unset".to_string(),
114 detail: error.to_string(),
115 })?;
116 document
117 .validate()
118 .map_err(|error| DocumentError::ParseError {
119 format: "YAML".to_string(),
120 detail: error.to_string(),
121 })?;
122 Ok(document.to_string())
123}
124
125pub fn append_array_item_preserving(
128 content: &str,
129 path: &str,
130 item: &Value,
131) -> DocumentResult<String> {
132 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
133 format: "YAML".to_string(),
134 detail: error.to_string(),
135 })?;
136 let fragment = to_string(&to_noyalib_value(item)?).map_err(|error| {
137 DocumentError::UnsupportedOperation {
138 format: "YAML".to_string(),
139 operation: "add".to_string(),
140 detail: error.to_string(),
141 }
142 })?;
143 let yaml_path = cst_path(&crate::document::parse_path(path)?, "add")?;
144 document
145 .push_back(&yaml_path, fragment.trim_end())
146 .map_err(|error| DocumentError::UnsupportedOperation {
147 format: "YAML".to_string(),
148 operation: "add".to_string(),
149 detail: error.to_string(),
150 })?;
151 document
152 .validate()
153 .map_err(|error| DocumentError::ParseError {
154 format: "YAML".to_string(),
155 detail: error.to_string(),
156 })?;
157 Ok(document.to_string())
158}
159
160pub fn remove_array_item_preserving(
162 content: &str,
163 path: &str,
164 index: usize,
165) -> DocumentResult<String> {
166 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
167 format: "YAML".to_string(),
168 detail: error.to_string(),
169 })?;
170 let yaml_path = cst_path(&crate::document::parse_path(path)?, "remove")?;
171 document
172 .remove(&format!("{yaml_path}[{index}]"))
173 .map_err(|error| DocumentError::UnsupportedOperation {
174 format: "YAML".to_string(),
175 operation: "remove".to_string(),
176 detail: error.to_string(),
177 })?;
178 document
179 .validate()
180 .map_err(|error| DocumentError::ParseError {
181 format: "YAML".to_string(),
182 detail: error.to_string(),
183 })?;
184 Ok(document.to_string())
185}
186
187fn cst_path(segments: &[String], operation: &str) -> DocumentResult<String> {
188 if segments.is_empty() {
189 return Err(DocumentError::UnsupportedOperation {
190 format: "YAML".to_string(),
191 operation: operation.to_string(),
192 detail: "root mutation is not supported by the CST path adapter".to_string(),
193 });
194 }
195 let mut path = String::new();
196 for segment in segments {
197 if segment.contains(['.', '\\']) {
198 return Err(DocumentError::UnsupportedOperation {
199 format: "YAML".to_string(),
200 operation: operation.to_string(),
201 detail: "escaped YAML keys require a quoted-key CST span and are not supported"
202 .to_string(),
203 });
204 }
205 if let Ok(index) = segment.parse::<usize>() {
206 path.push_str(&format!("[{index}]"));
207 } else {
208 if !path.is_empty() {
209 path.push('.');
210 }
211 path.push_str(segment);
212 }
213 }
214 Ok(path)
215}
216
217fn to_noyalib_value(value: &Value) -> DocumentResult<YamlValue> {
218 match value {
219 Value::Null => Ok(YamlValue::Null),
220 Value::Bool(value) => Ok(YamlValue::Bool(*value)),
221 Value::Integer(value) => Ok(YamlValue::from(*value)),
222 Value::Unsigned(value) => Ok(YamlValue::from(*value)),
223 Value::Float(value) if value.is_finite() => Ok(YamlValue::from(*value)),
224 Value::Float(_) => Err(DocumentError::UnsupportedOperation {
225 format: "YAML".to_string(),
226 operation: "set".to_string(),
227 detail: "non-finite YAML float is not representable".to_string(),
228 }),
229 Value::Number(text) if value.is_float() => text
235 .parse::<f64>()
236 .ok()
237 .filter(|value| value.is_finite())
238 .map(YamlValue::from)
239 .ok_or_else(|| DocumentError::UnsupportedOperation {
240 format: "YAML".to_string(),
241 operation: "set".to_string(),
242 detail: format!("float literal `{text}` is not representable in YAML"),
243 }),
244 Value::Number(text) => Err(DocumentError::UnsupportedOperation {
245 format: "YAML".to_string(),
246 operation: "set".to_string(),
247 detail: format!("integer literal `{text}` exceeds YAML's 64-bit integer range"),
248 }),
249 Value::String(value) => Ok(YamlValue::String(value.clone())),
250 Value::Array(values) => Ok(YamlValue::Sequence(
251 values
252 .iter()
253 .map(to_noyalib_value)
254 .collect::<DocumentResult<Vec<_>>>()?,
255 )),
256 Value::Object(values) => {
257 let mut mapping = YamlMapping::new();
258 for (key, value) in values {
259 mapping.insert(key.clone(), to_noyalib_value(value)?);
260 }
261 Ok(YamlValue::Mapping(mapping))
262 }
263 }
264}
265
266pub fn save(value: &Value) -> DocumentResult<String> {
267 let yaml_val = our_value_to_yaml_value(value)?;
268 to_string(&yaml_val).map_err(|e| DocumentError::ParseError {
269 format: "YAML".to_string(),
270 detail: e.to_string(),
271 })
272}
273
274fn value_to_our_value(v: YamlValue) -> Value {
275 match v {
276 YamlValue::Null => Value::Null,
277 YamlValue::Bool(b) => Value::Bool(b),
278 YamlValue::Number(n) => {
279 if let Some(i) = n.as_i64() {
280 Value::Integer(i)
281 } else if let Some(u) = n.as_u64() {
282 Value::Unsigned(u)
283 } else {
284 Value::Float(n.as_f64())
285 }
286 }
287 YamlValue::String(s) => Value::String(s),
288 YamlValue::Sequence(seq) => Value::Array(seq.into_iter().map(value_to_our_value).collect()),
289 YamlValue::Mapping(map) => {
290 let mut obj = std::collections::BTreeMap::new();
291 for (key, value) in map {
292 obj.insert(key, value_to_our_value(value));
293 }
294 Value::Object(obj)
295 }
296 YamlValue::Tagged(t) => {
297 let (_, value) = t.into_parts();
299 value_to_our_value(value)
300 }
301 }
302}
303
304fn our_value_to_yaml_value(v: &Value) -> DocumentResult<YamlValue> {
305 match v {
306 Value::Null => Ok(YamlValue::Null),
307 Value::Bool(b) => Ok(YamlValue::Bool(*b)),
308 Value::Integer(i) => Ok(YamlValue::Number((*i).into())),
309 Value::Unsigned(i) => Ok(YamlValue::Number((*i).into())),
310 Value::Float(f) => {
311 Ok(YamlValue::Number((*f).into()))
313 }
314 Value::Number(text) if v.is_float() => text
315 .parse::<f64>()
316 .ok()
317 .filter(|value| value.is_finite())
318 .map(|value| YamlValue::Number(value.into()))
319 .ok_or_else(|| DocumentError::UnsupportedOperation {
320 format: "YAML".to_string(),
321 operation: "save".to_string(),
322 detail: format!("float literal `{text}` is not representable in YAML"),
323 }),
324 Value::Number(text) => Err(DocumentError::UnsupportedOperation {
325 format: "YAML".to_string(),
326 operation: "save".to_string(),
327 detail: format!("integer literal `{text}` exceeds YAML's 64-bit integer range"),
328 }),
329 Value::String(s) => Ok(YamlValue::String(s.clone())),
330 Value::Array(a) => {
331 let seq = a
332 .iter()
333 .map(our_value_to_yaml_value)
334 .collect::<DocumentResult<Vec<_>>>()?;
335 Ok(YamlValue::Sequence(seq))
336 }
337 Value::Object(o) => {
338 let mut mapping = YamlMapping::new();
339 for (k, v) in o {
340 mapping.insert(k.clone(), our_value_to_yaml_value(v)?);
341 }
342 Ok(YamlValue::Mapping(mapping))
343 }
344 }
345}