diff --git a/src/lossless.rs b/src/lossless.rs index 61cd4a9..2ffd0ae 100644 --- a/src/lossless.rs +++ b/src/lossless.rs @@ -334,6 +334,7 @@ impl LosslessEffectiveMappingEntry { #[derive(Clone, Debug, PartialEq)] pub struct LosslessStream { source: Arc, + options: LoadOptions, documents: Vec, nodes: Vec, anchors: Vec, @@ -352,7 +353,7 @@ impl LosslessStream { let events = options.stream_events(input)?.collect::>>()?; let source: Arc = Arc::from(input); let trivia = scan_trivia(&source); - Builder::new(source, events, trivia).build() + Builder::new(source, events, trivia, options).build() } /// Returns the original YAML source. @@ -1427,7 +1428,7 @@ impl LosslessEdit<'_> { )); } let replacement = replacement.into(); - ensure_scalar_fragment(&replacement, current.span())?; + ensure_scalar_fragment_with_options(&replacement, current.span(), self.stream.options)?; self.replace_node_source(node, replacement) } @@ -1512,7 +1513,7 @@ impl LosslessEdit<'_> { self.insert_block_mapping_entry_source_with_options( mapping, entry_source, - LoadOptions::new(), + self.stream.options, ) } @@ -1621,7 +1622,7 @@ impl LosslessEdit<'_> { self.insert_flow_mapping_entry_source_with_options( mapping, entry_source, - LoadOptions::new(), + self.stream.options, ) } @@ -1723,7 +1724,7 @@ impl LosslessEdit<'_> { sequence, index, replacement, - LoadOptions::new(), + self.stream.options, ) } @@ -1780,7 +1781,7 @@ impl LosslessEdit<'_> { sequence, index, item_source, - LoadOptions::new(), + self.stream.options, ) } @@ -1904,7 +1905,7 @@ impl LosslessEdit<'_> { sequence, index, item_source, - LoadOptions::new(), + self.stream.options, ) } @@ -2012,7 +2013,8 @@ impl LosslessEdit<'_> { /// Returns validated edited YAML with untouched source bytes preserved. pub fn finish(self) -> Result { - self.finish_with_options(LoadOptions::new()) + let options = self.stream.options; + self.finish_with_options(options) } fn finish_with_options(mut self, options: LoadOptions) -> Result { @@ -2580,6 +2582,7 @@ impl LosslessTrivia { struct Builder { source: Arc, + options: LoadOptions, events: Vec, documents: Vec, nodes: Vec, @@ -2592,9 +2595,15 @@ struct Builder { } impl Builder { - fn new(source: Arc, events: Vec, trivia: Vec) -> Self { + fn new( + source: Arc, + events: Vec, + trivia: Vec, + options: LoadOptions, + ) -> Self { Self { source, + options, events, documents: Vec::new(), nodes: Vec::new(), @@ -2673,6 +2682,7 @@ impl Builder { } Ok(LosslessStream { source: self.source, + options: self.options, documents: self.documents, nodes: self.nodes, anchors: self.anchors, @@ -3566,10 +3576,6 @@ fn scan_trivia(input: &Arc) -> Vec { trivia } -fn ensure_scalar_fragment(replacement: &str, span: Span) -> Result<()> { - ensure_scalar_fragment_with_options(replacement, span, LoadOptions::new()) -} - fn ensure_scalar_fragment_with_options( replacement: &str, span: Span, diff --git a/tests/lossless_roundtrip.rs b/tests/lossless_roundtrip.rs index 8320ae7..756f6b5 100644 --- a/tests/lossless_roundtrip.rs +++ b/tests/lossless_roundtrip.rs @@ -796,6 +796,79 @@ fn lossless_edit_rejects_overlapping_replacements() { assert!(error.to_string().contains("lossless replacements overlap")); } +#[test] +fn lossless_edit_preserves_custom_load_options_for_noop_and_mutating_edits() { + let mut input = String::from("items:\n"); + for index in 0..=DEFAULT_MAX_COLLECTION_ITEMS { + input.push_str(" - item"); + input.push_str(&index.to_string()); + input.push('\n'); + } + input.push_str("flag: on\n"); + + let options = LoadOptions::yaml_1_1().without_collection_limit(); + let stream = saneyaml::parse_lossless_with_options(&input, options) + .expect("custom options allow the wide collection"); + let flag = stream + .resolve_path(0, &[PathSegment::from("flag")]) + .expect("flag path"); + assert!(matches!( + stream.node(flag).expect("flag node").kind(), + LosslessNodeKind::Scalar { value, .. } if value == "true" + )); + + assert_eq!(stream.edit().finish().expect("no-op edit"), input); + + let mut edit = stream.edit(); + edit.replace_scalar_source(flag, "off") + .expect("replace YAML 1.1 boolean scalar"); + let output = edit.finish().expect("custom options validate the edit"); + assert_eq!(output, input.replacen("flag: on", "flag: off", 1)); +} + +#[test] +fn lossless_structural_edit_enforces_custom_fragment_limits() { + let stream = saneyaml::parse_lossless_with_options( + "items:\n - one\n", + LoadOptions::new().max_scalar_bytes(5), + ) + .expect("source is within the custom scalar limit"); + let sequence = stream + .resolve_path(0, &[PathSegment::from("items")]) + .expect("items path"); + + let error = stream + .edit() + .insert_block_sequence_item_source(sequence, 1, "longer") + .expect_err("structural insertion must use the stream's scalar limit"); + assert!(error.to_string().contains("scalar")); +} + +#[test] +fn lossless_scalar_edit_enforces_custom_fragment_limits() { + let stream = saneyaml::parse_lossless_with_options( + "name: old\n", + LoadOptions::new().max_scalar_bytes(5), + ) + .expect("source is within the custom scalar limit"); + let scalar = stream + .nodes() + .iter() + .find(|node| { + matches!( + node.kind(), + LosslessNodeKind::Scalar { value, .. } if value == "old" + ) + }) + .expect("old scalar"); + + let error = stream + .edit() + .replace_scalar_source(scalar.id(), "longer") + .expect_err("scalar replacement must use the stream's scalar limit"); + assert!(error.to_string().contains("scalar")); +} + fn flow_mapping_with_key<'a>( stream: &'a saneyaml::LosslessStream, key: &str,