Skip to main content

yaml_rt_core/
edit.rs

1use std::fmt;
2
3use crate::fragment::indent_text;
4use crate::pointer::parse_sequence_index;
5use crate::{
6    CollectionStyle, FragmentError, JsonPointer, NodeId, PointerError, ResolvedScalar,
7    SemanticKind, SemanticValueError, Span, YamlDoc, YamlError, YamlFragment, YamlScalarStyle,
8    resolve_scalar, semantically_equal,
9};
10
11/// Failure while applying a pointer-addressed YAML edit.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct YamlEditError {
14    message: String,
15}
16
17impl YamlEditError {
18    fn new(message: impl Into<String>) -> Self {
19        Self {
20            message: message.into(),
21        }
22    }
23}
24
25impl fmt::Display for YamlEditError {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        formatter.write_str(&self.message)
28    }
29}
30
31impl std::error::Error for YamlEditError {}
32
33impl From<PointerError> for YamlEditError {
34    fn from(error: PointerError) -> Self {
35        Self::new(error.to_string())
36    }
37}
38
39impl From<FragmentError> for YamlEditError {
40    fn from(error: FragmentError) -> Self {
41        Self::new(error.to_string())
42    }
43}
44
45impl From<YamlError> for YamlEditError {
46    fn from(error: YamlError) -> Self {
47        Self::new(error.to_string())
48    }
49}
50
51impl From<SemanticValueError> for YamlEditError {
52    fn from(error: SemanticValueError) -> Self {
53        Self::new(error.to_string())
54    }
55}
56
57enum AddLocation {
58    Root(NodeId),
59    Mapping {
60        mapping: NodeId,
61        existing: Option<NodeId>,
62        key: String,
63    },
64    Sequence {
65        sequence: NodeId,
66        index: usize,
67    },
68}
69
70impl YamlDoc {
71    /// Applies RFC 6902 `add` semantics at a JSON Pointer destination.
72    pub fn add_at(
73        &mut self,
74        document: usize,
75        pointer: &JsonPointer,
76        value: &YamlFragment,
77    ) -> Result<(), YamlEditError> {
78        self.transaction(|work| {
79            let location = work.resolve_add_location(document, pointer)?;
80            work.queue_add(location, value)
81        })
82    }
83
84    /// Removes an existing value. Removing a document root is unsupported.
85    pub fn remove_at(
86        &mut self,
87        document: usize,
88        pointer: &JsonPointer,
89    ) -> Result<(), YamlEditError> {
90        if pointer.is_root() {
91            return Err(YamlEditError::new(
92                "removing a YAML document root is not supported",
93            ));
94        }
95        self.transaction(|work| {
96            let target = work.resolve_pointer(document, pointer)?;
97            work.queue_value_removal(target)
98        })
99    }
100
101    /// Replaces an existing value while preserving its surrounding syntax.
102    pub fn replace_at(
103        &mut self,
104        document: usize,
105        pointer: &JsonPointer,
106        value: &YamlFragment,
107    ) -> Result<(), YamlEditError> {
108        self.transaction(|work| {
109            let target = work.resolve_pointer(document, pointer)?;
110            work.queue_fragment_replacement(target, value)
111        })
112    }
113
114    /// Moves a value using RFC 6902 remove-then-add semantics.
115    pub fn move_at(
116        &mut self,
117        document: usize,
118        from: &JsonPointer,
119        path: &JsonPointer,
120    ) -> Result<(), YamlEditError> {
121        if from == path {
122            return Ok(());
123        }
124        if from.is_proper_prefix_of(path) {
125            return Err(YamlEditError::new(
126                "move source must not be a proper prefix of its destination",
127            ));
128        }
129        let mut work = self.clone();
130        let source = work.resolve_pointer(document, from)?;
131        if work.anchor_has_external_alias(source) {
132            return Err(YamlEditError::new(
133                "cannot move anchored subtree because an alias outside it depends on that anchor",
134            ));
135        }
136        let fragment = YamlFragment::from_document_node(&work, source)?;
137        if from.is_root() {
138            return Err(YamlEditError::new(
139                "a document root cannot be moved into another location",
140            ));
141        }
142        work.queue_value_removal(source)?;
143        work.commit_edits()?;
144        let location = work.resolve_add_location(document, path)?;
145        work.queue_add(location, &fragment)?;
146        work.commit_edits()?;
147        *self = work;
148        Ok(())
149    }
150
151    /// Deep-copies a value using RFC 6902 `copy` semantics.
152    pub fn copy_at(
153        &mut self,
154        document: usize,
155        from: &JsonPointer,
156        path: &JsonPointer,
157    ) -> Result<(), YamlEditError> {
158        let source = self.resolve_pointer(document, from)?;
159        let fragment = YamlFragment::from_document_node(self, source)?;
160        if fragment.contains_anchor() {
161            return Err(YamlEditError::new(format!(
162                "cannot copy {:?}: subtree contains an anchor",
163                from.as_str()
164            )));
165        }
166        self.add_at(document, path, &fragment)
167    }
168
169    /// Compares a pointer-selected target with a YAML value.
170    pub fn test_at(
171        &self,
172        document: usize,
173        pointer: &JsonPointer,
174        value: &YamlFragment,
175    ) -> Result<bool, YamlEditError> {
176        let target = self.resolve_pointer(document, pointer)?;
177        semantically_equal(self, target, value.document(), value.root()).map_err(Into::into)
178    }
179
180    fn transaction(
181        &mut self,
182        operation: impl FnOnce(&mut YamlDoc) -> Result<(), YamlEditError>,
183    ) -> Result<(), YamlEditError> {
184        let mut work = self.clone();
185        operation(&mut work)?;
186        work.commit_edits()?;
187        *self = work;
188        Ok(())
189    }
190
191    fn resolve_add_location(
192        &self,
193        document: usize,
194        pointer: &JsonPointer,
195    ) -> Result<AddLocation, YamlEditError> {
196        let Some((parent_pointer, token)) = pointer.parent() else {
197            let root = self
198                .document_root(document)?
199                .ok_or_else(|| YamlEditError::new("selected document has no root node"))?;
200            return Ok(AddLocation::Root(root));
201        };
202        let mut parent = self.resolve_pointer(document, &parent_pointer)?;
203        parent = self.resolve_aliases_for_pointer(
204            parent,
205            pointer,
206            pointer.tokens().len().saturating_sub(1),
207        )?;
208        match self.semantic_kind(parent) {
209            Some(SemanticKind::Mapping { .. }) => {
210                let existing = self
211                    .mapping_match(
212                        parent,
213                        token,
214                        pointer,
215                        pointer.tokens().len().saturating_sub(1),
216                    )?
217                    .map(|entry| entry.value);
218                Ok(AddLocation::Mapping {
219                    mapping: parent,
220                    existing,
221                    key: token.as_str().to_owned(),
222                })
223            }
224            Some(SemanticKind::Sequence { .. }) => {
225                let length = self.sequence_items(parent).count();
226                let parsed = parse_sequence_index(
227                    token,
228                    pointer,
229                    pointer.tokens().len().saturating_sub(1),
230                    true,
231                )?;
232                let index = if parsed == usize::MAX { length } else { parsed };
233                if index > length {
234                    return Err(YamlEditError::new(format!(
235                        "sequence index {index} is out of bounds for insertion into length {length}"
236                    )));
237                }
238                Ok(AddLocation::Sequence {
239                    sequence: parent,
240                    index,
241                })
242            }
243            _ => Err(YamlEditError::new(format!(
244                "add parent {:?} is not a mapping or sequence",
245                parent_pointer.as_str()
246            ))),
247        }
248    }
249
250    fn queue_add(
251        &mut self,
252        location: AddLocation,
253        value: &YamlFragment,
254    ) -> Result<(), YamlEditError> {
255        match location {
256            AddLocation::Root(root) => self.queue_fragment_replacement(root, value),
257            AddLocation::Mapping {
258                existing: Some(existing),
259                ..
260            } => self.queue_fragment_replacement(existing, value),
261            AddLocation::Mapping {
262                mapping,
263                existing: None,
264                key,
265            } => self.queue_mapping_insert(mapping, &key, value),
266            AddLocation::Sequence { sequence, index } => {
267                self.queue_sequence_insert(sequence, index, value)
268            }
269        }
270    }
271
272    fn queue_fragment_replacement(
273        &mut self,
274        target: NodeId,
275        value: &YamlFragment,
276    ) -> Result<(), YamlEditError> {
277        if self
278            .node(target)
279            .is_some_and(|node| node.kind() == crate::NodeKind::Scalar)
280            && matches!(
281                value.document().semantic_kind(value.root()),
282                Some(SemanticKind::Scalar { .. })
283            )
284            && value.document().raw_tag(value.root()).is_none()
285            && value.document().anchor(value.root()).is_none()
286        {
287            let mut replacement = value.to_yaml()?;
288            if !replacement.contains(['\n', '\r']) {
289                let (span, target_style) = self.scalar_replacement_target(target)?;
290                if scalar_is_string(self, target)?
291                    && scalar_is_string(value.document(), value.root())?
292                {
293                    let decoded = value.document().scalar_value(value.root())?;
294                    if let Ok(styled) = crate::format_scalar_value(&decoded, target_style) {
295                        replacement = styled;
296                    }
297                }
298                self.queue_edit(span, replacement)?;
299                return Ok(());
300            }
301        }
302        let replacement = if self.is_flow_context(target) {
303            value.render_flow(self)?
304        } else {
305            let yaml = value.prepared(self)?.to_yaml()?;
306            indent_continuation_lines(&yaml, self.node_indent(self.expect_node(target)?))
307        };
308        self.replace_node_text(target, replacement)?;
309        Ok(())
310    }
311
312    fn queue_value_removal(&mut self, target: NodeId) -> Result<(), YamlEditError> {
313        let Some(entry) = self.containing_entry(target) else {
314            self.remove_node(target)?;
315            return Ok(());
316        };
317        let Some(collection) = self.node(entry).and_then(|node| node.parent()) else {
318            self.remove_node(entry)?;
319            return Ok(());
320        };
321        let flow = matches!(
322            self.semantic_kind(collection),
323            Some(
324                SemanticKind::Mapping {
325                    style: CollectionStyle::Flow
326                } | SemanticKind::Sequence {
327                    style: CollectionStyle::Flow
328                }
329            )
330        );
331        if !flow {
332            self.remove_node(entry)?;
333            return Ok(());
334        }
335
336        let entries = self
337            .children(collection)
338            .filter(|node| self.containing_entry_child(*node))
339            .collect::<Vec<_>>();
340        let index = entries
341            .iter()
342            .position(|candidate| *candidate == entry)
343            .ok_or_else(|| YamlEditError::new("flow collection entry is missing"))?;
344        let entry_span = self.expect_node(entry)?.span;
345        let span = if let Some(next) = entries.get(index + 1).copied() {
346            Span::new(entry_span.start, self.expect_node(next)?.span.start)
347        } else if index > 0 {
348            let previous = self.expect_node(entries[index - 1])?.span;
349            Span::new(previous.end, entry_span.end)
350        } else {
351            entry_span
352        };
353        self.queue_edit(span, String::new())?;
354        Ok(())
355    }
356
357    fn containing_entry_child(&self, node: NodeId) -> bool {
358        self.node(node).is_some_and(|node| {
359            matches!(
360                node.kind(),
361                crate::NodeKind::MappingEntry | crate::NodeKind::SequenceEntry
362            )
363        })
364    }
365
366    fn queue_mapping_insert(
367        &mut self,
368        mapping: NodeId,
369        key: &str,
370        value: &YamlFragment,
371    ) -> Result<(), YamlEditError> {
372        let Some(SemanticKind::Mapping { style }) = self.semantic_kind(mapping) else {
373            return Err(YamlEditError::new(
374                "mapping insertion target is not a mapping",
375            ));
376        };
377        let key = emit_string_key(key);
378        match style {
379            CollectionStyle::Flow => {
380                let mapping_node = self.expect_node(mapping)?;
381                let close = closing_delimiter_offset(self, mapping_node.span, '}')?;
382                let prefix = if self.mapping_entries(mapping).next().is_some() {
383                    ", "
384                } else {
385                    ""
386                };
387                let value = value.render_flow(self)?;
388                self.queue_edit(
389                    Span::empty_from_usize(close),
390                    format!("{prefix}{key}: {value}"),
391                )?;
392            }
393            CollectionStyle::Block => {
394                let mapping_node = self.expect_node(mapping)?;
395                let indent = self.node_indent(mapping_node);
396                let offset = self.mapping_insertion_offset(mapping_node);
397                let mut insertion = insertion_prefix(self, offset);
398                let value = value.prepared(self)?.to_yaml()?;
399                insertion.push_str(&format_block_mapping_entry(
400                    &key,
401                    &value,
402                    indent,
403                    self.preferred_line_ending(),
404                ));
405                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
406            }
407        }
408        Ok(())
409    }
410
411    fn queue_sequence_insert(
412        &mut self,
413        sequence: NodeId,
414        index: usize,
415        value: &YamlFragment,
416    ) -> Result<(), YamlEditError> {
417        let Some(SemanticKind::Sequence { style }) = self.semantic_kind(sequence) else {
418            return Err(YamlEditError::new(
419                "sequence insertion target is not a sequence",
420            ));
421        };
422        let items = self.sequence_items(sequence).collect::<Vec<_>>();
423        match style {
424            CollectionStyle::Flow => {
425                let sequence_node = self.expect_node(sequence)?;
426                let offset = if let Some(item) = items.get(index).copied() {
427                    self.expect_node(item)?.span.start as usize
428                } else {
429                    closing_delimiter_offset(self, sequence_node.span, ']')?
430                };
431                let value = value.render_flow(self)?;
432                let insertion = if items.is_empty() {
433                    value
434                } else if index < items.len() {
435                    format!("{value}, ")
436                } else {
437                    format!(", {value}")
438                };
439                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
440            }
441            CollectionStyle::Block => {
442                let sequence_node = self.expect_node(sequence)?;
443                let indent = self.node_indent(sequence_node);
444                let offset = if let Some(item) = items.get(index).copied() {
445                    let entry = self.containing_entry(item).unwrap_or(item);
446                    self.line_start_offset(self.expect_node(entry)?.span.start as usize)
447                } else {
448                    self.sequence_insertion_offset(sequence_node)
449                };
450                let mut insertion = insertion_prefix(self, offset);
451                let value = value.prepared(self)?.to_yaml()?;
452                insertion.push_str(&format_block_sequence_entry(
453                    &value,
454                    indent,
455                    self.preferred_line_ending(),
456                ));
457                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
458            }
459        }
460        Ok(())
461    }
462
463    fn is_flow_context(&self, mut node: NodeId) -> bool {
464        while let Some(parent) = self.node(node).and_then(|node| node.parent()) {
465            if matches!(
466                self.semantic_kind(parent),
467                Some(
468                    SemanticKind::Mapping {
469                        style: CollectionStyle::Flow
470                    } | SemanticKind::Sequence {
471                        style: CollectionStyle::Flow
472                    }
473                )
474            ) {
475                return true;
476            }
477            node = parent;
478        }
479        false
480    }
481
482    fn anchor_has_external_alias(&self, root: NodeId) -> bool {
483        let Some(root_span) = self.node(root).map(|node| node.span()) else {
484            return false;
485        };
486        let anchored = self
487            .nodes
488            .iter()
489            .enumerate()
490            .map(|(index, _)| NodeId::from_usize(index))
491            .filter(|node| {
492                self.anchor(*node).is_some()
493                    && self.node(*node).is_some_and(|node| {
494                        node.span().start >= root_span.start && node.span().end <= root_span.end
495                    })
496            })
497            .collect::<Vec<_>>();
498        self.nodes
499            .iter()
500            .enumerate()
501            .map(|(index, _)| NodeId::from_usize(index))
502            .filter(|node| matches!(self.semantic_kind(*node), Some(SemanticKind::Alias)))
503            .any(|alias| {
504                let outside = self.node(alias).is_some_and(|node| {
505                    node.span().start < root_span.start || node.span().end > root_span.end
506                });
507                outside
508                    && self
509                        .resolve_alias(alias)
510                        .is_some_and(|target| anchored.contains(&target))
511            })
512    }
513
514    pub(crate) fn line_start_offset(&self, offset: usize) -> usize {
515        let offset = u32::try_from(offset).unwrap_or(u32::MAX);
516        match self.source.line_starts().binary_search(&offset) {
517            Ok(index) => self.source.line_starts()[index] as usize,
518            Err(index) => self.source.line_starts()[index.saturating_sub(1)] as usize,
519        }
520    }
521}
522
523fn closing_delimiter_offset(
524    doc: &YamlDoc,
525    span: Span,
526    delimiter: char,
527) -> Result<usize, YamlEditError> {
528    let source = doc.source.slice(span);
529    let relative = source
530        .rfind(delimiter)
531        .ok_or_else(|| YamlEditError::new(format!("missing `{delimiter}` delimiter")))?;
532    Ok(span.start as usize + relative)
533}
534
535fn insertion_prefix(doc: &YamlDoc, offset: usize) -> String {
536    if offset == doc.source.len()
537        && !doc
538            .source
539            .as_str()
540            .as_bytes()
541            .last()
542            .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
543    {
544        doc.preferred_line_ending().to_owned()
545    } else {
546        String::new()
547    }
548}
549
550fn format_block_mapping_entry(key: &str, value: &str, indent: usize, ending: &str) -> String {
551    let prefix = " ".repeat(indent);
552    if !value.contains(['\n', '\r']) {
553        return format!("{prefix}{key}: {value}{ending}");
554    }
555    let value = indent_text(value, indent + 2);
556    let mut output = format!("{prefix}{key}:{ending}{value}");
557    if !output.ends_with(['\n', '\r']) {
558        output.push_str(ending);
559    }
560    output
561}
562
563fn format_block_sequence_entry(value: &str, indent: usize, ending: &str) -> String {
564    let prefix = " ".repeat(indent);
565    if !value.contains(['\n', '\r']) {
566        return format!("{prefix}- {value}{ending}");
567    }
568    let value = indent_text(value, indent + 2);
569    let mut output = format!("{prefix}-{ending}{value}");
570    if !output.ends_with(['\n', '\r']) {
571        output.push_str(ending);
572    }
573    output
574}
575
576fn indent_continuation_lines(value: &str, indent: usize) -> String {
577    if indent == 0 {
578        return value.to_owned();
579    }
580    let prefix = " ".repeat(indent);
581    let mut output = String::with_capacity(value.len());
582    let mut after_break = false;
583    for character in value.chars() {
584        if after_break && !matches!(character, '\r' | '\n') {
585            output.push_str(&prefix);
586            after_break = false;
587        }
588        output.push(character);
589        if character == '\n' {
590            after_break = true;
591        } else if character != '\r' {
592            after_break = false;
593        }
594    }
595    output
596}
597
598fn emit_string_key(value: &str) -> String {
599    if safe_plain_string(value) {
600        value.to_owned()
601    } else {
602        crate::fragment::quote_string(value)
603    }
604}
605
606fn safe_plain_string(value: &str) -> bool {
607    if value.is_empty()
608        || value.trim() != value
609        || value.contains(['\n', '\r', '\t', ':', '#', '[', ']', '{', '}', ','])
610        || value.starts_with(['-', '?', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`'])
611    {
612        return false;
613    }
614    matches!(
615        resolve_scalar(value, YamlScalarStyle::Plain, None),
616        Ok(ResolvedScalar::String)
617    )
618}
619
620fn scalar_is_string(doc: &YamlDoc, node: NodeId) -> Result<bool, YamlEditError> {
621    let Some(SemanticKind::Scalar { style }) = doc.semantic_kind(node) else {
622        return Ok(false);
623    };
624    let value = doc.scalar_value(node)?;
625    let tag = doc.resolved_tag(node)?;
626    Ok(matches!(
627        resolve_scalar(&value, style, tag.as_deref()),
628        Ok(ResolvedScalar::String)
629    ))
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635
636    fn pointer(value: &str) -> JsonPointer {
637        JsonPointer::parse(value).unwrap()
638    }
639
640    fn fragment(value: &str) -> YamlFragment {
641        YamlFragment::parse(value).unwrap()
642    }
643
644    #[test]
645    fn adds_replaces_and_removes_block_mapping_values() {
646        let mut doc = YamlDoc::parse("server:\n  host: localhost # keep\n").unwrap();
647        doc.add_at(0, &pointer("/server/port"), &fragment("8080"))
648            .unwrap();
649        assert_eq!(
650            doc.as_source(),
651            "server:\n  host: localhost # keep\n  port: 8080\n"
652        );
653        doc.replace_at(0, &pointer("/server/host"), &fragment("example.com"))
654            .unwrap();
655        assert_eq!(
656            doc.as_source(),
657            "server:\n  host: example.com # keep\n  port: 8080\n"
658        );
659        doc.remove_at(0, &pointer("/server/port")).unwrap();
660        assert_eq!(doc.as_source(), "server:\n  host: example.com # keep\n");
661    }
662
663    #[test]
664    fn adds_string_keys_without_changing_their_schema_type() {
665        let mut doc = YamlDoc::parse("{}\n").unwrap();
666        doc.add_at(0, &pointer("/true"), &fragment("value"))
667            .unwrap();
668        assert_eq!(doc.as_source(), "{\"true\": value}\n");
669        assert!(
670            doc.resolve_pointer(0, &pointer("/true")).is_ok(),
671            "{}",
672            doc.as_source()
673        );
674    }
675
676    #[test]
677    fn inserts_block_and_flow_sequence_items() {
678        let mut block = YamlDoc::parse("items:\n  - a\n  - c\n").unwrap();
679        block
680            .add_at(0, &pointer("/items/1"), &fragment("b"))
681            .unwrap();
682        block
683            .add_at(0, &pointer("/items/-"), &fragment("d"))
684            .unwrap();
685        assert_eq!(block.as_source(), "items:\n  - a\n  - b\n  - c\n  - d\n");
686
687        let mut flow = YamlDoc::parse("items: [a, c]\n").unwrap();
688        flow.add_at(0, &pointer("/items/1"), &fragment("b"))
689            .unwrap();
690        assert_eq!(flow.as_source(), "items: [a, b, c]\n");
691    }
692
693    #[test]
694    fn mutations_are_transactional() {
695        let input = "items: [a]\n";
696        let mut doc = YamlDoc::parse(input).unwrap();
697        assert!(doc.add_at(0, &pointer("/items/4"), &fragment("x")).is_err());
698        assert_eq!(doc.as_source(), input);
699    }
700
701    #[test]
702    fn move_uses_remove_then_add_sequence_indices() {
703        let mut doc = YamlDoc::parse("[a, b, c]\n").unwrap();
704        doc.move_at(0, &pointer("/0"), &pointer("/2")).unwrap();
705        assert_eq!(doc.as_source(), "[b, c, a]\n");
706    }
707
708    #[test]
709    fn copy_rejects_anchors_and_test_is_semantic() {
710        let mut doc = YamlDoc::parse("one: &one {value: 1}\ntwo: null\n").unwrap();
711        assert!(
712            doc.copy_at(0, &pointer("/one"), &pointer("/two"))
713                .unwrap_err()
714                .to_string()
715                .contains("anchor")
716        );
717        assert!(
718            doc.test_at(0, &pointer("/one/value"), &fragment("1.0"))
719                .unwrap()
720        );
721    }
722}