1use crate::document::{DocumentError, DocumentResult, Value};
6use noyalib::{
7 DuplicateKeyPolicy, Mapping as YamlMapping, ParserConfig, Value as YamlValue,
8 cst::{GreenChild, GreenNode, SyntaxKind, parse_document},
9 from_str_with_config, to_string,
10};
11
12pub fn load(content: &str) -> DocumentResult<Value> {
13 let (protected, literals) = protect_exact_numbers(content)?;
14 let parser_config = ParserConfig::new()
15 .duplicate_key_policy(DuplicateKeyPolicy::Error)
16 .lossless_u64_integers(true);
17 from_str_with_config::<YamlValue>(&protected, &parser_config)
18 .map(|value| value_to_our_value(value, &literals))
19 .map_err(|e| DocumentError::ParseError {
20 format: "YAML".to_string(),
21 detail: e.to_string(),
22 })
23}
24
25pub fn set_preserving(content: &str, path: &str, value: &Value) -> DocumentResult<String> {
29 let segments = crate::document::parse_path(path)?;
30 let yaml_path = cst_path(&segments, "set")?;
31 guard_cst_segments(content, &segments, "set", true)?;
32 if !matches!(
33 value,
34 Value::Null
35 | Value::Bool(_)
36 | Value::Integer(_)
37 | Value::Unsigned(_)
38 | Value::Float(_)
39 | Value::Number(_)
40 | Value::String(_)
41 ) {
42 return Err(DocumentError::UnsupportedOperation {
43 format: "YAML".to_string(),
44 operation: "set".to_string(),
45 detail: "collection mutation requires a dedicated CST fragment editor".to_string(),
46 });
47 }
48 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
49 format: "YAML".to_string(),
50 detail: error.to_string(),
51 })?;
52 let exists = load(content)
55 .ok()
56 .is_some_and(|loaded| crate::document::get_path_ref(&loaded, path, &[]).is_ok());
57 if exists {
58 let result = if let Value::Number(text) = value {
59 document.set(&yaml_path, text)
60 } else {
61 document.set_value(&yaml_path, &to_noyalib_value(value)?)
62 };
63 result.map_err(|error| DocumentError::UnsupportedOperation {
64 format: "YAML".to_string(),
65 operation: "set".to_string(),
66 detail: error.to_string(),
67 })?;
68 } else {
69 let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
70 if last.parse::<usize>().is_ok() {
71 return Err(DocumentError::UnsupportedOperation {
72 format: "YAML".to_string(),
73 operation: "set".to_string(),
74 detail: "cannot create a new sequence index; the element must already exist"
75 .to_string(),
76 });
77 }
78 let parent_path = if parents.is_empty() {
79 String::new()
80 } else {
81 cst_path(parents, "set")?
82 };
83 let fragment = yaml_fragment(value, "set")?;
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 guard_cst_segments(content, &segments, "unset", false)?;
106 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
107 format: "YAML".to_string(),
108 detail: error.to_string(),
109 })?;
110 document
111 .remove(&yaml_path)
112 .map_err(|error| DocumentError::UnsupportedOperation {
113 format: "YAML".to_string(),
114 operation: "unset".to_string(),
115 detail: error.to_string(),
116 })?;
117 document
118 .validate()
119 .map_err(|error| DocumentError::ParseError {
120 format: "YAML".to_string(),
121 detail: error.to_string(),
122 })?;
123 Ok(document.to_string())
124}
125
126pub fn append_array_item_preserving(
129 content: &str,
130 path: &str,
131 item: &Value,
132) -> DocumentResult<String> {
133 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
134 format: "YAML".to_string(),
135 detail: error.to_string(),
136 })?;
137 let fragment = yaml_fragment(item, "add")?;
138 let yaml_path = cst_path(&crate::document::parse_path(path)?, "add")?;
139 document
140 .push_back(&yaml_path, fragment.trim_end())
141 .map_err(|error| DocumentError::UnsupportedOperation {
142 format: "YAML".to_string(),
143 operation: "add".to_string(),
144 detail: error.to_string(),
145 })?;
146 document
147 .validate()
148 .map_err(|error| DocumentError::ParseError {
149 format: "YAML".to_string(),
150 detail: error.to_string(),
151 })?;
152 Ok(document.to_string())
153}
154
155pub fn remove_array_item_preserving(
157 content: &str,
158 path: &str,
159 index: usize,
160) -> DocumentResult<String> {
161 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
162 format: "YAML".to_string(),
163 detail: error.to_string(),
164 })?;
165 let yaml_path = cst_path(&crate::document::parse_path(path)?, "remove")?;
166 document
167 .remove(&format!("{yaml_path}[{index}]"))
168 .map_err(|error| DocumentError::UnsupportedOperation {
169 format: "YAML".to_string(),
170 operation: "remove".to_string(),
171 detail: error.to_string(),
172 })?;
173 document
174 .validate()
175 .map_err(|error| DocumentError::ParseError {
176 format: "YAML".to_string(),
177 detail: error.to_string(),
178 })?;
179 Ok(document.to_string())
180}
181
182fn cst_path(segments: &[String], operation: &str) -> DocumentResult<String> {
183 if segments.is_empty() {
184 return Err(DocumentError::UnsupportedOperation {
185 format: "YAML".to_string(),
186 operation: operation.to_string(),
187 detail: "root mutation is not supported by the CST path adapter".to_string(),
188 });
189 }
190 let mut path = String::new();
191 for segment in segments {
192 if segment.contains(['.', '\\', '[', ']']) {
193 return Err(DocumentError::UnsupportedOperation {
194 format: "YAML".to_string(),
195 operation: operation.to_string(),
196 detail:
197 "escaped or bracketed YAML keys require a quoted-key CST span and are not supported"
198 .to_string(),
199 });
200 }
201 if let Ok(index) = segment.parse::<usize>() {
202 path.push_str(&format!("[{index}]"));
203 } else {
204 if !path.is_empty() {
205 path.push('.');
206 }
207 path.push_str(segment);
208 }
209 }
210 Ok(path)
211}
212
213fn guard_cst_segments(
214 content: &str,
215 segments: &[String],
216 operation: &str,
217 allow_missing: bool,
218) -> DocumentResult<()> {
219 let root = load(content)?;
220 let mut current = &root;
221 for (index, segment) in segments.iter().enumerate() {
222 match current {
223 Value::Object(object) => {
224 if segment.parse::<usize>().is_ok() || segment.contains(['[', ']']) {
225 return Err(DocumentError::UnsupportedOperation {
226 format: "YAML".to_string(),
227 operation: operation.to_string(),
228 detail: format!(
229 "mapping key `{segment}` is ambiguous in the CST path grammar"
230 ),
231 });
232 }
233 match object.get(segment) {
234 Some(next) => current = next,
235 None if allow_missing => {
236 if segments[index..]
237 .iter()
238 .any(|part| part.parse::<usize>().is_ok() || part.contains(['[', ']']))
239 {
240 return Err(DocumentError::UnsupportedOperation {
241 format: "YAML".to_string(),
242 operation: operation.to_string(),
243 detail: "a missing mapping chain contains a CST-ambiguous key"
244 .to_string(),
245 });
246 }
247 return Ok(());
248 }
249 None => {
250 return Err(DocumentError::PathNotFound {
251 path: crate::document::join_path(&segments[..=index]),
252 });
253 }
254 }
255 }
256 Value::Array(values) => {
257 let array_index =
258 segment
259 .parse::<usize>()
260 .map_err(|_| DocumentError::UnregisteredArray {
261 path: crate::document::join_path(&segments[..index]),
262 })?;
263 current =
264 values
265 .get(array_index)
266 .ok_or_else(|| DocumentError::IndexOutOfBounds {
267 path: crate::document::join_path(&segments[..index]),
268 index: array_index,
269 len: values.len(),
270 })?;
271 }
272 value => {
273 return Err(DocumentError::NotTraversable {
274 path: crate::document::join_path(&segments[..index]),
275 got: value.kind_name().to_string(),
276 });
277 }
278 }
279 }
280 Ok(())
281}
282
283fn to_noyalib_value(value: &Value) -> DocumentResult<YamlValue> {
284 match value {
285 Value::Null => Ok(YamlValue::Null),
286 Value::Bool(value) => Ok(YamlValue::Bool(*value)),
287 Value::Integer(value) => Ok(YamlValue::from(*value)),
288 Value::Unsigned(value) => Ok(YamlValue::from(*value)),
289 Value::Float(value) if value.is_finite() => Ok(YamlValue::from(*value)),
290 Value::Float(_) => Err(DocumentError::UnsupportedOperation {
291 format: "YAML".to_string(),
292 operation: "set".to_string(),
293 detail: "non-finite YAML float is not representable".to_string(),
294 }),
295 Value::Number(text) if value.is_float() => text
301 .parse::<f64>()
302 .ok()
303 .filter(|value| value.is_finite())
304 .map(YamlValue::from)
305 .ok_or_else(|| DocumentError::UnsupportedOperation {
306 format: "YAML".to_string(),
307 operation: "set".to_string(),
308 detail: format!("float literal `{text}` is not representable in YAML"),
309 }),
310 Value::Number(text) => Err(DocumentError::UnsupportedOperation {
311 format: "YAML".to_string(),
312 operation: "set".to_string(),
313 detail: format!("integer literal `{text}` exceeds YAML's 64-bit integer range"),
314 }),
315 Value::String(value) => Ok(YamlValue::String(value.clone())),
316 Value::Array(values) => Ok(YamlValue::Sequence(
317 values
318 .iter()
319 .map(to_noyalib_value)
320 .collect::<DocumentResult<Vec<_>>>()?,
321 )),
322 Value::Object(values) => {
323 let mut mapping = YamlMapping::new();
324 for (key, value) in values {
325 mapping.insert(key.clone(), to_noyalib_value(value)?);
326 }
327 Ok(YamlValue::Mapping(mapping))
328 }
329 }
330}
331
332pub fn save(value: &Value) -> DocumentResult<String> {
333 let mut prefix = "__AFDATA_EXACT_NUMBER_".to_string();
334 while value_contains_text(value, &prefix) {
335 prefix.push('_');
336 }
337 let mut literals = Vec::new();
338 let yaml_val = our_value_to_yaml_value(value, &prefix, &mut literals)?;
339 let mut output = to_string(&yaml_val).map_err(|e| DocumentError::ParseError {
340 format: "YAML".to_string(),
341 detail: e.to_string(),
342 })?;
343 for (sentinel, literal) in literals {
344 output = output.replace(&sentinel, &literal);
345 }
346 Ok(output)
347}
348
349fn value_to_our_value(v: YamlValue, literals: &std::collections::HashMap<String, String>) -> Value {
350 match v {
351 YamlValue::Null => Value::Null,
352 YamlValue::Bool(b) => Value::Bool(b),
353 YamlValue::Number(n) => {
354 if let Some(i) = n.as_i64() {
355 Value::Integer(i)
356 } else if let Some(u) = n.as_u64() {
357 Value::Unsigned(u)
358 } else {
359 Value::Float(n.as_f64())
360 }
361 }
362 YamlValue::String(s) => literals
363 .get(&s)
364 .cloned()
365 .map(Value::Number)
366 .unwrap_or(Value::String(s)),
367 YamlValue::Sequence(seq) => Value::Array(
368 seq.into_iter()
369 .map(|value| value_to_our_value(value, literals))
370 .collect(),
371 ),
372 YamlValue::Mapping(map) => {
373 let mut obj = std::collections::BTreeMap::new();
374 for (key, value) in map {
375 let key = literals.get(&key).cloned().unwrap_or(key);
376 obj.insert(key, value_to_our_value(value, literals));
377 }
378 Value::Object(obj)
379 }
380 YamlValue::Tagged(t) => {
381 let (_, value) = t.into_parts();
383 value_to_our_value(value, literals)
384 }
385 }
386}
387
388fn our_value_to_yaml_value(
389 v: &Value,
390 prefix: &str,
391 literals: &mut Vec<(String, String)>,
392) -> DocumentResult<YamlValue> {
393 match v {
394 Value::Null => Ok(YamlValue::Null),
395 Value::Bool(b) => Ok(YamlValue::Bool(*b)),
396 Value::Integer(i) => Ok(YamlValue::Number((*i).into())),
397 Value::Unsigned(i) => Ok(YamlValue::Number((*i).into())),
398 Value::Float(f) if f.is_finite() => Ok(YamlValue::Number((*f).into())),
399 Value::Float(_) => Err(DocumentError::UnsupportedOperation {
400 format: "YAML".to_string(),
401 operation: "save".to_string(),
402 detail: "non-finite float is not representable in YAML".to_string(),
403 }),
404 Value::Number(text) => {
405 if !is_json_number(text) {
406 return Err(DocumentError::UnsupportedOperation {
407 format: "YAML".to_string(),
408 operation: "save".to_string(),
409 detail: format!("invalid number literal `{text}`"),
410 });
411 }
412 let sentinel = format!("{prefix}{}__", literals.len());
413 literals.push((sentinel.clone(), text.clone()));
414 Ok(YamlValue::String(sentinel))
415 }
416 Value::String(s) => Ok(YamlValue::String(s.clone())),
417 Value::Array(a) => {
418 let seq = a
419 .iter()
420 .map(|value| our_value_to_yaml_value(value, prefix, literals))
421 .collect::<DocumentResult<Vec<_>>>()?;
422 Ok(YamlValue::Sequence(seq))
423 }
424 Value::Object(o) => {
425 let mut mapping = YamlMapping::new();
426 for (k, v) in o {
427 mapping.insert(k.clone(), our_value_to_yaml_value(v, prefix, literals)?);
428 }
429 Ok(YamlValue::Mapping(mapping))
430 }
431 }
432}
433
434fn yaml_fragment(value: &Value, operation: &str) -> DocumentResult<String> {
435 if let Value::Number(text) = value
436 && is_json_number(text)
437 {
438 return Ok(text.clone());
439 }
440 if matches!(value, Value::Array(_) | Value::Object(_)) {
441 return save(value);
442 }
443 to_string(&to_noyalib_value(value)?).map_err(|error| DocumentError::UnsupportedOperation {
444 format: "YAML".to_string(),
445 operation: operation.to_string(),
446 detail: error.to_string(),
447 })
448}
449
450fn protect_exact_numbers(
451 content: &str,
452) -> DocumentResult<(String, std::collections::HashMap<String, String>)> {
453 let document = parse_document(content).map_err(|error| DocumentError::ParseError {
454 format: "YAML".to_string(),
455 detail: error.to_string(),
456 })?;
457 let mut spans = Vec::new();
458 collect_exact_number_spans(document.syntax(), content, 0, &mut spans);
459 let mut prefix = "__AFDATA_EXACT_NUMBER_".to_string();
460 while content.contains(&prefix) {
461 prefix.push('_');
462 }
463 let mut protected = content.to_string();
464 let mut literals = std::collections::HashMap::new();
465 for (index, (start, end)) in spans.into_iter().enumerate().rev() {
466 let literal = content[start..end].to_string();
467 let sentinel = format!("{prefix}{index}__");
468 let quoted =
469 serde_json::to_string(&sentinel).map_err(|error| DocumentError::ParseError {
470 format: "YAML".to_string(),
471 detail: error.to_string(),
472 })?;
473 protected.replace_range(start..end, "ed);
474 literals.insert(sentinel, literal);
475 }
476 Ok((protected, literals))
477}
478
479fn collect_exact_number_spans(
480 node: &GreenNode,
481 source: &str,
482 base: usize,
483 spans: &mut Vec<(usize, usize)>,
484) {
485 let mut offset = base;
486 for child in node.children() {
487 match child {
488 GreenChild::Node(node) => collect_exact_number_spans(node, source, offset, spans),
489 GreenChild::Token {
490 kind: SyntaxKind::PlainScalar,
491 len,
492 } => {
493 let token_end = offset + *len as usize;
494 let text = source[offset..token_end].trim_end_matches([' ', '\t', '\r', '\n']);
495 let end = offset + text.len();
496 if should_preserve_number(text) {
497 spans.push((offset, end));
498 }
499 }
500 GreenChild::Token { .. } => {}
501 }
502 offset += child.text_len();
503 }
504}
505
506fn should_preserve_number(text: &str) -> bool {
507 if !is_json_number(text) {
508 return false;
509 }
510 text.contains(['.', 'e', 'E']) || (text.parse::<i64>().is_err() && text.parse::<u64>().is_err())
511}
512
513fn is_json_number(text: &str) -> bool {
514 serde_json::from_str::<serde_json::Value>(text).is_ok_and(|value| value.is_number())
515}
516
517fn value_contains_text(value: &Value, needle: &str) -> bool {
518 match value {
519 Value::Number(text) | Value::String(text) => text.contains(needle),
520 Value::Array(values) => values
521 .iter()
522 .any(|value| value_contains_text(value, needle)),
523 Value::Object(values) => values
524 .iter()
525 .any(|(key, value)| key.contains(needle) || value_contains_text(value, needle)),
526 _ => false,
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::{load, save, set_preserving};
533 use crate::document::Value;
534
535 #[test]
536 fn preserves_large_integer_and_high_precision_float_literals() {
537 let source = concat!(
538 "huge: 123456789012345678901234567890\n",
539 "precise: 0.1000000000000000055511151231257827\n",
540 );
541 let value = load(source).expect("load");
542 assert_eq!(
543 value.get("huge"),
544 Some(&Value::Number("123456789012345678901234567890".to_string()))
545 );
546 assert_eq!(
547 value.get("precise"),
548 Some(&Value::Number(
549 "0.1000000000000000055511151231257827".to_string()
550 ))
551 );
552
553 let rendered = save(&value).expect("save");
554 assert!(rendered.contains("123456789012345678901234567890"));
555 assert!(rendered.contains("0.1000000000000000055511151231257827"));
556 assert_eq!(load(&rendered).expect("reload"), value);
557 }
558
559 #[test]
560 fn exact_number_set_keeps_the_literal() {
561 let edited = set_preserving(
562 "price: 1.0\n",
563 "price",
564 &Value::Number("12345678901234567890.123456789".to_string()),
565 )
566 .expect("set");
567 assert_eq!(edited, "price: 12345678901234567890.123456789\n");
568 assert_eq!(
569 load(&edited).expect("reload").get("price"),
570 Some(&Value::Number("12345678901234567890.123456789".to_string()))
571 );
572 }
573
574 #[test]
575 fn save_rejects_non_finite_float() {
576 let error = save(&Value::Float(f64::NAN)).expect_err("NaN must fail");
577 assert!(error.to_string().contains("non-finite"));
578 }
579}