1use crate::document::{Addressing, 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> {
35 let segments = crate::document::parse_path(path)?;
36 let yaml_path = cst_path(&segments, "set")?;
37 guard_cst_segments(content, &segments, "set", true)?;
38 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
39 format: "YAML".to_string(),
40 detail: error.to_string(),
41 })?;
42 let mut expected = load(content)?;
45 let exists = crate::document::get_path_ref(&expected, path, Addressing::INDEX_ONLY).is_ok();
46 let canonical = load(&save(value)?)?;
49 crate::document::set_path(&mut expected, path, &canonical, Addressing::INDEX_ONLY)?;
50 if exists && matches!(value, Value::Array(_) | Value::Object(_)) {
51 replace_collection_in_place(&mut document, content, &yaml_path, value)?;
52 } else if exists {
53 let result = if let Value::Number(text) = value {
54 document.set(&yaml_path, text)
55 } else {
56 document.set_value(&yaml_path, &to_noyalib_value(value)?)
57 };
58 result.map_err(|error| DocumentError::UnsupportedOperation {
59 format: "YAML".to_string(),
60 operation: "set".to_string(),
61 detail: error.to_string(),
62 })?;
63 } else {
64 let (last, parents) = segments.split_last().ok_or(DocumentError::EmptyPath)?;
65 if last.parse::<usize>().is_ok() {
66 return Err(DocumentError::UnsupportedOperation {
67 format: "YAML".to_string(),
68 operation: "set".to_string(),
69 detail: "cannot create a new sequence index; the element must already exist"
70 .to_string(),
71 });
72 }
73 let parent_path = if parents.is_empty() {
74 String::new()
75 } else {
76 cst_path(parents, "set")?
77 };
78 let result = match value {
79 Value::Array(items) if items.is_empty() => {
83 document.insert_entry(&parent_path, last, "[]")
84 }
85 Value::Object(entries) if entries.is_empty() => {
86 document.insert_entry(&parent_path, last, "{}")
87 }
88 Value::Array(_) | Value::Object(_) => {
92 document.insert_entry_value(&parent_path, last, &to_noyalib_value(value)?)
93 }
94 _ => document.insert_entry(&parent_path, last, yaml_fragment(value, "set")?.trim_end()),
95 };
96 if result.is_err() {
97 let retry = if let Value::Number(text) = value {
102 document
103 .insert_entry_value(&parent_path, last, &YamlValue::Null)
104 .and_then(|()| document.set(&yaml_path, text))
105 } else {
106 document.insert_entry_value(&parent_path, last, &to_noyalib_value(value)?)
107 };
108 retry.map_err(|error| DocumentError::UnsupportedOperation {
109 format: "YAML".to_string(),
110 operation: "set".to_string(),
111 detail: error.to_string(),
112 })?;
113 }
114 }
115 document
116 .validate()
117 .map_err(|error| DocumentError::ParseError {
118 format: "YAML".to_string(),
119 detail: error.to_string(),
120 })?;
121 let output = document.to_string();
122 if load(&output)? != expected {
123 return Err(DocumentError::UnsupportedOperation {
124 format: "YAML".to_string(),
125 operation: "set".to_string(),
126 detail: "YAML edit did not preserve the requested document value".to_string(),
127 });
128 }
129 Ok(output)
130}
131
132fn replace_collection_in_place(
156 document: &mut noyalib::cst::Document,
157 content: &str,
158 yaml_path: &str,
159 value: &Value,
160) -> DocumentResult<()> {
161 let unsupported = |detail: String| DocumentError::UnsupportedOperation {
162 format: "YAML".to_string(),
163 operation: "set".to_string(),
164 detail,
165 };
166 let (value_start, value_end) = document
167 .span_at(yaml_path)
168 .ok_or_else(|| unsupported(format!("could not locate the value at `{yaml_path}`")))?;
169 let (key_start, key_end) = document
170 .key_span(yaml_path)
171 .ok_or_else(|| unsupported(format!("could not locate the key at `{yaml_path}`")))?;
172 if value_start < key_end {
173 return Err(unsupported(format!(
174 "`{yaml_path}` resolves through a YAML alias; editing it would rewrite the anchor \
175 instead. Materialize the alias first."
176 )));
177 }
178
179 let bytes = content.as_bytes();
180 let key_column = line_column_of(content, key_start);
181 let indent = key_column + document.indent_unit();
182 let newline = if content.contains("\r\n") {
183 "\r\n"
184 } else {
185 "\n"
186 };
187
188 let mut start = value_start;
189 while start > 0 && matches!(bytes[start - 1], b' ' | b'\t' | b'\n' | b'\r') {
190 start -= 1;
191 }
192 let end = end_of_trailing_inline_comment(content, value_end);
193
194 let fragment = block_fragment(value, document.indent_unit(), indent, newline)?;
195 document
196 .replace_span(start, end, &fragment)
197 .map_err(|error| unsupported(error.to_string()))
198}
199
200fn block_fragment(
206 value: &Value,
207 indent_unit: usize,
208 indent: usize,
209 newline: &str,
210) -> DocumentResult<String> {
211 match value {
212 Value::Array(items) if items.is_empty() => return Ok(" []".to_string()),
213 Value::Object(entries) if entries.is_empty() => return Ok(" {}".to_string()),
214 _ => {}
215 }
216 let config = noyalib::SerializerConfig::new()
217 .indent(indent_unit)
218 .flow_style(noyalib::FlowStyle::Block);
219 let rendered = noyalib::to_string_value_with_config(&to_noyalib_value(value)?, &config)
223 .map_err(|error| DocumentError::UnsupportedOperation {
224 format: "YAML".to_string(),
225 operation: "set".to_string(),
226 detail: error.to_string(),
227 })?;
228 let pad = " ".repeat(indent);
229 let mut fragment = String::new();
230 for line in rendered.trim_end_matches(['\r', '\n']).lines() {
231 fragment.push_str(newline);
232 if line.is_empty() {
233 continue;
234 }
235 fragment.push_str(&pad);
236 fragment.push_str(line);
237 }
238 Ok(fragment)
239}
240
241fn line_column_of(content: &str, offset: usize) -> usize {
243 let line_start = content[..offset].rfind('\n').map_or(0, |index| index + 1);
244 offset - line_start
245}
246
247fn end_of_trailing_inline_comment(content: &str, value_end: usize) -> usize {
250 let bytes = content.as_bytes();
251 let mut index = value_end;
252 while index < bytes.len() && matches!(bytes[index], b' ' | b'\t') {
253 index += 1;
254 }
255 if index >= bytes.len() || bytes[index] != b'#' {
256 return value_end;
257 }
258 while index < bytes.len() && bytes[index] != b'\n' {
259 index += 1;
260 }
261 if index > 0 && bytes[index - 1] == b'\r' {
262 index -= 1;
263 }
264 index
265}
266
267pub fn unset_preserving(content: &str, path: &str) -> DocumentResult<String> {
269 let segments = crate::document::parse_path(path)?;
270 let yaml_path = cst_path(&segments, "unset")?;
271 guard_cst_segments(content, &segments, "unset", false)?;
272 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
273 format: "YAML".to_string(),
274 detail: error.to_string(),
275 })?;
276 document
277 .remove(&yaml_path)
278 .map_err(|error| DocumentError::UnsupportedOperation {
279 format: "YAML".to_string(),
280 operation: "unset".to_string(),
281 detail: error.to_string(),
282 })?;
283 document
284 .validate()
285 .map_err(|error| DocumentError::ParseError {
286 format: "YAML".to_string(),
287 detail: error.to_string(),
288 })?;
289 Ok(document.to_string())
290}
291
292pub fn append_array_item_preserving(
295 content: &str,
296 path: &str,
297 item: &Value,
298) -> DocumentResult<String> {
299 let segments = array_path_segments(path)?;
300 let yaml_path = cst_path(&segments, "add")?;
301 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
302 format: "YAML".to_string(),
303 detail: error.to_string(),
304 })?;
305 let loaded = load(content)?;
306 let existing = array_at_path(&loaded, path, "add")?;
307 let previous_len = existing.len();
308 if previous_len == 0 {
309 return set_preserving(content, path, &Value::Array(vec![item.clone()]));
314 }
315 let mut fragment = yaml_fragment(item, "add")?;
316 {
317 let item_path = format!("{yaml_path}[{}]", previous_len - 1);
318 let (item_start, _) =
319 document
320 .span_at(&item_path)
321 .ok_or_else(|| DocumentError::UnsupportedOperation {
322 format: "YAML".to_string(),
323 operation: "add".to_string(),
324 detail: "could not resolve the existing sequence-item indentation".to_string(),
325 })?;
326 let dash_column = preceding_sequence_dash_column(content, item_start).ok_or_else(|| {
327 DocumentError::UnsupportedOperation {
328 format: "YAML".to_string(),
329 operation: "add".to_string(),
330 detail: "only block sequences can be extended without rebuilding the document"
331 .to_string(),
332 }
333 })?;
334 fragment = indent_continuation_lines(fragment.trim_end(), dash_column + 2);
335 }
336 document
337 .push_back(&yaml_path, fragment.trim_end())
338 .map_err(|error| DocumentError::UnsupportedOperation {
339 format: "YAML".to_string(),
340 operation: "add".to_string(),
341 detail: error.to_string(),
342 })?;
343 document
344 .validate()
345 .map_err(|error| DocumentError::ParseError {
346 format: "YAML".to_string(),
347 detail: error.to_string(),
348 })?;
349 let output = document.to_string();
350 let edited = load(&output)?;
351 let edited_items = array_at_path(&edited, path, "add")?;
352 if edited_items.len() != previous_len + 1 || edited_items.last() != Some(item) {
353 return Err(DocumentError::UnsupportedOperation {
354 format: "YAML".to_string(),
355 operation: "add".to_string(),
356 detail: "the source edit did not reproduce the requested keyed item".to_string(),
357 });
358 }
359 Ok(output)
360}
361
362pub fn remove_array_item_preserving(
364 content: &str,
365 path: &str,
366 index: usize,
367) -> DocumentResult<String> {
368 let segments = array_path_segments(path)?;
369 let loaded = load(content)?;
370 if array_at_path(&loaded, path, "remove")?.len() == 1 && index == 0 {
371 return set_preserving(content, path, &Value::Array(Vec::new()));
377 }
378 let mut document = parse_document(content).map_err(|error| DocumentError::ParseError {
379 format: "YAML".to_string(),
380 detail: error.to_string(),
381 })?;
382 let yaml_path = cst_path(&segments, "remove")?;
383 document
384 .remove(&format!("{yaml_path}[{index}]"))
385 .map_err(|error| DocumentError::UnsupportedOperation {
386 format: "YAML".to_string(),
387 operation: "remove".to_string(),
388 detail: error.to_string(),
389 })?;
390 document
391 .validate()
392 .map_err(|error| DocumentError::ParseError {
393 format: "YAML".to_string(),
394 detail: error.to_string(),
395 })?;
396 Ok(document.to_string())
397}
398
399fn array_path_segments(path: &str) -> DocumentResult<Vec<String>> {
402 if path.is_empty() {
403 Ok(Vec::new())
404 } else {
405 crate::document::parse_path(path)
406 }
407}
408
409fn array_at_path<'a>(
410 value: &'a Value,
411 path: &str,
412 operation: &str,
413) -> DocumentResult<&'a Vec<Value>> {
414 let target = if path.is_empty() {
415 value
416 } else {
417 crate::document::get_path_ref(value, path, Addressing::INDEX_ONLY)?
418 };
419 target
420 .as_array()
421 .ok_or_else(|| DocumentError::UnsupportedOperation {
422 format: "YAML".to_string(),
423 operation: operation.to_string(),
424 detail: "target is not an array".to_string(),
425 })
426}
427
428fn preceding_sequence_dash_column(content: &str, value_start: usize) -> Option<usize> {
429 let bytes = content.as_bytes();
430 let mut position = value_start;
431 let dash = loop {
432 position = position.checked_sub(1)?;
433 match bytes.get(position)? {
434 b' ' | b'\t' => {}
435 b'-' => break position,
436 _ => return None,
437 }
438 };
439 let line_start = content.as_bytes()[..dash]
440 .iter()
441 .rposition(|byte| *byte == b'\n')
442 .map_or(0, |newline| newline + 1);
443 Some(dash - line_start)
444}
445
446fn indent_continuation_lines(fragment: &str, indent: usize) -> String {
447 if !fragment.contains('\n') {
448 return fragment.to_string();
449 }
450 let padding = " ".repeat(indent);
451 let mut output = String::with_capacity(fragment.len() + indent * 4);
452 for (index, line) in fragment.split('\n').enumerate() {
453 if index > 0 {
454 output.push('\n');
455 if !line.is_empty() {
456 output.push_str(&padding);
457 }
458 }
459 output.push_str(line);
460 }
461 output
462}
463
464fn cst_path(segments: &[String], operation: &str) -> DocumentResult<String> {
465 let mut path = String::new();
466 for segment in segments {
467 if segment.contains(['.', '\\', '[', ']']) {
468 return Err(DocumentError::UnsupportedOperation {
469 format: "YAML".to_string(),
470 operation: operation.to_string(),
471 detail:
472 "escaped or bracketed YAML keys require a quoted-key CST span and are not supported"
473 .to_string(),
474 });
475 }
476 if let Ok(index) = segment.parse::<usize>() {
477 path.push_str(&format!("[{index}]"));
478 } else {
479 if !path.is_empty() {
480 path.push('.');
481 }
482 path.push_str(segment);
483 }
484 }
485 Ok(path)
486}
487
488fn guard_cst_segments(
489 content: &str,
490 segments: &[String],
491 operation: &str,
492 allow_missing: bool,
493) -> DocumentResult<()> {
494 let root = load(content)?;
495 let mut current = &root;
496 for (index, segment) in segments.iter().enumerate() {
497 match current {
498 Value::Object(object) => {
499 if segment.parse::<usize>().is_ok() || segment.contains(['[', ']']) {
500 return Err(DocumentError::UnsupportedOperation {
501 format: "YAML".to_string(),
502 operation: operation.to_string(),
503 detail: format!(
504 "mapping key `{segment}` is ambiguous in the CST path grammar"
505 ),
506 });
507 }
508 match object.get(segment) {
509 Some(next) => current = next,
510 None if allow_missing => {
511 if segments[index..]
512 .iter()
513 .any(|part| part.parse::<usize>().is_ok() || part.contains(['[', ']']))
514 {
515 return Err(DocumentError::UnsupportedOperation {
516 format: "YAML".to_string(),
517 operation: operation.to_string(),
518 detail: "a missing mapping chain contains a CST-ambiguous key"
519 .to_string(),
520 });
521 }
522 return Ok(());
523 }
524 None => {
525 return Err(DocumentError::PathNotFound {
526 path: crate::document::join_path(&segments[..=index]),
527 });
528 }
529 }
530 }
531 Value::Array(values) => {
532 let array_index =
533 segment
534 .parse::<usize>()
535 .map_err(|_| DocumentError::UnregisteredArray {
536 path: crate::document::join_path(&segments[..index]),
537 })?;
538 current =
539 values
540 .get(array_index)
541 .ok_or_else(|| DocumentError::IndexOutOfBounds {
542 path: crate::document::join_path(&segments[..index]),
543 index: array_index,
544 len: values.len(),
545 })?;
546 }
547 value => {
548 return Err(DocumentError::NotTraversable {
549 path: crate::document::join_path(&segments[..index]),
550 got: value.kind_name().to_string(),
551 });
552 }
553 }
554 }
555 Ok(())
556}
557
558fn to_noyalib_value(value: &Value) -> DocumentResult<YamlValue> {
559 match value {
560 Value::Null => Ok(YamlValue::Null),
561 Value::Bool(value) => Ok(YamlValue::Bool(*value)),
562 Value::Integer(value) => Ok(YamlValue::from(*value)),
563 Value::Unsigned(value) => Ok(YamlValue::from(*value)),
564 Value::Float(value) if value.is_finite() => Ok(YamlValue::from(*value)),
565 Value::Float(_) => Err(DocumentError::UnsupportedOperation {
566 format: "YAML".to_string(),
567 operation: "set".to_string(),
568 detail: "non-finite YAML float is not representable".to_string(),
569 }),
570 Value::Number(text) if value.is_float() => text
576 .parse::<f64>()
577 .ok()
578 .filter(|value| value.is_finite())
579 .map(YamlValue::from)
580 .ok_or_else(|| DocumentError::UnsupportedOperation {
581 format: "YAML".to_string(),
582 operation: "set".to_string(),
583 detail: format!("float literal `{text}` is not representable in YAML"),
584 }),
585 Value::Number(text) => Err(DocumentError::UnsupportedOperation {
586 format: "YAML".to_string(),
587 operation: "set".to_string(),
588 detail: format!("integer literal `{text}` exceeds YAML's 64-bit integer range"),
589 }),
590 Value::String(value) => Ok(YamlValue::String(value.clone())),
591 Value::Array(values) => Ok(YamlValue::Sequence(
592 values
593 .iter()
594 .map(to_noyalib_value)
595 .collect::<DocumentResult<Vec<_>>>()?,
596 )),
597 Value::Object(values) => {
598 let mut mapping = YamlMapping::new();
599 for (key, value) in values {
600 mapping.insert(key.clone(), to_noyalib_value(value)?);
601 }
602 Ok(YamlValue::Mapping(mapping))
603 }
604 }
605}
606
607pub fn save(value: &Value) -> DocumentResult<String> {
608 let mut prefix = "__AFDATA_EXACT_NUMBER_".to_string();
609 while value_contains_text(value, &prefix) {
610 prefix.push('_');
611 }
612 let mut literals = Vec::new();
613 let yaml_val = our_value_to_yaml_value(value, &prefix, &mut literals)?;
614 let mut output = to_string(&yaml_val).map_err(|e| DocumentError::ParseError {
615 format: "YAML".to_string(),
616 detail: e.to_string(),
617 })?;
618 for (sentinel, literal) in literals {
619 output = output.replace(&sentinel, &literal);
620 }
621 Ok(output)
622}
623
624fn value_to_our_value(v: YamlValue, literals: &std::collections::HashMap<String, String>) -> Value {
625 match v {
626 YamlValue::Null => Value::Null,
627 YamlValue::Bool(b) => Value::Bool(b),
628 YamlValue::Number(n) => {
629 if let Some(i) = n.as_i64() {
630 Value::Integer(i)
631 } else if let Some(u) = n.as_u64() {
632 Value::Unsigned(u)
633 } else {
634 Value::Float(n.as_f64())
635 }
636 }
637 YamlValue::String(s) => literals
638 .get(&s)
639 .cloned()
640 .map(Value::Number)
641 .unwrap_or(Value::String(s)),
642 YamlValue::Sequence(seq) => Value::Array(
643 seq.into_iter()
644 .map(|value| value_to_our_value(value, literals))
645 .collect(),
646 ),
647 YamlValue::Mapping(map) => {
648 let mut obj = std::collections::BTreeMap::new();
649 for (key, value) in map {
650 let key = literals.get(&key).cloned().unwrap_or(key);
651 obj.insert(key, value_to_our_value(value, literals));
652 }
653 Value::Object(obj)
654 }
655 YamlValue::Tagged(t) => {
656 let (_, value) = t.into_parts();
658 value_to_our_value(value, literals)
659 }
660 }
661}
662
663fn our_value_to_yaml_value(
664 v: &Value,
665 prefix: &str,
666 literals: &mut Vec<(String, String)>,
667) -> DocumentResult<YamlValue> {
668 match v {
669 Value::Null => Ok(YamlValue::Null),
670 Value::Bool(b) => Ok(YamlValue::Bool(*b)),
671 Value::Integer(i) => Ok(YamlValue::Number((*i).into())),
672 Value::Unsigned(i) => Ok(YamlValue::Number((*i).into())),
673 Value::Float(f) if f.is_finite() => Ok(YamlValue::Number((*f).into())),
674 Value::Float(_) => Err(DocumentError::UnsupportedOperation {
675 format: "YAML".to_string(),
676 operation: "save".to_string(),
677 detail: "non-finite float is not representable in YAML".to_string(),
678 }),
679 Value::Number(text) => {
680 if !is_json_number(text) {
681 return Err(DocumentError::UnsupportedOperation {
682 format: "YAML".to_string(),
683 operation: "save".to_string(),
684 detail: format!("invalid number literal `{text}`"),
685 });
686 }
687 let sentinel = format!("{prefix}{}__", literals.len());
688 literals.push((sentinel.clone(), text.clone()));
689 Ok(YamlValue::String(sentinel))
690 }
691 Value::String(s) => Ok(YamlValue::String(s.clone())),
692 Value::Array(a) => {
693 let seq = a
694 .iter()
695 .map(|value| our_value_to_yaml_value(value, prefix, literals))
696 .collect::<DocumentResult<Vec<_>>>()?;
697 Ok(YamlValue::Sequence(seq))
698 }
699 Value::Object(o) => {
700 let mut mapping = YamlMapping::new();
701 for (k, v) in o {
702 mapping.insert(k.clone(), our_value_to_yaml_value(v, prefix, literals)?);
703 }
704 Ok(YamlValue::Mapping(mapping))
705 }
706 }
707}
708
709fn yaml_fragment(value: &Value, operation: &str) -> DocumentResult<String> {
710 if let Value::Number(text) = value
711 && is_json_number(text)
712 {
713 return Ok(text.clone());
714 }
715 if matches!(value, Value::Array(_) | Value::Object(_)) {
716 return save(value);
717 }
718 to_string(&to_noyalib_value(value)?).map_err(|error| DocumentError::UnsupportedOperation {
719 format: "YAML".to_string(),
720 operation: operation.to_string(),
721 detail: error.to_string(),
722 })
723}
724
725fn protect_exact_numbers(
726 content: &str,
727) -> DocumentResult<(String, std::collections::HashMap<String, String>)> {
728 let document = parse_document(content).map_err(|error| DocumentError::ParseError {
729 format: "YAML".to_string(),
730 detail: error.to_string(),
731 })?;
732 let mut spans = Vec::new();
733 collect_exact_number_spans(document.syntax(), content, 0, &mut spans);
734 let mut prefix = "__AFDATA_EXACT_NUMBER_".to_string();
735 while content.contains(&prefix) {
736 prefix.push('_');
737 }
738 let mut protected = content.to_string();
739 let mut literals = std::collections::HashMap::new();
740 for (index, (start, end)) in spans.into_iter().enumerate().rev() {
741 let literal = content[start..end].to_string();
742 let sentinel = format!("{prefix}{index}__");
743 let quoted =
744 serde_json::to_string(&sentinel).map_err(|error| DocumentError::ParseError {
745 format: "YAML".to_string(),
746 detail: error.to_string(),
747 })?;
748 protected.replace_range(start..end, "ed);
749 literals.insert(sentinel, literal);
750 }
751 Ok((protected, literals))
752}
753
754fn collect_exact_number_spans(
755 node: &GreenNode,
756 source: &str,
757 base: usize,
758 spans: &mut Vec<(usize, usize)>,
759) {
760 let mut offset = base;
761 for child in node.children() {
762 match child {
763 GreenChild::Node(node) => collect_exact_number_spans(node, source, offset, spans),
764 GreenChild::Token {
765 kind: SyntaxKind::PlainScalar,
766 len,
767 } => {
768 let token_end = offset + *len as usize;
769 let text = source[offset..token_end].trim_end_matches([' ', '\t', '\r', '\n']);
770 let end = offset + text.len();
771 if should_preserve_number(text) {
772 spans.push((offset, end));
773 }
774 }
775 GreenChild::Token { .. } => {}
776 }
777 offset += child.text_len();
778 }
779}
780
781fn should_preserve_number(text: &str) -> bool {
782 if !is_json_number(text) {
783 return false;
784 }
785 text.contains(['.', 'e', 'E']) || (text.parse::<i64>().is_err() && text.parse::<u64>().is_err())
786}
787
788fn is_json_number(text: &str) -> bool {
789 serde_json::from_str::<serde_json::Value>(text).is_ok_and(|value| value.is_number())
790}
791
792fn value_contains_text(value: &Value, needle: &str) -> bool {
793 match value {
794 Value::Number(text) | Value::String(text) => text.contains(needle),
795 Value::Array(values) => values
796 .iter()
797 .any(|value| value_contains_text(value, needle)),
798 Value::Object(values) => values
799 .iter()
800 .any(|(key, value)| key.contains(needle) || value_contains_text(value, needle)),
801 _ => false,
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::{load, save, set_preserving};
808 use crate::document::Value;
809
810 #[test]
811 fn new_fields_in_flow_mappings_preserve_their_exact_value_and_source() {
812 use crate::document::{Document, Format};
813 for (source, path) in [
814 ("{}\n", "field"),
815 ("{existing: before}\n", "field"),
816 ("outer: {}\n", "outer.field"),
817 ("# keep\r\n{}\r\n", "field"),
818 ] {
819 for value in [
820 Value::String("new".into()),
821 Value::Bool(true),
822 Value::Null,
823 Value::Integer(7),
824 Value::Unsigned(7),
825 Value::Float(2.0),
826 Value::Array(Vec::new()),
827 Value::Object(Default::default()),
828 Value::Number("123456789012345678901234567890".into()),
829 Value::Number("0.10000000000000000000001".into()),
830 ] {
831 let mut document = Document::parse(source, Format::Yaml).unwrap();
832 document
833 .set(path, value.clone())
834 .expect("insert into a flow mapping");
835 assert_eq!(document.value_at(path).unwrap(), value);
836 assert_eq!(
837 load(document.source()).unwrap(),
838 load(&save(document.value()).unwrap()).unwrap()
839 );
840 if source.starts_with("# keep") {
841 assert!(document.source().starts_with("# keep\r\n"));
842 assert!(document.source().ends_with("\r\n"));
843 }
844 }
845 }
846 }
847
848 #[test]
849 fn a_refused_key_cannot_fall_back_to_inserting_multiple_entries() {
850 use crate::document::{Document, Format};
851 let source = "attachments:\n";
852 let mut document = Document::parse(source, Format::Yaml).unwrap();
853 let original = document.value().clone();
854 assert!(
855 document
856 .set("one:\ntwo", Value::String("injected".into()))
857 .is_err()
858 );
859 assert_eq!(document.source(), source);
860 assert_eq!(document.value(), &original);
861 }
862
863 #[test]
864 fn preserves_large_integer_and_high_precision_float_literals() {
865 let source = concat!(
866 "huge: 123456789012345678901234567890\n",
867 "precise: 0.1000000000000000055511151231257827\n",
868 );
869 let value = load(source).expect("load");
870 assert_eq!(
871 value.get("huge"),
872 Some(&Value::Number("123456789012345678901234567890".to_string()))
873 );
874 assert_eq!(
875 value.get("precise"),
876 Some(&Value::Number(
877 "0.1000000000000000055511151231257827".to_string()
878 ))
879 );
880
881 let rendered = save(&value).expect("save");
882 assert!(rendered.contains("123456789012345678901234567890"));
883 assert!(rendered.contains("0.1000000000000000055511151231257827"));
884 assert_eq!(load(&rendered).expect("reload"), value);
885 }
886
887 #[test]
888 fn exact_number_set_keeps_the_literal() {
889 let edited = set_preserving(
890 "price: 1.0\n",
891 "price",
892 &Value::Number("12345678901234567890.123456789".to_string()),
893 )
894 .expect("set");
895 assert_eq!(edited, "price: 12345678901234567890.123456789\n");
896 assert_eq!(
897 load(&edited).expect("reload").get("price"),
898 Some(&Value::Number("12345678901234567890.123456789".to_string()))
899 );
900 }
901
902 fn contact_source() -> &'static str {
905 "# a leading comment\ndisplay_name: Alice Example\nkind: contact\nemails:\n - alice@example.com # work\ntags: []\nrole: ''\n"
906 }
907
908 #[test]
909 fn replacing_a_collection_keeps_every_other_byte() {
910 let edited = set_preserving(
911 contact_source(),
912 "tags",
913 &Value::Array(vec![Value::String("vip".into())]),
914 )
915 .expect("an empty sequence can grow");
916 assert_eq!(
917 edited,
918 "# a leading comment\ndisplay_name: Alice Example\nkind: contact\nemails:\n - alice@example.com # work\ntags:\n - vip\nrole: ''\n"
919 );
920
921 let cleared = set_preserving(&edited, "tags", &Value::Array(Vec::new()))
924 .expect("a sequence can shrink to empty");
925 assert_eq!(cleared, contact_source());
926 }
927
928 #[test]
929 fn replacing_a_collection_quotes_elements_that_need_it() {
930 let edited = set_preserving(
931 contact_source(),
932 "tags",
933 &Value::Array(vec![
934 Value::String("true".into()),
935 Value::String("123".into()),
936 Value::String("a: b".into()),
937 Value::String("#c".into()),
938 Value::String(String::new()),
939 ]),
940 )
941 .expect("tricky scalars are the emitter's problem, not ours");
942 let loaded = load(&edited).expect("the result parses");
944 assert_eq!(
945 crate::document::get_path(&loaded, "tags", crate::document::Addressing::INDEX_ONLY)
946 .expect("tags"),
947 Value::Array(vec![
948 Value::String("true".into()),
949 Value::String("123".into()),
950 Value::String("a: b".into()),
951 Value::String("#c".into()),
952 Value::String(String::new()),
953 ])
954 );
955 }
956
957 #[test]
958 fn replacing_a_collection_takes_the_comment_that_described_it() {
959 let edited = set_preserving(
962 "emails:\n - alice@example.com # work\nrole: ''\n",
963 "emails",
964 &Value::Array(vec![Value::String("bob@example.com".into())]),
965 )
966 .expect("replace");
967 assert_eq!(edited, "emails:\n - bob@example.com\nrole: ''\n");
968 assert!(!edited.contains("# work"), "{edited}");
969 }
970
971 #[test]
972 fn a_collection_reached_through_an_alias_is_refused() {
973 let source = "base: &b\n - x\nother: *b\n";
976 let error = set_preserving(
977 source,
978 "other",
979 &Value::Array(vec![Value::String("y".into())]),
980 )
981 .expect_err("an alias target must not be edited through");
982 assert!(error.to_string().contains("alias"), "{error}");
983 }
984
985 #[test]
986 fn a_new_key_can_be_created_with_a_collection_value() {
987 let edited = set_preserving(
988 "kind: contact\n",
989 "tags",
990 &Value::Array(vec![Value::String("vip".into())]),
991 )
992 .expect("a missing key is inserted");
993 assert_eq!(edited, "kind: contact\ntags:\n - vip\n");
994
995 let empty = set_preserving("kind: contact\n", "tags", &Value::Array(Vec::new()))
996 .expect("a missing key can be inserted empty");
997 assert_eq!(empty, "kind: contact\ntags: []\n");
998 }
999
1000 #[test]
1001 fn replacing_a_collection_keeps_the_source_newline_style() {
1002 let edited = set_preserving(
1003 "kind: contact\r\ntags: []\r\nrole: ''\r\n",
1004 "tags",
1005 &Value::Array(vec![Value::String("vip".into())]),
1006 )
1007 .expect("CRLF source");
1008 assert_eq!(edited, "kind: contact\r\ntags:\r\n - vip\r\nrole: ''\r\n");
1009 assert!(
1011 !edited.replace("\r\n", "").contains('\n'),
1012 "mixed endings: {edited:?}"
1013 );
1014 }
1015
1016 #[test]
1017 fn a_new_key_lands_after_a_bare_null_last_entry() {
1018 let source = "kind: draft\nattachments:\n";
1021 let edited = set_preserving(source, "sync_intent", &Value::String("send".into()))
1022 .expect("a new key can follow a bare-null entry");
1023 assert_eq!(edited, "kind: draft\nattachments:\nsync_intent: send\n");
1024 }
1025
1026 #[test]
1027 fn save_rejects_non_finite_float() {
1028 let error = save(&Value::Float(f64::NAN)).expect_err("NaN must fail");
1029 assert!(error.to_string().contains("non-finite"));
1030 }
1031}