1use std::borrow::Cow;
4
5use line_index::{LineCol, TextRange, TextSize};
6
7#[derive(thiserror::Error, Debug)]
9pub enum Error {
10 #[error("YAML query error: {0}")]
11 Query(#[from] yamlpath::QueryError),
12 #[error("YAML serialization error: {0}")]
13 Serialization(#[from] serde_yaml::Error),
14 #[error("Invalid operation: {0}")]
15 InvalidOperation(String),
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum Style {
21 BlockMapping,
23 BlockSequence,
25 MultilineFlowMapping,
34 FlowMapping,
36 MultilineFlowSequence,
44 FlowSequence,
46 MultilineLiteralScalar,
48 MultilineFoldedScalar,
50 DoubleQuoted,
52 SingleQuoted,
54 PlainScalar,
56}
57
58impl Style {
59 pub fn from_feature(feature: &yamlpath::Feature, doc: &yamlpath::Document) -> Self {
61 let content = doc.extract(feature);
62 let trimmed = content.trim().as_bytes();
63 let multiline = trimmed.contains(&b'\n');
64
65 match feature.kind() {
66 yamlpath::FeatureKind::BlockMapping => Style::BlockMapping,
67 yamlpath::FeatureKind::BlockSequence => Style::BlockSequence,
68 yamlpath::FeatureKind::FlowMapping => {
69 if multiline {
70 Style::MultilineFlowMapping
71 } else {
72 Style::FlowMapping
73 }
74 }
75 yamlpath::FeatureKind::FlowSequence => {
76 if multiline {
77 Style::MultilineFlowSequence
78 } else {
79 Style::FlowSequence
80 }
81 }
82 yamlpath::FeatureKind::Scalar => match trimmed[0] {
83 b'|' => Style::MultilineLiteralScalar,
84 b'>' => Style::MultilineFoldedScalar,
85 b'"' => Style::DoubleQuoted,
86 b'\'' => Style::SingleQuoted,
87 _ => Style::PlainScalar,
88 },
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
98pub struct Patch<'doc> {
99 pub route: yamlpath::Route<'doc>,
101 pub operation: Op<'doc>,
103}
104
105#[derive(Debug, Clone)]
107pub enum Op<'doc> {
108 RewriteFragment {
142 from: subfeature::Subfeature<'doc>,
143 to: Cow<'doc, str>,
144 },
145 ReplaceComment { new: Cow<'doc, str> },
154 EmplaceComment { new: Cow<'doc, str> },
159 Replace(serde_yaml::Value),
161 Add {
171 key: String,
172 value: serde_yaml::Value,
173 },
174 MergeInto {
178 key: String,
179 updates: indexmap::IndexMap<String, serde_yaml::Value>,
180 },
181 #[allow(dead_code)]
183 Remove,
184 Append { value: serde_yaml::Value },
188}
189
190pub fn apply_yaml_patches(
201 document: &yamlpath::Document,
202 patches: &[Patch],
203) -> Result<yamlpath::Document, Error> {
204 let mut patches = patches.iter();
205
206 let mut next_document = {
207 let Some(patch) = patches.next() else {
208 return Err(Error::InvalidOperation("no patches provided".to_string()));
209 };
210
211 apply_single_patch(document, patch)?
212 };
213
214 for patch in patches {
215 next_document = apply_single_patch(&next_document, patch)?;
216 }
217
218 Ok(next_document)
219}
220
221fn apply_single_patch(
223 document: &yamlpath::Document,
224 patch: &Patch,
225) -> Result<yamlpath::Document, Error> {
226 let content = document.source();
227 let mut patched_content = match &patch.operation {
228 Op::RewriteFragment { from, to } => {
229 let (extracted_feature, range) = if patch.route.is_empty() {
235 let source = document.source();
236 (source, 0..source.len())
237 } else {
238 let Some(feature) = route_to_feature_exact(&patch.route, document)? else {
239 return Err(Error::InvalidOperation(format!(
240 "no pre-existing value to patch at {route:?}",
241 route = patch.route
242 )));
243 };
244
245 (
246 document.extract(&feature),
247 feature.location.byte_span.0..feature.location.byte_span.1,
248 )
249 };
250
251 let bias = from.after;
252
253 if bias > extracted_feature.len() {
254 return Err(Error::InvalidOperation(format!(
255 "replacement scan index {bias} is out of bounds for feature",
256 )));
257 }
258
259 let Some(span) = from.locate_within(extracted_feature) else {
260 return Err(Error::InvalidOperation(format!(
261 "no match for '{from:?}' in feature",
262 )));
263 };
264
265 let mut patched_feature = extracted_feature.to_string();
266 patched_feature.replace_range(span.as_range(), to);
267
268 let mut patched_content = content.to_string();
270 patched_content.replace_range(range, &patched_feature);
271
272 patched_content
273 }
274 Op::ReplaceComment { new } => {
275 let feature = route_to_feature_exact(&patch.route, document)?.ok_or_else(|| {
276 Error::InvalidOperation(format!(
277 "no existing feature at {route:?}",
278 route = patch.route
279 ))
280 })?;
281
282 let comment_features = document.feature_comments(&feature);
283 let comment_feature = match comment_features.len() {
284 0 => return Ok(document.clone()),
285 1 => &comment_features[0],
286 _ => {
287 return Err(Error::InvalidOperation(format!(
288 "multiple comments found at {route:?}",
289 route = patch.route
290 )));
291 }
292 };
293
294 let mut result = content.to_string();
295 result.replace_range(comment_feature, new);
296
297 result
298 }
299 Op::EmplaceComment { new } => {
300 let feature = route_to_feature_exact(&patch.route, document)?.ok_or_else(|| {
303 Error::InvalidOperation(format!(
304 "no existing feature at {route:?}",
305 route = patch.route
306 ))
307 })?;
308
309 if matches!(
313 Style::from_feature(&feature, document),
314 Style::SingleQuoted | Style::DoubleQuoted
315 ) && feature.is_multiline()
316 {
317 return Err(Error::InvalidOperation(format!(
318 "cannot emplace comment on non-block multi-line scalar at {route:?}",
319 route = patch.route
320 )));
321 }
322
323 let comment_features = document.feature_comments(&feature);
324 match comment_features.len() {
325 0 => {
326 let line_range = line_span(document, feature.location.byte_span.0);
331 let mut insert_pos = line_range.end;
332 if let Some(b'\n') = document.source().as_bytes().get(insert_pos - 1) {
333 insert_pos -= 1;
334 }
335 if let Some(b'\r') = document.source().as_bytes().get(insert_pos - 1) {
336 insert_pos -= 1;
337 }
338
339 let mut result = content.to_string();
340 result.insert_str(insert_pos, &format!(" {new}"));
341
342 result
343 }
344 1 => {
345 return apply_single_patch(
346 document,
347 &Patch {
348 route: patch.route.clone(),
349 operation: Op::ReplaceComment { new: new.clone() },
350 },
351 );
352 }
353 _ => {
354 return Err(Error::InvalidOperation(format!(
355 "multiple comments found at {route:?}",
356 route = patch.route
357 )));
358 }
359 }
360 }
361 Op::Replace(value) => {
362 let feature = route_to_feature_pretty(&patch.route, document)?;
363
364 let replacement = apply_value_replacement(&feature, document, value, true)?;
366
367 let current_content = document.extract(&feature);
369 let current_content_with_ws = document.extract_with_leading_whitespace(&feature);
370
371 let (start_span, end_span) = if current_content_with_ws.contains(':') {
373 let ws_start = feature.location.byte_span.0
375 - (current_content_with_ws.len() - current_content.len());
376 (ws_start, feature.location.byte_span.1)
377 } else {
378 (feature.location.byte_span.0, feature.location.byte_span.1)
380 };
381
382 let mut result = content.to_string();
384 result.replace_range(start_span..end_span, &replacement);
385
386 result
387 }
388 Op::Add { key, value } => {
389 let key_query = patch.route.with_key(key.as_str());
393
394 if document.query_exists(&key_query) {
395 return Err(Error::InvalidOperation(format!(
396 "key '{key}' already exists at {route:?}",
397 key = key,
398 route = patch.route
399 )));
400 }
401
402 let feature = if patch.route.is_empty() {
403 document.top_feature()?
404 } else {
405 route_to_feature_exact(&patch.route, document)?.ok_or_else(|| {
406 Error::InvalidOperation(format!(
407 "no existing mapping at {route:?}",
408 route = patch.route
409 ))
410 })?
411 };
412
413 let style = Style::from_feature(&feature, document);
414 let feature_content = document.extract(&feature);
415
416 let updated_feature = match style {
417 Style::BlockMapping => {
418 handle_block_mapping_addition(feature_content, document, &feature, key, value)
419 }
420 Style::FlowMapping => handle_flow_mapping_addition(feature_content, key, value),
421 Style::MultilineFlowMapping => Err(Error::InvalidOperation(format!(
423 "add operation is not permitted against multiline flow mapping route: {:?}",
424 patch.route
425 ))),
426 _ => Err(Error::InvalidOperation(format!(
427 "add operation is not permitted against non-mapping route: {:?}",
428 patch.route
429 ))),
430 }?;
431
432 let mut result = content.to_string();
434 result.replace_range(&feature, &updated_feature);
435
436 result
437 }
438 Op::MergeInto { key, updates } => {
439 let existing_key_route = patch.route.with_key(key.as_str());
440 match route_to_feature_exact(&existing_key_route, document) {
441 Ok(Some(existing_feature)) => {
443 let style = Style::from_feature(&existing_feature, document);
445 if !matches!(style, Style::BlockMapping | Style::FlowMapping) {
446 return Err(Error::InvalidOperation(format!(
447 "can't perform merge against non-mapping at {existing_key_route:?}"
448 )));
449 }
450
451 let existing_content =
468 document.extract_with_leading_whitespace(&existing_feature);
469 let existing_mapping = serde_yaml::from_str::<serde_yaml::Mapping>(
470 existing_content,
471 )
472 .map_err(|e| {
473 Error::InvalidOperation(format!(
474 "MergeInto: failed to parse existing mapping at {existing_key_route:?}: {e}"
475 ))
476 })?;
477
478 let mut current_document = document.clone();
480 for (k, v) in updates {
481 if existing_mapping.contains_key(k) {
482 current_document = apply_single_patch(
483 ¤t_document,
484 &Patch {
485 route: existing_key_route.with_key(k.as_str()),
486 operation: Op::Replace(v.clone()),
487 },
488 )?;
489 } else {
490 current_document = apply_single_patch(
491 ¤t_document,
492 &Patch {
493 route: existing_key_route.clone(),
494 operation: Op::Add {
495 key: k.into(),
496 value: v.clone(),
497 },
498 },
499 )?;
500 }
501 }
502
503 return Ok(current_document);
504 }
505 Ok(None) => {
508 return Err(Error::InvalidOperation(format!(
509 "MergeInto: cannot merge into empty key at {existing_key_route:?}"
510 )));
511 }
512 Err(Error::Query(yamlpath::QueryError::ExhaustedMapping(_))) => {
514 return apply_single_patch(
515 document,
516 &Patch {
517 route: patch.route.clone(),
518 operation: Op::Add {
519 key: key.clone(),
520 value: serde_yaml::to_value(updates.clone())?,
521 },
522 },
523 );
524 }
525 Err(e) => return Err(e),
526 }
527 }
528 Op::Remove => {
529 if patch.route.is_empty() {
530 return Err(Error::InvalidOperation(
531 "Cannot remove root document".to_string(),
532 ));
533 }
534
535 let feature = route_to_feature_pretty(&patch.route, document)?;
536
537 let start_pos = {
541 let range = line_span(document, feature.location.byte_span.0);
542 range.start
543 };
544 let end_pos = {
545 let range = line_span(document, feature.location.byte_span.1);
546 range.end
547 };
548
549 let mut result = content.to_string();
550 result.replace_range(start_pos..end_pos, "");
551
552 result
553 }
554 Op::Append { value } => {
555 let feature = route_to_feature_exact(&patch.route, document)?.ok_or_else(|| {
556 Error::InvalidOperation(format!(
557 "no existing sequence at {route:?}",
558 route = patch.route
559 ))
560 })?;
561
562 let style = Style::from_feature(&feature, document);
563
564 match style {
565 Style::BlockSequence => {
566 let updated_feature = handle_block_sequence_append(document, &feature, value)?;
567
568 let mut result = content.to_string();
570 result.replace_range(&feature, &updated_feature);
571
572 result
573 }
574 Style::FlowSequence => {
575 return Err(Error::InvalidOperation(format!(
576 "append operation is not permitted against flow sequence route: {:?}",
577 patch.route
578 )));
579 }
580 _ => {
581 return Err(Error::InvalidOperation(format!(
582 "append operation is only permitted against sequence routes: {:?}",
583 patch.route
584 )));
585 }
586 }
587 }
588 };
589
590 if !patched_content.ends_with('\n') {
591 patched_content.push('\n');
592 }
593
594 yamlpath::Document::new(patched_content).map_err(Error::from)
595}
596
597pub fn route_to_feature_pretty<'a>(
598 route: &yamlpath::Route<'_>,
599 doc: &'a yamlpath::Document,
600) -> Result<yamlpath::Feature<'a>, Error> {
601 doc.query_pretty(route).map_err(Error::from)
602}
603
604pub fn route_to_feature_exact<'a>(
605 route: &yamlpath::Route<'_>,
606 doc: &'a yamlpath::Document,
607) -> Result<Option<yamlpath::Feature<'a>>, Error> {
608 doc.query_exact(route).map_err(Error::from)
609}
610
611fn serialize_yaml_value(value: &serde_yaml::Value) -> Result<String, Error> {
613 let yaml_str = serde_yaml::to_string(value)?;
614 Ok(yaml_str.trim_end().to_string()) }
616
617pub fn serialize_flow(value: &serde_yaml::Value) -> Result<String, Error> {
622 let mut buf = String::new();
623 fn serialize_inner(value: &serde_yaml::Value, buf: &mut String) -> Result<(), Error> {
624 match value {
625 serde_yaml::Value::Null => {
626 buf.push_str("null");
629 Ok(())
630 }
631 serde_yaml::Value::Bool(b) => {
632 buf.push_str(if *b { "true" } else { "false" });
633 Ok(())
634 }
635 serde_yaml::Value::Number(n) => {
636 buf.push_str(&n.to_string());
637 Ok(())
638 }
639 serde_yaml::Value::String(s) => {
640 if s.chars()
643 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
644 {
645 buf.push_str(s);
646 } else {
647 buf.push_str(
650 &serde_json::to_string(s)
651 .map_err(|e| Error::InvalidOperation(e.to_string()))?,
652 );
653 }
654
655 Ok(())
656 }
657 serde_yaml::Value::Sequence(values) => {
658 buf.push('[');
660 for (i, item) in values.iter().enumerate() {
661 if i > 0 {
662 buf.push_str(", ");
663 }
664 serialize_inner(item, buf)?;
665 }
666 buf.push(']');
667 Ok(())
668 }
669 serde_yaml::Value::Mapping(mapping) => {
670 buf.push_str("{ ");
672 for (i, (key, value)) in mapping.iter().enumerate() {
673 if i > 0 {
674 buf.push_str(", ");
675 }
676 if !matches!(key, serde_yaml::Value::String(_)) {
677 return Err(Error::InvalidOperation(format!(
678 "mapping keys must be strings, found: {key:?}"
679 )));
680 }
681 serialize_inner(key, buf)?;
682
683 buf.push_str(": ");
684 if !matches!(value, serde_yaml::Value::Null) {
685 serialize_inner(value, buf)?;
688 }
689 }
690 buf.push_str(" }");
691 Ok(())
692 }
693 serde_yaml::Value::Tagged(tagged_value) => Err(Error::InvalidOperation(format!(
694 "cannot serialize tagged value: {tagged_value:?}"
695 ))),
696 }
697 }
698
699 serialize_inner(value, &mut buf)?;
700 Ok(buf)
701}
702
703fn line_span(doc: &yamlpath::Document, pos: usize) -> core::ops::Range<usize> {
707 let pos = TextSize::new(pos as u32);
708 let LineCol { line, .. } = doc.line_index().line_col(pos);
709 doc.line_index()
710 .line(line)
711 .expect("impossible: line index gave us an invalid line")
712 .into()
713}
714
715pub fn extract_leading_indentation_for_block_item(
722 doc: &yamlpath::Document,
723 feature: &yamlpath::Feature,
724) -> usize {
725 let line_range = line_span(doc, feature.location.byte_span.0);
726
727 let line_content = &doc.source()[line_range].trim_end();
730
731 let mut accept_dash = true;
732 for (idx, b) in line_content.bytes().enumerate() {
733 match b {
734 b' ' => {
735 accept_dash = true;
736 }
737 b'-' => {
738 if accept_dash {
739 accept_dash = false;
740 } else {
741 return idx - 1;
742 }
743 }
744 _ => {
745 if !accept_dash {
748 return idx - 1;
749 } else {
750 return idx;
751 }
752 }
753 }
754 }
755
756 line_content.len() + 1
780}
781
782pub fn extract_leading_whitespace<'doc>(
785 doc: &'doc yamlpath::Document,
786 feature: &yamlpath::Feature,
787) -> &'doc str {
788 let line_range = line_span(doc, feature.location.byte_span.0);
789 let line_content = &doc.source()[line_range];
790
791 let end = line_content
792 .bytes()
793 .position(|b| b != b' ')
794 .unwrap_or(line_content.len());
795
796 &line_content[..end]
797}
798
799fn indent_multiline_yaml(content: &str, base_indent: &str) -> String {
801 let lines: Vec<&str> = content.lines().collect();
802 if lines.len() <= 1 {
803 return content.to_string();
804 }
805
806 let mut result = String::new();
807 for (i, line) in lines.iter().enumerate() {
808 if i == 0 {
809 result.push_str(line);
810 } else {
811 result.push('\n');
812 result.push_str(base_indent);
813 if !line.trim().is_empty() {
814 result.push_str(" "); result.push_str(line.trim_start());
816 }
817 }
818 }
819 result
820}
821
822fn handle_block_mapping_addition(
823 feature_content: &str,
824 doc: &yamlpath::Document,
825 feature: &yamlpath::Feature,
826 key: &str,
827 value: &serde_yaml::Value,
828) -> Result<String, Error> {
829 let new_value_str = if matches!(value, serde_yaml::Value::Sequence(_)) {
831 serialize_flow(value)?
833 } else {
834 serialize_yaml_value(value)?
835 };
836 let new_value_str = new_value_str.trim_end(); let indent = " ".repeat(extract_leading_indentation_for_block_item(doc, feature));
840
841 let mut final_entry = if let serde_yaml::Value::Mapping(mapping) = &value {
843 if mapping.is_empty() {
844 format!("\n{indent}{key}: {new_value_str}")
846 } else {
847 let value_lines = new_value_str.lines();
849 let mut result = format!("\n{indent}{key}:");
850 for line in value_lines {
851 if !line.trim().is_empty() {
852 result.push('\n');
853 result.push_str(&indent);
854 result.push_str(" "); result.push_str(line.trim_start());
856 }
857 }
858 result
859 }
860 } else if new_value_str.contains('\n') {
861 let indented_value = indent_multiline_yaml(new_value_str, &indent);
863 format!("\n{indent}{key}: {indented_value}")
864 } else {
865 format!("\n{indent}{key}: {new_value_str}")
866 };
867
868 let insertion_point = find_content_end(feature, doc);
872
873 if insertion_point < feature.location.byte_span.1 {
877 final_entry.push('\n');
878 }
879
880 let needs_leading_newline = if insertion_point > 0 {
883 doc.source().as_bytes().get(insertion_point - 1) != Some(&b'\n')
884 } else {
885 true
886 };
887
888 let final_entry_to_insert = if needs_leading_newline {
889 final_entry
890 } else {
891 final_entry
893 .strip_prefix('\n')
894 .unwrap_or(&final_entry)
895 .to_string()
896 };
897
898 let bias = feature.location.byte_span.0;
902 let relative_insertion_point = insertion_point - bias;
903
904 let mut updated_feature = feature_content.to_string();
905 updated_feature.insert_str(relative_insertion_point, &final_entry_to_insert);
906
907 Ok(updated_feature)
908}
909
910fn handle_block_sequence_append(
911 doc: &yamlpath::Document,
912 feature: &yamlpath::Feature,
913 value: &serde_yaml::Value,
914) -> Result<String, Error> {
915 let feature_content = doc.extract(feature);
916 let indent = extract_leading_whitespace(doc, feature);
917
918 let value_str = if matches!(value, serde_yaml::Value::Sequence(_)) {
920 serialize_flow(value)?
921 } else {
922 serialize_yaml_value(value)?
923 };
924 let insertion_point = find_content_end(feature, doc);
925 let bias = feature.location.byte_span.0;
926 let relative_insertion_point = insertion_point - bias;
927
928 let needs_leading_newline = if relative_insertion_point > 0 {
930 feature_content.chars().nth(relative_insertion_point - 1) != Some('\n')
931 } else {
932 !feature_content.is_empty()
933 };
934
935 let mut new_item = String::new();
936 if needs_leading_newline {
937 new_item.push('\n');
938 }
939
940 let mut lines = value_str.lines();
941 if let Some(first_line) = lines.next() {
942 new_item.push_str(&format!("{}- {}", indent, first_line));
944
945 let item_content_indent = format!("{} ", indent);
947 for line in lines {
948 new_item.push('\n');
949 new_item.push_str(&item_content_indent);
950 new_item.push_str(line);
951 }
952 } else {
953 new_item.push_str(&format!("{}- {}", indent, value_str));
955 }
956
957 let mut updated_feature = feature_content.to_string();
958 updated_feature.insert_str(relative_insertion_point, &new_item);
959
960 Ok(updated_feature)
961}
962
963fn handle_flow_mapping_addition(
965 feature_content: &str,
966 key: &str,
967 value: &serde_yaml::Value,
968) -> Result<String, Error> {
969 let mut existing_mapping = serde_yaml::from_str::<serde_yaml::Mapping>(feature_content)
980 .map_err(Error::Serialization)?;
981
982 existing_mapping.insert(key.into(), value.clone());
983
984 let updated_content = serialize_flow(&serde_yaml::Value::Mapping(existing_mapping))?;
985
986 Ok(updated_content)
987}
988
989pub fn find_content_end(feature: &yamlpath::Feature, doc: &yamlpath::Document) -> usize {
991 let lines: Vec<_> = doc
992 .line_index()
993 .lines(TextRange::new(
994 (feature.location.byte_span.0 as u32).into(),
995 (feature.location.byte_span.1 as u32).into(),
996 ))
997 .collect();
998
999 for line in lines.into_iter().rev() {
1002 let line_content = &doc.source()[line];
1003 let trimmed = line_content.trim();
1004
1005 if !trimmed.is_empty() && !trimmed.starts_with('#') {
1006 return line.end().into();
1007 }
1008 }
1009
1010 feature.location.byte_span.1 }
1012
1013fn apply_value_replacement(
1015 feature: &yamlpath::Feature,
1016 doc: &yamlpath::Document,
1017 value: &serde_yaml::Value,
1018 support_multiline_literals: bool,
1019) -> Result<String, Error> {
1020 let current_content_with_ws = doc.extract_with_leading_whitespace(feature);
1022
1023 let start_byte = feature.location.byte_span.0;
1025 let end_byte = feature.location.byte_span.1;
1026
1027 let trimmed_content = current_content_with_ws.trim();
1030 let is_flow_mapping = trimmed_content.starts_with('{')
1031 && trimmed_content.ends_with('}')
1032 && !trimmed_content.contains('\n');
1033
1034 if is_flow_mapping {
1035 return handle_flow_mapping_value_replacement(
1037 doc.source(),
1038 start_byte,
1039 end_byte,
1040 current_content_with_ws,
1041 value,
1042 );
1043 }
1044
1045 let replacement = if let Some(colon_pos) = current_content_with_ws.find(':') {
1047 let key_part = ¤t_content_with_ws[..colon_pos + 1];
1049 let value_part = ¤t_content_with_ws[colon_pos + 1..];
1050
1051 if support_multiline_literals {
1052 let is_multiline_literal = value_part.trim_start().starts_with('|');
1054
1055 if is_multiline_literal {
1056 if let serde_yaml::Value::String(string_content) = value
1058 && string_content.contains('\n')
1059 {
1060 let leading_whitespace = extract_leading_whitespace(doc, feature);
1062 let content_indent = format!("{leading_whitespace} "); let indented_content = string_content
1066 .lines()
1067 .map(|line| {
1068 if line.trim().is_empty() {
1069 String::new()
1070 } else {
1071 format!("{}{}", content_indent, line.trim_start())
1072 }
1073 })
1074 .collect::<Vec<_>>()
1075 .join("\n");
1076
1077 let pipe_pos = value_part.find('|').expect("impossible");
1079 let key_with_pipe = ¤t_content_with_ws
1080 [..colon_pos + 1 + value_part[..pipe_pos].len() + 1];
1081 return Ok(format!(
1082 "{}\n{}",
1083 key_with_pipe.trim_end(),
1084 indented_content
1085 ));
1086 }
1087 }
1088 }
1089
1090 let val_str = serialize_yaml_value(value)?;
1092 format!("{} {}", key_part, val_str.trim())
1093 } else {
1094 serialize_yaml_value(value)?
1097 };
1098
1099 Ok(replacement)
1100}
1101
1102fn handle_flow_mapping_value_replacement(
1104 _content: &str,
1105 _start_byte: usize,
1106 _end_byte: usize,
1107 current_content: &str,
1108 value: &serde_yaml::Value,
1109) -> Result<String, Error> {
1110 let val_str = serialize_yaml_value(value)?;
1111 let val_str = val_str.trim();
1112
1113 let trimmed = current_content.trim();
1115
1116 if let Some(colon_pos) = trimmed.find(':') {
1118 let before_colon = &trimmed[..colon_pos];
1119 let after_colon = &trimmed[colon_pos + 1..];
1120
1121 let value_part = after_colon.trim().trim_end_matches('}').trim();
1123
1124 if value_part.is_empty() {
1125 let key_part = before_colon.trim_start_matches('{').trim();
1127 Ok(format!("{{ {key_part}: {val_str} }}"))
1128 } else {
1129 let key_part = before_colon.trim_start_matches('{').trim();
1131 Ok(format!("{{ {key_part}: {val_str} }}"))
1132 }
1133 } else {
1134 let key_part = trimmed.trim_start_matches('{').trim_end_matches('}').trim();
1136 Ok(format!("{{ {key_part}: {val_str} }}"))
1137 }
1138}