mod common;
use common::{
assert_edited, assert_file_edited, assert_file_edited_valid, assert_parser_shaped, value_of,
};
use rowan::ast::AstNode;
use std::str::FromStr;
use yaml_edit::validator::Validator;
use yaml_edit::{
debug, AsYaml, Document, Mapping, MappingBuilder, MappingEntry, ScalarValue, Sequence,
SequenceBuilder, YamlFile, YamlNode, YamlValue,
};
fn root(src: &str) -> Document {
Document::from_str(src).unwrap()
}
fn nested(mapping: &Mapping) -> Mapping {
mapping.get_mapping("m").unwrap()
}
#[track_caller]
fn assert_valid_copy(file: &YamlFile, expected: &str) {
assert_file_edited(file, expected);
assert_eq!(Validator::new().validate_syntax(file.syntax()), vec![]);
}
#[track_caller]
fn check_copy(target: &str, edit: impl FnOnce(&Mapping), expected: &str) -> YamlFile {
let file = YamlFile::from_str(target).unwrap();
edit(&file.document().unwrap().as_mapping().unwrap());
assert_valid_copy(&file, expected);
file
}
#[test]
fn set_node_value() {
let file = YamlFile::from_str("a: 1\n").unwrap();
let mapping = file.document().unwrap().as_mapping().unwrap();
mapping.set("b", value_of("k: v\n", "k"));
assert_file_edited(&file, "a: 1\nb: v\n");
assert_eq!(mapping.get("b").unwrap().to_string(), "v");
let file = YamlFile::from_str("a: &x 1\n").unwrap();
let mapping = file.document().unwrap().as_mapping().unwrap();
mapping.set("b", value_of("a: &x 1\nb: *x\n", "b"));
assert_file_edited(&file, "a: &x 1\nb: *x\n");
let file = YamlFile::from_str("a: 1\n").unwrap();
let mapping = file.document().unwrap().as_mapping().unwrap();
let tagged = value_of("c: !t v\n", "c");
mapping.set("b", tagged.as_tagged().unwrap());
assert_file_edited(&file, "a: 1\nb: !t v\n");
}
#[test]
fn rename_key_to_scalar_node() {
let src = YamlFile::from_str("k: v\n").unwrap();
let key = src
.document()
.unwrap()
.as_mapping()
.unwrap()
.keys()
.next()
.unwrap();
let file = YamlFile::from_str("a: 1\n").unwrap();
let mapping = file.document().unwrap().as_mapping().unwrap();
assert!(mapping.rename_key("a", key));
assert_file_edited(&file, "k: 1\n");
assert!(mapping.contains_key("k"));
}
fn shift(text: &str) -> String {
text.split('\n')
.enumerate()
.map(|(i, line)| match (i, line) {
(0, _) | (_, "") => line.to_string(),
_ => format!(" {line}"),
})
.collect::<Vec<_>>()
.join("\n")
}
#[track_caller]
fn check_set_node(src: &str, nested_mapping: bool, key: &str, source: &YamlNode, expected: &str) {
let file = YamlFile::from_str(src).unwrap();
let root = file.document().unwrap().as_mapping().unwrap();
let mapping = if nested_mapping { nested(&root) } else { root };
mapping.set(key, source);
assert_file_edited(&file, expected);
}
const BLOCK_VALUES: &[&str] = &[
" |\n x\n",
" >-\n x\n y\n",
" &a |\n x\n",
" &a\n x: 1\n",
" &a\n - a\n",
" !t\n x: 1\n",
" &a !t\n x: 1\n",
" !t |\n x\n",
" !!str >\n x\n",
"\n x: 1\n",
"\n - a\n",
];
#[test]
fn set_block_value_node() {
for value in BLOCK_VALUES {
let source = value_of(&format!("v:{value}"), "v");
check_set_node("k: 1\n", false, "k", &source, &format!("k:{value}"));
check_set_node("k: 1\n", false, "n", &source, &format!("k: 1\nn:{value}"));
check_set_node(
"m:\n k: 1\n",
true,
"k",
&source,
&format!("m:\n k:{}", shift(value)),
);
check_set_node(
"m:\n k: 1\n",
true,
"n",
&source,
&format!("m:\n k: 1\n n:{}", shift(value)),
);
}
}
#[test]
fn set_tagged_node_value_keeps_properties_and_comments() {
for value in [
" !t # c\n x: 1\n",
" !t &a\n x: 1\n",
" !t &a # c\n x: 1\n",
" !t\n # c\n x: 1\n",
" !t\n\n x: 1\n",
" !t &a 1\n",
" !t &a |\n x\n",
" !t | # c\n x\n",
] {
let source = value_of(&format!("v:{value}"), "v");
check_set_node("k: 1\n", false, "n", &source, &format!("k: 1\nn:{value}"));
check_set_node(
"m:\n k: 1\n",
true,
"n",
&source,
&format!("m:\n k: 1\n n:{}", shift(value)),
);
}
}
#[test]
fn set_block_value_node_keeps_key_line_comment() {
for (value, expected) in [
(" |\n x\n", "k: | # note\n x\n"),
(" &a |\n x\n", "k: &a | # note\n x\n"),
(" &a\n x: 1\n", "k: &a # note\n x: 1\n"),
(" !t\n x: 1\n", "k: !t # note\n x: 1\n"),
(" &a !t\n x: 1\n", "k: &a !t # note\n x: 1\n"),
(" !t |\n x\n", "k: !t | # note\n x\n"),
("\n x: 1\n", "k: # note\n x: 1\n"),
] {
let source = value_of(&format!("v:{value}"), "v");
for src in ["k: 1 # note\nj: 2\n", "k: # note\n - old\nj: 2\n"] {
check_set_node(src, false, "k", &source, &format!("{expected}j: 2\n"));
}
}
}
#[test]
fn builder_pair_block_value_node() {
for value in BLOCK_VALUES {
let source = value_of(&format!("v:{value}"), "v");
let doc = MappingBuilder::new()
.pair("n", &source)
.pair("j", 1)
.build_document();
let expected = format!("n:{value}j: 1\n");
assert_eq!(doc.to_string(), expected);
assert_parser_shaped(&doc);
}
}
#[test]
fn set_sequence_item_to_block_scalar() {
for (target, expected) in [
("- 1\n- 2\n", "- |\n text\n- 2\n"),
("- 1\n", "- |\n text\n"),
("a:\n - 1\n - 2\n", "a:\n - |\n text\n - 2\n"),
] {
let file = YamlFile::from_str(target).unwrap();
let root = file.document().unwrap();
let sequence = match root.as_sequence() {
Some(s) => s,
None => root.as_mapping().unwrap().get_sequence("a").unwrap(),
};
sequence.set(0, value_of("k: |\n text\n", "k"));
assert_file_edited(&file, expected);
}
}
#[test]
fn copy_multiline_flow_value() {
let list = || value_of("v: [a,\n b]\n", "v");
check_copy(
"m:\n k: 1\n",
|m| nested(m).set("n", list()),
"m:\n k: 1\n n: [a,\n b]\n",
);
check_copy(
"m:\n k: 1\n",
|m| nested(m).set("n", value_of("v: {a: 1, # c\n b: 2}\n", "v")),
"m:\n k: 1\n n: {a: 1, # c\n b: 2}\n",
);
check_copy(
"m:\n - k: 1\n",
|m| {
let item = m.get_sequence("m").unwrap().get(0).unwrap();
item.as_mapping().unwrap().set("n", list());
},
"m:\n - k: 1\n n: [a,\n b]\n",
);
check_copy(
"m:\n f: {x: 1}\n",
|m| nested(m).get_mapping("f").unwrap().set("n", list()),
"m:\n f: {x: 1, n: [a,\n b]}\n",
);
check_copy(
"m:\n f: [x]\n",
|m| nested(m).get_sequence("f").unwrap().push(list()),
"m:\n f: [x, [a,\n b]]\n",
);
let tagged = || value_of("v: !t [a,\n b]\n", "v");
check_copy("k: 1\n", |m| m.set("n", tagged()), "k: 1\nn: !t [a,\n b]\n");
check_copy(
"m:\n k: 1\n",
|m| nested(m).set("n", tagged()),
"m:\n k: 1\n n: !t [a,\n b]\n",
);
check_copy(
"m:\n - x\n",
|m| {
m.get_sequence("m").unwrap().set(0, tagged());
},
"m:\n - !t [a,\n b]\n",
);
}
#[test]
fn copy_commented_block_value_into_flow_collection() {
let block = || value_of("v:\n x: 1 # c\n y: 2\n", "v");
check_copy(
"a:\n f: {n: 1}\n",
|m| {
let a = m.get_mapping("a").unwrap();
a.get_mapping("f").unwrap().set("n", block());
},
"a:\n f: {n: {x: 1, # c\n y: 2}}\n",
);
check_copy(
"a:\n f: [1]\n",
|m| {
let a = m.get_mapping("a").unwrap();
a.get_sequence("f").unwrap().set(0, block());
},
"a:\n f: [{x: 1, # c\n y: 2}]\n",
);
}
#[track_caller]
fn assert_scalar_copied_to_nested_key(src: &str, target: &str, expected: &str) {
let file = check_copy(target, |m| nested(m).set("n", value_of(src, "v")), expected);
let mapping = file.document().unwrap().as_mapping().unwrap();
let copied = nested(&mapping).get("n").unwrap();
assert_eq!(
copied.as_scalar().unwrap().as_string(),
value_of(src, "v").as_scalar().unwrap().as_string()
);
}
#[test]
fn copy_multiline_scalar_to_nested_key() {
for (src, target, expected) in [
(
"v: \"a\n b\"\n",
"m:\n k: 1\n",
"m:\n k: 1\n n: \"a\n b\"\n",
),
("v: \"a\n b\"\n", "m:\n n: 1\n", "m:\n n: \"a\n b\"\n"),
(
"v: 'a\n\n b ''c'''\n",
"m:\n k: 1\n",
"m:\n k: 1\n n: 'a\n\n b ''c'''\n",
),
(
"v: \"a \\\n \\tb\n \n\n \\ c\\\n d\"\n",
"m:\n k: 1\n",
"m:\n k: 1\n n: \"a \\\n \\tb\n \n\n \\ c\\\n d\"\n",
),
(
"v: x\n y\n\n \n \t z\n",
"m:\n k: 1\n",
"m:\n k: 1\n n: x\n y\n\n \n \t z\n",
),
(
"v: \"a\r\n b\"\r\nw: x\r\n y\r\n",
"m:\n k: 1\n",
"m:\n k: 1\n n: \"a\r\n b\"\n",
),
] {
assert_scalar_copied_to_nested_key(src, target, expected);
}
}
#[test]
fn copy_nested_multiline_scalars_to_root_keys() {
let source = value_of("m:\n q: \"a\n b\"\n p: x\n y\n", "m");
let source = source.as_mapping().unwrap();
let file = check_copy(
"k: 1\n",
|m| {
m.set("q", source.get("q").unwrap());
m.set("p", source.get("p").unwrap());
},
"k: 1\nq: \"a\n b\"\np: x\n y\n",
);
let mapping = file.document().unwrap().as_mapping().unwrap();
assert_eq!(
mapping.get("q").unwrap().as_scalar().unwrap().as_string(),
"a b"
);
assert_eq!(
mapping.get("p").unwrap().as_scalar().unwrap().as_string(),
"x y"
);
}
#[test]
fn copy_multiline_scalar_into_collections() {
check_copy(
"m:\n - x\n",
|m| {
let seq = m.get_sequence("m").unwrap();
seq.set(0, value_of("v: \"a\n b\"\n", "v"));
seq.push(value_of("v: x\n y\n", "v"));
},
"m:\n - \"a\n b\"\n - x\n y\n",
);
check_copy(
"m:\n f: {x: 1}\n",
|m| {
let flow = nested(m).get_mapping("f").unwrap();
flow.set("n", value_of("v: \"a\n b\"\n", "v"));
},
"m:\n f: {x: 1, n: \"a\n b\"}\n",
);
check_copy(
"m:\n f: [x]\n",
|m| {
let flow = nested(m).get_sequence("f").unwrap();
flow.push(value_of("v: x\n y\n", "v"));
},
"m:\n f: [x, x\n y]\n",
);
check_copy(
"m:\n o:\n k: 1\n",
|m| {
let o = nested(m).get_mapping("o").unwrap();
o.insert_at_index_preserving(0, "k", value_of("v: \"a\n b\"\n", "v"));
},
"m:\n o:\n k: \"a\n b\"\n",
);
}
#[test]
fn copy_collection_holding_multiline_quoted_scalar() {
for (value, expected) in [
("v: [\"a\n\n b\"]\n", "m:\n k: 1\n n: [\"a\n\n b\"]\n"),
(
"v:\n q: 'a\n b'\n",
"m:\n k: 1\n n:\n q: 'a\n b'\n",
),
(
"v:\n- \"a\n b\"\n",
"m:\n k: 1\n n:\n - \"a\n b\"\n",
),
] {
check_copy(
"m:\n k: 1\n",
|m| nested(m).set("n", value_of(value, "v")),
expected,
);
}
}
#[test]
fn new_entry_with_multiline_explicit_key() {
let src = YamlFile::from_str("? \"a\n b\"\n: 1\n").unwrap();
let key = src
.document()
.unwrap()
.as_mapping()
.unwrap()
.keys()
.next()
.unwrap();
let entry = MappingEntry::new_at_indent(&key, 1, false, true, 2);
assert_eq!(entry.to_string(), "? \"a\n b\"\n : 1\n");
let file = YamlFile::from_str(&format!("m:\n {}", entry)).unwrap();
assert_eq!(Validator::new().validate_syntax(file.syntax()), vec![]);
let mapping = file.document().unwrap().as_mapping().unwrap();
let copied = nested(&mapping).keys().next().unwrap();
assert_eq!(copied.as_scalar().unwrap().as_string(), "a b");
}
#[test]
fn copy_multiline_flow_value_keeps_tabs() {
for (value, expected) in [
("[a,\n \tb]\n", "m:\n k: 1\n n: [a,\n \tb]\n"),
("[a,\n \t b]\n", "m:\n k: 1\n n: [a,\n \t b]\n"),
(
"{a: 1,\n\t\n b: 2}\n",
"m:\n k: 1\n n: {a: 1,\n \t\n b: 2}\n",
),
] {
check_copy(
"m:\n k: 1\n",
|m| nested(m).set("n", value_of(&format!("v: {value}"), "v")),
expected,
);
}
let source = value_of("m:\n v: [a,\n \tb]\n", "m");
let source = source.as_mapping().unwrap().get("v").unwrap();
check_copy("k: 1\n", |m| m.set("n", source), "k: 1\nn: [a,\n \tb]\n");
}
#[test]
fn copy_root_value_continued_at_column_zero() {
fn set_n(value: impl AsYaml) -> impl FnOnce(&Mapping) {
move |m| nested(m).set("n", value)
}
for (value, expected) in [
("[a,\nb]\n", "m:\n k: 1\n n: [a,\n b]\n"),
("[\n a\n]\n", "m:\n k: 1\n n: [\n a\n ]\n"),
] {
check_copy(
"m:\n k: 1\n",
set_n(root(value).as_sequence().unwrap()),
expected,
);
}
check_copy(
"m:\n k: 1\n",
set_n(root("{a: 1,\nb: 2}\n").as_mapping().unwrap()),
"m:\n k: 1\n n: {a: 1,\n b: 2}\n",
);
check_copy(
"m:\n k: 1\n",
set_n(root("!t [a,\nb]\n").as_tagged().unwrap()),
"m:\n k: 1\n n: !t [a,\n b]\n",
);
check_copy(
"m:\n k: 1\n",
set_n(root("\"a\nb\"\n").as_scalar().unwrap()),
"m:\n k: 1\n n: \"a\n b\"\n",
);
check_copy(
"k: 1\n",
|m| m.set("n", root("[a,\n\t b]\n").as_sequence().unwrap()),
"k: 1\nn: [a,\n \t b]\n",
);
check_copy(
"m:\n - x\n",
|m| {
let seq = m.get_sequence("m").unwrap();
seq.set(0, root("[a,\nb]\n").as_sequence().unwrap());
seq.push(root("[c,\nd]\n").as_sequence().unwrap());
},
"m:\n - [a,\n b]\n - [c,\n d]\n",
);
}
#[test]
fn set_value_to_unterminated_block_collection() {
for (source, expected) in [
("a: 1", "a:\n - x"),
("a: 1\n", "a:\n - x\n"),
("a: 1\nb: 2\n", "a:\n - x\nb: 2\n"),
] {
let seq = YamlFile::from_str("- x").unwrap();
let seq = seq.document().unwrap().as_sequence().unwrap();
check_copy(source, |m| m.set("a", seq), expected);
}
let map = YamlFile::from_str("k: v").unwrap();
let map = map.document().unwrap().as_mapping().unwrap();
check_copy("a: 1\n", |m| m.set("a", map), "a:\n k: v\n");
}
#[test]
fn set_implicitly_tagged_scalar_value() {
#[cfg_attr(not(feature = "base64"), allow(unused_mut))]
let mut cases = vec![
(
ScalarValue::timestamp("2001-12-14"),
"a: 1\nb: !!timestamp 2001-12-14\n",
),
(ScalarValue::regex("^a+$"), "a: 1\nb: !!regex ^a+$\n"),
];
#[cfg(feature = "base64")]
cases.push((ScalarValue::binary(b"hi"), "a: 1\nb: !!binary aGk=\n"));
for (value, expected) in cases {
let file = YamlFile::from_str("a: 1\n").unwrap();
let mapping = file.document().unwrap().as_mapping().unwrap();
mapping.set("b", &value);
assert_file_edited(&file, expected);
let file = YamlFile::from_str("a: 1\n").unwrap();
let mapping = file.document().unwrap().as_mapping().unwrap();
mapping.set("b", YamlValue::Scalar(value));
assert_file_edited(&file, expected);
}
}
#[test]
fn set_implicitly_tagged_scalar_key() {
for (src, expected) in [
(
"? !!timestamp 2001-12-14\n: 1\n",
"? !!timestamp 2001-12-14\n: 2\n",
),
(
"{!!timestamp 2001-12-14: 1}\n",
"{!!timestamp 2001-12-14: 2}\n",
),
("!!timestamp 2001-12-14: 1\n", "!!timestamp 2001-12-14: 2\n"),
] {
let file = YamlFile::from_str(src).unwrap();
let mapping = file.document().unwrap().as_mapping().unwrap();
mapping.set(ScalarValue::timestamp("2001-12-14"), 2);
assert_file_edited(&file, expected);
}
let file = YamlFile::from_str("a: 1\n").unwrap();
let mapping = file.document().unwrap().as_mapping().unwrap();
mapping.set(ScalarValue::timestamp("2001-12-14"), 2);
mapping.set(ScalarValue::regex("^a+$"), 3);
assert_file_edited(&file, "a: 1\n!!timestamp 2001-12-14: 2\n!!regex ^a+$: 3\n");
}
#[track_caller]
fn detached_text(node: &rowan::SyntaxNode<yaml_edit::Lang>) -> String {
let root = node.ancestors().last().unwrap();
debug::validate_tree(&root).unwrap();
root.to_string()
}
fn replaced_mapping(src: &str) -> (YamlFile, Mapping) {
let file = YamlFile::from_str(src).unwrap();
let root = file.document().unwrap().as_mapping().unwrap();
let inner = root.get_mapping("k").unwrap();
root.set("k", 1);
(file, inner)
}
fn replaced_sequence(src: &str) -> (YamlFile, Sequence) {
let file = YamlFile::from_str(src).unwrap();
let root = file.document().unwrap().as_mapping().unwrap();
let inner = root.get_sequence("k").unwrap();
root.set("k", 1);
(file, inner)
}
fn replaced_item(src: &str) -> (YamlFile, Sequence) {
let file = YamlFile::from_str(src).unwrap();
let root = file.document().unwrap().as_sequence().unwrap();
let inner = root.get(0).unwrap().as_sequence().cloned().unwrap();
root.set(0, 1);
(file, inner)
}
#[test]
fn drain_replaced_mapping_value() {
let (file, inner) = replaced_mapping("k:\n a: 1\n b: 2\nz: 1\n");
inner.remove("a");
inner.remove("b");
assert_eq!(file.to_string(), "k: 1\nz: 1\n");
assert_eq!(detached_text(inner.syntax()), "\n {}\n");
inner.set("n", 3);
assert_eq!(file.to_string(), "k: 1\nz: 1\n");
assert_eq!(detached_text(inner.syntax()), "\n n: 3\n");
let (file, inner) = replaced_mapping("k:\n a: 1\n # c\n b: 2\nz: 1\n");
inner.clear();
assert_eq!(file.to_string(), "k: 1\nz: 1\n");
assert_eq!(detached_text(inner.syntax()), "\n # c\n {}\n");
let file = YamlFile::from_str("k:\n a: 1\nz: 1\n").unwrap();
let root = file.document().unwrap().as_mapping().unwrap();
let inner = root.get_mapping("k").unwrap();
inner.remove("a");
root.set("k", 1);
assert_eq!(detached_text(inner.syntax()), "{}");
inner.set("n", 3);
assert_eq!(file.to_string(), "k: 1\nz: 1\n");
assert_eq!(detached_text(inner.syntax()), "\n n: 3\n");
}
#[test]
fn drain_replaced_sequence_value() {
let (file, inner) = replaced_sequence("k:\n - a\n - b\nz: 1\n");
inner.clear();
assert_eq!(file.to_string(), "k: 1\nz: 1\n");
assert_eq!(detached_text(inner.syntax()), "\n []\n");
inner.push(3);
assert_eq!(detached_text(inner.syntax()), "\n - 3\n");
}
#[test]
fn remove_last_entry_of_replaced_item() {
let (file, inner) = replaced_item("- - a\n - b\n- x\n");
inner.remove(1);
assert_eq!(file.to_string(), "- 1\n- x\n");
assert_eq!(detached_text(inner.syntax()), "- - a\n");
let (file, inner) = replaced_item("- - a\n # c\n - b\n- x\n");
inner.remove(1);
assert_eq!(detached_text(inner.syntax()), "- - a\n # c\n");
inner.remove(0);
assert_eq!(file.to_string(), "- 1\n- x\n");
assert_eq!(detached_text(inner.syntax()), "- []\n # c\n");
}
#[test]
fn remove_last_entry_of_removed_key() {
let file = YamlFile::from_str("? a: 1\n b: 2\n: v\nz: 1\n").unwrap();
let root = file.document().unwrap().as_mapping().unwrap();
let key = root.keys().next().unwrap();
let inner = key.as_mapping().cloned().unwrap();
root.remove(&key);
inner.remove("b");
assert_eq!(file.to_string(), "z: 1\n");
assert_eq!(detached_text(inner.syntax()), "? a: 1\n: v\n");
}
#[test]
fn refill_removed_explicit_key() {
let file = YamlFile::from_str("? a: 1\n b: 2\nz: 1\n").unwrap();
let root = file.document().unwrap().as_mapping().unwrap();
let key = root.keys().next().unwrap();
let inner = key.as_mapping().cloned().unwrap();
root.remove(&key);
inner.clear();
assert_eq!(detached_text(inner.syntax()), "? {}\n");
inner.set("n", 3);
assert_eq!(file.to_string(), "z: 1\n");
assert_eq!(detached_text(inner.syntax()), "? n: 3\n");
}
#[test]
fn document_as_value_is_its_root() {
let built = || {
MappingBuilder::new()
.pair("b", 2)
.pair("c", 3)
.build_document()
};
type Edit<'a> = &'a dyn Fn(&YamlFile);
let cases: [(&str, Edit, &str); 8] = [
(
"- a\n",
&|f| f.document().unwrap().as_sequence().unwrap().push(built()),
"- a\n- b: 2\n c: 3\n",
),
(
"x: 1\n",
&|f| {
f.document().unwrap().set("k", built());
},
"x: 1\nk:\n b: 2\n c: 3\n",
),
(
"x: [1]\n",
&|f| {
let doc = f.document().unwrap();
doc.get("x").unwrap().as_sequence().unwrap().push(built());
},
"x: [1, {b: 2, c: 3}]\n",
),
(
"- a\n",
&|f| {
let seq = f.document().unwrap().as_sequence().unwrap();
seq.push(SequenceBuilder::new().item("p").item("q").build_document());
},
"- a\n- - p\n - q\n",
),
(
"- a\n",
&|f| {
let seq = f.document().unwrap().as_sequence().unwrap();
seq.push(Document::from_str("--- |\n text\n").unwrap());
},
"- a\n- |\n text\n",
),
(
"k: 1\n",
&|f| {
let doc = f.document().unwrap();
doc.set("k", Document::from_str("!t\nb: 2\n").unwrap());
},
"k: !t\n b: 2\n",
),
(
"k: 1\n",
&|f| {
let doc = f.document().unwrap();
doc.set("k", Document::from_str("&x\nb: 2\n").unwrap());
},
"k: &x\n b: 2\n",
),
(
"- a\n",
&|f| {
let seq = f.document().unwrap().as_sequence().unwrap();
seq.push(Document::new());
},
"- a\n- null\n",
),
];
for (input, edit, expected) in cases {
let file = YamlFile::from_str(input).unwrap();
edit(&file);
assert_file_edited_valid(&file, expected);
}
for source in ["--- # c\nv # after\n# trail\n", "# lead\nv\n"] {
let file = YamlFile::from_str("x: 1\n").unwrap();
let doc = file.document().unwrap();
doc.set("k", Document::from_str(source).unwrap());
assert_file_edited_valid(&file, "x: 1\nk: v\n");
}
}
#[test]
fn insert_root_level_collection_from_other_document() {
let src = YamlFile::from_str("host: localhost\nport: 5432\n").unwrap();
let db = src.document().unwrap().as_mapping().unwrap();
let src = YamlFile::from_str("- a\n- b\n").unwrap();
let features = src.document().unwrap().as_sequence().unwrap();
let doc = YamlFile::from_str("name: project\nversion: 1\n")
.unwrap()
.document()
.unwrap();
doc.set("database", &db);
assert_edited(
&doc,
"name: project\nversion: 1\ndatabase:\n host: localhost\n port: 5432\n",
);
let doc = YamlFile::from_str("name: project\nversion: 1\n")
.unwrap()
.document()
.unwrap();
assert!(doc.insert_before("version", "database", &db));
assert!(doc.insert_after("name", "features", &features));
assert_edited(&doc, "name: project\nfeatures:\n- a\n- b\ndatabase:\n host: localhost\n port: 5432\nversion: 1\n");
}
#[test]
fn set_block_value_keeps_implicit_null() {
let src = YamlFile::from_str("s:\n ? q\n").unwrap();
let value = src.document().unwrap().get("s").unwrap();
let doc = YamlFile::from_str("a: 1\n").unwrap();
doc.document().unwrap().set("s", &value);
assert_file_edited(&doc, "a: 1\ns:\n ? q\n");
}
#[test]
fn set_plain_scalar_with_indicator_line() {
let source = YamlFile::from_str("x: b\n > c\n").unwrap();
let scalar = source
.document()
.unwrap()
.as_mapping()
.unwrap()
.get("x")
.unwrap()
.as_scalar()
.unwrap()
.clone();
let file = YamlFile::from_str("a: 1\nz: 2\n").unwrap();
file.document()
.unwrap()
.as_mapping()
.unwrap()
.set("a", scalar.clone());
assert_file_edited_valid(&file, "a: b\n > c\nz: 2\n");
let file = YamlFile::from_str("k:\n - 1\n").unwrap();
file.document()
.unwrap()
.as_mapping()
.unwrap()
.get_sequence("k")
.unwrap()
.push(scalar);
assert_file_edited_valid(&file, "k:\n - 1\n - b\n > c\n");
}
#[test]
fn set_copied_multi_line_scalar_with_cr_line_breaks() {
let source = yaml_edit::Document::from_str("k: 'x\r y\r\r z'\r").unwrap();
let value = source.as_mapping().unwrap().get("k").unwrap();
let file = YamlFile::from_str("a:\r b:\r c: 1\r").unwrap();
let doc = file.document().unwrap();
let a = doc.as_mapping().unwrap().get_mapping("a").unwrap();
a.get_mapping("b").unwrap().set("c", &value);
assert_file_edited_valid(&file, "a:\r b:\r c: 'x\r y\r\r z'\r");
}
#[test]
fn set_copied_flow_sequence_with_cr_line_breaks() {
let source = yaml_edit::Document::from_str("[a,\rb]\r").unwrap();
let value = source.as_sequence().unwrap();
let file = YamlFile::from_str("a:\r b:\r c: 1\r").unwrap();
let doc = file.document().unwrap();
let a = doc.as_mapping().unwrap().get_mapping("a").unwrap();
a.get_mapping("b").unwrap().set("c", &value);
assert_file_edited_valid(&file, "a:\r b:\r c: [a,\r b]\r");
}
#[test]
fn copy_multi_line_scalars() {
let src = Document::from_str("p: b\n c\n d\nq: |\n x\n y\n").unwrap();
let src = src.as_mapping().unwrap();
for (key, expected) in [
("p", "top:\n k: b\n c\n d\n"),
("q", "top:\n k: |\n x\n y\n"),
] {
let doc = Document::from_str("top:\n k: 1\n").unwrap();
let top = doc.as_mapping().unwrap().get_mapping("top").unwrap();
top.set("k", src.get(key).unwrap());
assert_edited(&doc, expected);
let reparsed = Document::from_str(expected).unwrap();
let reparsed = reparsed.as_mapping().unwrap().get_mapping("top").unwrap();
assert_eq!(
reparsed.get("k").unwrap().as_scalar().unwrap().as_string(),
src.get(key).unwrap().as_scalar().unwrap().as_string()
);
}
}
#[test]
fn copy_tagged_node() {
let src = YamlFile::from_str("t: !t &x\n a: 1\nm:\n f: !t [1,\n 2]\n").unwrap();
let src = src.document().unwrap().as_mapping().unwrap();
let block = src.get("t").unwrap().as_tagged().unwrap().clone();
let flow = src
.get_mapping("m")
.unwrap()
.get("f")
.unwrap()
.as_tagged()
.unwrap()
.clone();
for (value, set, pushed) in [
(
&block,
"top:\n k: !t &x\n a: 1\n",
"- 0\n- !t &x\n a: 1\n",
),
(
&flow,
"top:\n k: !t [1,\n 2]\n",
"- 0\n- !t [1,\n 2]\n",
),
] {
let doc = YamlFile::from_str("top:\n k: 0\n")
.unwrap()
.document()
.unwrap();
let top = doc.as_mapping().unwrap().get_mapping("top").unwrap();
top.set("k", value);
assert_edited(&doc, set);
let doc = YamlFile::from_str("- 0\n").unwrap().document().unwrap();
doc.as_sequence().unwrap().push(value);
assert_edited(&doc, pushed);
}
let doc = YamlFile::from_str("f: {a: 1}\n")
.unwrap()
.document()
.unwrap();
let f = doc.as_mapping().unwrap().get_mapping("f").unwrap();
f.set("b", &block);
assert_edited(&doc, "f: {a: 1, b: !t &x {a: 1}}\n");
}
#[test]
fn copy_tagged_collection() {
let src = YamlFile::from_str("s: !t\n a: 1\n b: 2\nf: !t [1,\n 2]\n").unwrap();
let src = src.document().unwrap();
let src = src.as_mapping().unwrap();
for (key, set, pushed) in [
(
"s",
"top:\n k: !t\n a: 1\n b: 2\n",
"- 0\n- !t\n a: 1\n b: 2\n",
),
(
"f",
"top:\n k: !t [1,\n 2]\n",
"- 0\n- !t [1,\n 2]\n",
),
] {
let value = src.get(key).unwrap();
let doc = YamlFile::from_str("top:\n k: 0\n")
.unwrap()
.document()
.unwrap();
let top = doc.as_mapping().unwrap().get_mapping("top").unwrap();
top.set("k", value.clone());
assert_edited(&doc, set);
let doc = YamlFile::from_str("- 0\n").unwrap().document().unwrap();
doc.as_sequence().unwrap().push(value);
assert_edited(&doc, pushed);
}
}
#[test]
fn tagged_block_value_is_reindented() {
let src = Document::from_str(
"a: !!map\n k: 1\n j: 2\nx:\n b: !!seq\n - 1\n - 2\nc: !!str foo\n",
)
.unwrap();
let root = src.as_mapping().unwrap();
let map = root.get("a").unwrap();
let seq = root.get_mapping("x").unwrap().get("b").unwrap();
let scalar = root.get("c").unwrap();
let target = |doc: &Document| -> Sequence {
doc.as_sequence()
.or_else(|| doc.as_mapping()?.get_sequence("s"))
.unwrap()
};
type Op<'a> = Box<dyn Fn(&Document) + 'a>;
let cases: Vec<(&str, Op, &str)> = vec![
(
"s:\n - a\n",
Box::new(|d: &Document| target(d).push(&map)),
"s:\n - a\n - !!map\n k: 1\n j: 2\n",
),
(
"- a\n",
Box::new(|d: &Document| target(d).push(&map)),
"- a\n- !!map\n k: 1\n j: 2\n",
),
(
"s:\n - a\n",
Box::new(|d: &Document| target(d).insert(0, &map)),
"s:\n - !!map\n k: 1\n j: 2\n - a\n",
),
(
"s:\n - a\n - b\n",
Box::new(|d: &Document| assert!(target(d).set(0, &map))),
"s:\n - !!map\n k: 1\n j: 2\n - b\n",
),
(
"- a\n",
Box::new(|d: &Document| target(d).push(&seq)),
"- a\n- !!seq\n - 1\n - 2\n",
),
(
"- a\n",
Box::new(|d: &Document| target(d).push(&scalar)),
"- a\n- !!str foo\n",
),
];
for (input, op, expected) in cases {
let doc = Document::from_str(input).unwrap();
op(&doc);
assert_edited(&doc, expected);
}
let doc = Document::from_str("o:\n b: 1\n").unwrap();
let inner = doc.as_mapping().unwrap().get_mapping("o").unwrap();
inner.set("b", &map);
inner.set("c", &map);
assert_edited(
&doc,
"o:\n b: !!map\n k: 1\n j: 2\n c: !!map\n k: 1\n j: 2\n",
);
}
#[test]
fn copy_tagged_plain_scalar_into_flow() {
for (src, flow) in [
("v: !t a, b\n", "!t \"a, b\""),
("v: !t a]b\n", "!t \"a]b\""),
("v: !t a{b}\n", "!t \"a{b}\""),
("v: !t &x a, b\n", "!t &x \"a, b\""),
("v: !t\n a, b\n", "!t \"a, b\""),
("v: !t a:b\n", "!t a:b"),
("v: !t 'a, b'\n", "!t 'a, b'"),
] {
let src_doc = YamlFile::from_str(src).unwrap().document().unwrap();
let value = src_doc.as_mapping().unwrap().get("v").unwrap();
for (target, edit, expected) in [
("[x]\n", "set", format!("[{flow}]\n")),
("[x]\n", "push", format!("[x, {flow}]\n")),
("{k: x}\n", "set", format!("{{k: {flow}}}\n")),
("a: {k: [x]}\n", "nested", format!("a: {{k: [{flow}]}}\n")),
] {
let doc = YamlFile::from_str(target).unwrap().document().unwrap();
match edit {
"set" if target.starts_with('[') => {
doc.as_sequence().unwrap().set(0, &value);
}
"set" => doc.as_mapping().unwrap().set("k", &value),
"push" => doc.as_sequence().unwrap().push(&value),
_ => {
let inner = doc.as_mapping().unwrap().get_mapping("a").unwrap();
inner.get_sequence("k").unwrap().set(0, &value);
}
}
assert_eq!(doc.to_string(), expected, "{src:?} {edit} {target:?}");
assert_parser_shaped(&doc);
let read = YamlFile::from_str(&doc.to_string())
.unwrap()
.document()
.unwrap();
let item = match edit {
"set" if target.starts_with('[') => read.as_sequence().unwrap().get(0),
"set" => read.as_mapping().unwrap().get("k"),
"push" => read.as_sequence().unwrap().get(1),
_ => read
.as_mapping()
.unwrap()
.get_mapping("a")
.unwrap()
.get_sequence("k")
.unwrap()
.get(0),
};
assert!(
yaml_edit::yaml_eq(&item.unwrap(), &value),
"{src:?} {edit} {target:?}"
);
}
}
}
#[test]
fn copy_tagged_plain_scalar_into_block() {
let src_doc = YamlFile::from_str("v: !t a, b\n")
.unwrap()
.document()
.unwrap();
let value = src_doc.as_mapping().unwrap().get("v").unwrap();
let doc = YamlFile::from_str("k: x\n").unwrap().document().unwrap();
doc.as_mapping().unwrap().set("k", &value);
assert_edited(&doc, "k: !t a, b\n");
let doc = YamlFile::from_str("- x\n").unwrap().document().unwrap();
doc.as_sequence().unwrap().set(0, &value);
assert_edited(&doc, "- !t a, b\n");
}