mod common;
use common::{
assert_cst_ok, assert_file_cst_ok, assert_file_edited_valid, assert_file_parser_shaped,
assert_parser_shaped, check_renders, count_nodes, edit_root, keys, parse_clean, render,
render_documents, root_mapping,
};
use rowan::ast::AstNode;
use std::str::FromStr;
use yaml_edit::validator::{Rule, Validator};
use yaml_edit::{debug, Alias, Document, Mapping, ScalarValue, SyntaxKind, YamlFile, YamlNode};
#[track_caller]
fn render_root(yaml: &str) -> String {
let file = YamlFile::from_str(yaml).unwrap_or_else(|e| panic!("{yaml:?}: {e}"));
assert_eq!(file.to_string(), yaml);
assert_eq!(file.documents().count(), 1, "{yaml:?}");
let doc = file.document().unwrap();
let root = doc
.syntax()
.children()
.find_map(YamlNode::from_syntax)
.unwrap();
render(&root)
}
fn scalar(node: &YamlNode) -> String {
node.as_scalar().unwrap().as_string()
}
#[test]
fn complex_keys() {
let spec_example = r#"
? - Detroit Tigers
- Chicago Cubs
:
- 2001-07-23
? [ New York Yankees,
Atlanta Braves ]
: [ 2001-07-02, 2001-08-12,
2001-08-14 ]
"#;
for (yaml, expected) in [
(
r#"
? key1
: value1
? key2
: value2
"#,
"map{key1: value1, key2: value2}",
),
(
r#"
? simple
:
- item1
- item2
? another
:
nested:
key: value
"#,
"map{simple: seq[item1, item2], another: map{nested: map{key: value}}}",
),
(
r#"
[a, b]: value1
[c, d]: value2
"#,
"map{[a, b]: value1, [c, d]: value2}",
),
(
r#"
{name: John, age: 30}: developer
{name: Jane, age: 25}: designer
"#,
"map{{name: John, age: 30}: developer, {name: Jane, age: 25}: designer}",
),
(
r#"
? [a, b, c]
: value1
? [d, e, f]
: value2
"#,
"map{[a, b, c]: value1, [d, e, f]: value2}",
),
(
r#"
? {x: 1, y: 2}
: point1
? {x: 3, y: 4}
: point2
"#,
"map{{x: 1, y: 2}: point1, {x: 3, y: 4}: point2}",
),
(
r#"
simple: value1
? explicit
: value2
[list, key]: value3
{map: key}: value4
"#,
"map{simple: value1, explicit: value2, [list, key]: value3, {map: key}: value4}",
),
(
r#"
? [a, [b, c]]
: nested_list
? {outer: {inner: value}}
: nested_map
"#,
"map{[a, [b, c]]: nested_list, {outer: {inner: value}}: nested_map}",
),
(
r#"
? |
multiline
key
: value
"#,
"map{multiline\nkey\n: value}",
),
(
r#"
? key_without_value
? another_key
: has_value
"#,
"map{key_without_value: ~, another_key: has_value}",
),
(
r#"
[a, b]: value1
{x: 1}: value2
? explicit
: value3
"#,
"map{[a, b]: value1, {x: 1}: value2, explicit: value3}",
),
(
spec_example,
"map{seq[Detroit Tigers, Chicago Cubs]: seq[2001-07-23], \
[New York Yankees, Atlanta Braves]: [2001-07-02, 2001-08-12, 2001-08-14]}",
),
(
r#"
{ [a, b]: value1, {x: 1}: value2 }
"#,
"{[a, b]: value1, {x: 1}: value2}",
),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
}
}
#[test]
fn scalar_keys_found_by_get_string() {
for (yaml, key, value) in [
("\n? key1\n: value1\n? key2\n: value2\n", "key1", "value1"),
("\n? key1\n: value1\n? key2\n: value2\n", "key2", "value2"),
(
"\nsimple: value1\n? explicit\n: value2\n[list, key]: value3\n{map: key}: value4\n",
"simple",
"value1",
),
(
"\nsimple: value1\n? explicit\n: value2\n[list, key]: value3\n{map: key}: value4\n",
"explicit",
"value2",
),
(
"\n? key_without_value\n? another_key\n: has_value\n",
"another_key",
"has_value",
),
(
"\n[a, b]: value1\n{x: 1}: value2\n? explicit\n: value3\n",
"explicit",
"value3",
),
("[1]: 2\nk: v\n", "k", "v"),
] {
let doc = Document::from_str(yaml).unwrap();
assert_eq!(doc.get_string(key), Some(value.to_string()), "{yaml:?}");
}
}
#[test]
fn keys_after_complex_key() {
for (yaml, expected) in [
("[1]: 2\nk: v\n", "map{[1]: 2, k: v}"),
("[1]: 2\n&a k: v\n", "map{[1]: 2, k: v}"),
("&k [a]: v\n&j {b: 1}: w\n", "map{[a]: v, {b: 1}: w}"),
("[1]: 2\n? x\n: y\nz: 1\n", "map{[1]: 2, x: y, z: 1}"),
("a:\n [1]: 2\nj: w\n", "map{a: map{[1]: 2}, j: w}"),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
}
}
#[test]
fn sequence_entry_after_complex_key_is_an_error() {
assert!("[1]: 2\n- x\n".parse::<YamlFile>().is_err());
}
#[test]
fn nested_explicit_key_with_empty_value() {
for (yaml, expected) in [
("k:\n ? a\n :\n b: c\n", "map{k: map{a: ~, b: c}}"),
("- ? a\n :\n b: c\n", "seq[map{a: ~, b: c}]"),
("k:\n ? a\n :\n - x\n", "map{k: map{a: seq[x]}}"),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
}
}
#[test]
fn block_scalar_in_explicit_entry_does_not_swallow_next_entry() {
for (yaml, expected) in [
("? a\n: |\nb: 1\n", "map{a: , b: 1}"),
("? a\n: >\n? b\n", "map{a: , b: ~}"),
("? |\nb: 1\n", "map{: ~, b: 1}"),
("x:\n ? a\n : |\n b: 1\n", "map{x: map{a: , b: 1}}"),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
}
}
#[test]
fn comment_after_explicit_indicator() {
let yaml = "? # c\nb: 1\n";
let doc = yaml.parse::<YamlFile>().unwrap();
assert_eq!(
debug::tree_to_string(doc.syntax()),
r##"DOCUMENT
MAPPING
MAPPING_ENTRY
QUESTION: "?"
WHITESPACE: " "
KEY
COMMENT: "# c"
SCALAR
NULL: ""
VALUE
SCALAR
NULL: ""
NEWLINE: "\\n"
MAPPING_ENTRY
KEY
SCALAR
STRING: "b"
COLON: ":"
WHITESPACE: " "
VALUE
SCALAR
INT: "1"
NEWLINE: "\\n"
"##
);
for (yaml, expected) in [
("? # c\nb: 1\n", "map{~: ~, b: 1}"),
("? a\n: # c\nb: 1\n", "map{a: ~, b: 1}"),
("? # c", "map{~: ~}"),
("? a\n: # c", "map{a: ~}"),
("- ? # c\n- 1\n", "seq[map{~: ~}, 1]"),
("- ? a\n : # c\n- 1\n", "seq[map{a: ~}, 1]"),
("? # c\n k: v\n: x\n", "map{map{k: v}: x}"),
("? # c\n- a\n: v\n", "map{seq[a]: v}"),
("? a\n: # c\n- b\n", "map{a: seq[b]}"),
("? a\n: # c\n - b\n", "map{a: seq[b]}"),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
}
}
#[test]
fn set_multiline_string_key_is_quoted() {
use yaml_edit::{ScalarValue, YamlValue};
let plain = ScalarValue::string("x\ny\n");
let tagged = ScalarValue::string("x\ny\n").with_tag("!t");
let tagged_value = YamlValue::tagged("!t", YamlValue::Scalar(ScalarValue::string("x\ny\n")));
for (key, expected) in [
(YamlValue::Scalar(plain), "a: 1\n\"x\\ny\\n\": 2\n"),
(YamlValue::Scalar(tagged), "a: 1\n!t \"x\\ny\\n\": 2\n"),
(tagged_value, "a: 1\n!t \"x\\ny\\n\": 2\n"),
] {
let file: YamlFile = "a: 1\n".parse().unwrap();
let mapping = root_mapping(&file);
mapping.set(&key, 2);
assert_eq!(file.to_string(), expected);
assert_file_cst_ok(&file);
assert_eq!(mapping.len(), 2);
assert_eq!(mapping.get(&key).unwrap().to_string(), "2");
}
}
#[test]
fn lone_annotation_in_explicit_entry() {
let yaml = "? !!str\nb: 1\n";
let doc = yaml.parse::<YamlFile>().unwrap();
assert_eq!(
debug::tree_to_string(doc.syntax()),
r##"DOCUMENT
MAPPING
MAPPING_ENTRY
QUESTION: "?"
WHITESPACE: " "
KEY
TAGGED_NODE
TAG: "!!str"
SCALAR
NULL: ""
VALUE
SCALAR
NULL: ""
NEWLINE: "\\n"
MAPPING_ENTRY
KEY
SCALAR
STRING: "b"
COLON: ":"
WHITESPACE: " "
VALUE
SCALAR
INT: "1"
NEWLINE: "\\n"
"##
);
for (yaml, expected) in [
("? !!str\nb: 1\n", "map{!!str ~: ~, b: 1}"),
("? &a\nb: 1\n", "map{~: ~, b: 1}"),
("? !!str &a\nb: 1\n", "map{!!str ~: ~, b: 1}"),
("? a\n: !!str\nb: 1\n", "map{a: !!str ~, b: 1}"),
("? a\n: &x\nb: 1\n", "map{a: ~, b: 1}"),
("- ? !!str\n b: 1\n", "seq[map{!!str ~: ~, b: 1}]"),
("- ? a\n : &x\n b: 1\n", "seq[map{a: ~, b: 1}]"),
("? !!map\n? b\n: 1\n", "map{!!map ~: ~, b: 1}"),
("? a\n: &x\n? b\n: 1\n", "map{a: ~, b: 1}"),
("? !!map\n b: 1\n: v\n", "map{!!map map{b: 1}: v}"),
("? a\n: &x\n b: 1\n", "map{a: map{b: 1}}"),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
}
}
#[test]
fn annotated_sequence_at_explicit_indicator_column() {
for (yaml, expected) in [
("? a\n: !!seq\n- b\n", "map{a: !!seq seq[b]}"),
("- ? a\n : !!seq\n- b\n", "seq[map{a: !!seq ~}, b]"),
("? !!seq\n- a\n: v\n", "map{!!seq seq[a]: v}"),
("? &x\n- a\n: v\n", "map{seq[a]: v}"),
("- ? !!seq\n - a\n : v\n", "seq[map{!!seq seq[a]: v}]"),
("- ? &x\n - a\n : v\n", "seq[map{seq[a]: v}]"),
("- ? !!seq\n- a\n", "seq[map{!!seq ~: ~}, a]"),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
}
for yaml in [
"? !!seq\n- a\n: v\n",
"? &x\n- a\n: v\n",
"- ? !!seq\n - a\n : v\n",
"- ? &x\n - a\n : v\n",
] {
let doc = yaml.parse::<YamlFile>().unwrap();
assert_file_cst_ok(&doc);
assert_eq!(
Validator::new()
.validate_syntax(doc.syntax())
.into_iter()
.map(|v| v.message)
.collect::<Vec<_>>(),
Vec::<String>::new(),
"{yaml:?}"
);
}
}
#[test]
fn explicit_key_after_implicit_with_indentless_sequence_value() {
for (yaml, expected) in [
("a: 1\n? b\n:\n- x\n", "map{a: 1, b: seq[x]}"),
("a: 1\n? b\n: # c\n- x\n", "map{a: 1, b: seq[x]}"),
("a: 1\n? b\n:\n# c\n- x\n", "map{a: 1, b: seq[x]}"),
("a: 1\n? b\n:\n- x\nc: 2\n", "map{a: 1, b: seq[x], c: 2}"),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
assert_file_cst_ok(&yaml.parse::<YamlFile>().unwrap());
}
}
#[test]
fn value_left_of_a_nested_explicit_key() {
for (yaml, expected) in [
("? ? \n: v\n", "map{map{~: ~}: v}"),
("? ? a\n: v\n", "map{map{a: ~}: v}"),
("? ? a\n : b\n: v\n", "map{map{a: b}: v}"),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
}
}
#[test]
fn comment_lines_after_a_bare_explicit_key() {
let yaml = "?\n # c\nk: v\n";
assert_eq!(render_root(yaml), "map{~: ~, k: v}");
let file = yaml.parse::<YamlFile>().unwrap();
root_mapping(&file).reorder_fields(["k"]);
assert_eq!(file.to_string(), " # c\nk: v\n?\n");
assert_file_parser_shaped(&file);
}
#[test]
fn edit_inside_key_making_it_a_duplicate() {
let file = "? [a]\n: 1\n? []\n: 2\n".parse::<YamlFile>().unwrap();
let key = root_mapping(&file).iter().next().unwrap().0;
assert_eq!(scalar(&key.as_sequence().unwrap().pop().unwrap()), "a");
assert_eq!(file.to_string(), "? []\n: 1\n? []\n: 2\n");
assert_file_parser_shaped(&file);
let reparsed = file.to_string().parse::<YamlFile>().unwrap();
let violations: Vec<_> = Validator::new()
.validate_syntax(reparsed.syntax())
.into_iter()
.map(|v| (v.rule, v.to_string()))
.collect();
assert_eq!(
violations,
vec![(
Rule::DuplicateKeys,
"[ERROR] 3:1: Duplicate key: \"[]\" (semantically equal to \"[]\") (DuplicateKeys)"
.to_string()
)]
);
}
const THREE_REFERENCES: &str = r#"
Reference: First
Reference: Second
Reference: Third
"#;
const TWO_REFERENCES: &str = r#"
Reference: First
Reference: Second
"#;
fn values_of(mapping: &Mapping, key: &str) -> Vec<String> {
mapping
.find_all_entries_by_key(key)
.map(|entry| scalar(&entry.value_node().unwrap()))
.collect()
}
#[test]
fn find_all_entries_by_key() {
let yaml = r#"
Reference: First
Reference: Second
Other: Value
Reference: Third
"#;
let doc = Document::from_str(yaml).unwrap();
let mapping = doc.as_mapping().unwrap();
assert_eq!(
values_of(&mapping, "Reference"),
vec!["First", "Second", "Third"]
);
}
#[test]
fn remove_all_but_first_occurrence() {
let positions = r#"
Name: Slugify
Reference: First
Archive: GitHub
Reference: Second
Version: 1.0
Reference: Third
"#;
for (source, expected) in [
(THREE_REFERENCES, "\nReference: First\n"),
(
positions,
"\nName: Slugify\nReference: First\nArchive: GitHub\nVersion: 1.0\n",
),
] {
let doc = Document::from_str(source).unwrap();
let mapping = doc.as_mapping().unwrap();
let refs: Vec<_> = mapping.find_all_entries_by_key("Reference").collect();
assert_eq!(refs.len(), 3);
for entry in refs.into_iter().skip(1) {
entry.remove();
assert_cst_ok(&doc);
}
assert_eq!(values_of(&mapping, "Reference"), vec!["First"]);
assert_eq!(doc.to_string(), expected);
}
}
#[test]
fn remove_nth_occurrence() {
for (source, key, n, removed, remaining, expected) in [
(
THREE_REFERENCES,
"Reference",
0,
Some("First"),
&["Second", "Third"][..],
"\nReference: Second\nReference: Third\n",
),
(
THREE_REFERENCES,
"Reference",
1,
Some("Second"),
&["First", "Third"],
"\nReference: First\nReference: Third\n",
),
(
THREE_REFERENCES,
"Reference",
2,
Some("Third"),
&["First", "Second"],
TWO_REFERENCES,
),
(
TWO_REFERENCES,
"Reference",
2,
None,
&["First", "Second"],
TWO_REFERENCES,
),
(
TWO_REFERENCES,
"NotFound",
0,
None,
&["First", "Second"],
TWO_REFERENCES,
),
] {
let doc = Document::from_str(source).unwrap();
let mapping = doc.as_mapping().unwrap();
let entry = mapping.remove_nth_occurrence(key, n);
assert_cst_ok(&doc);
assert_eq!(
entry.map(|e| scalar(&e.value_node().unwrap())).as_deref(),
removed
);
assert_eq!(values_of(&mapping, "Reference"), remaining);
assert_eq!(doc.to_string(), expected);
}
}
type MappingEdit = fn(&Mapping);
#[track_caller]
fn edit_nested(source: &str, key: &str, edit: impl FnOnce(&Mapping)) -> String {
let file = YamlFile::from_str(source).unwrap();
edit(&root_mapping(&file).get_mapping(key).unwrap());
assert_file_parser_shaped(&file);
file.to_string()
}
#[track_caller]
fn edit_root_valid(source: &str, edit: impl FnOnce(&Mapping)) -> String {
let text = edit_root(source, edit);
assert_valid(&text);
text
}
#[track_caller]
fn assert_valid(text: &str) {
let doc = Document::from_str(text).unwrap();
assert_eq!(Validator::new().validate(&doc), vec![], "{text:?}");
}
#[test]
fn explicit_key_value_set() {
assert_eq!(
edit_root("? key1\n: value1\n? key2\n: value2\n", |m| {
assert_eq!(
render(&YamlNode::Mapping(m.clone())),
"map{key1: value1, key2: value2}"
);
m.set("key1", "newvalue");
assert_eq!(scalar(&m.get("key1").unwrap()), "newvalue");
}),
"? key1\n: newvalue\n? key2\n: value2\n"
);
assert_eq!(
edit_root("? existing\n: value\n", |m| {
assert_eq!(
render(&YamlNode::Mapping(m.clone())),
"map{existing: value}"
);
m.set("newkey", "newvalue");
assert_eq!(
render(&YamlNode::Mapping(m.clone())),
"map{existing: value, newkey: newvalue}"
);
}),
"? existing\n: value\n? newkey\n: newvalue\n"
);
}
#[test]
fn nested_explicit_key_entry_keeps_indent() {
let cases: [(&str, MappingEdit, &str); 3] = [
(
"outer:\n ? a\n : 1\n",
|m| m.set("b", "9"),
"outer:\n ? a\n : 1\n ? b\n : '9'\n",
),
(
"outer:\n ? a\n : 1\n ? c\n : 2\n",
|m| m.insert_at_index(1, "b", "9"),
"outer:\n ? a\n : 1\n ? b\n : '9'\n ? c\n : 2\n",
),
(
"outer:\n ? b\n : 1\n ? c\n : 2\n",
|m| m.insert_at_index(0, "a", "9"),
"outer:\n ? a\n : '9'\n ? b\n : 1\n ? c\n : 2\n",
),
];
for (source, edit, expected) in cases {
assert_eq!(edit_nested(source, "outer", edit), expected);
}
}
#[test]
fn explicit_key_reorder_fields_keeps_line_breaks() {
let doc = Document::from_str("? a\n: 1\n? b\n: 2\n").unwrap();
doc.as_mapping().unwrap().reorder_fields(["b"]);
assert_cst_ok(&doc);
assert_eq!(doc.to_string(), "? b\n: 2\n? a\n: 1\n");
}
#[test]
fn set_then_rename_keeps_an_explicit_key_on_its_own_line() {
for (source, nested, expected) in [
("? a\n: 1\n", None, "? a\n: 1\n? z\n: '0'\n"),
(
"m:\n ? a\n : 1\n",
Some("m"),
"m:\n ? a\n : 1\n ? z\n : '0'\n",
),
] {
let file = YamlFile::from_str(source).unwrap();
let root = root_mapping(&file);
let m = nested.map_or(root.clone(), |k| root.get_mapping(k).unwrap());
m.set("u", "0");
assert_file_parser_shaped(&file);
assert!(m.rename_key("u", "z"), "{source:?}");
assert_file_parser_shaped(&file);
assert_eq!(file.to_string(), expected);
}
}
#[test]
fn renaming_a_plain_key_stays_inline() {
let doc = Document::from_str("a: 1\nb: 2\n").unwrap();
let mapping = doc.as_mapping().unwrap();
assert!(mapping.rename_key("a", "z"));
assert_cst_ok(&doc);
assert_eq!(doc.to_string(), "z: 1\nb: 2\n");
}
#[test]
fn explicit_key_entry_leaves_line_break_to_mapping() {
let source = "? a\n: 1\n? c\n: 3\n";
let cases: [(&str, MappingEdit, &str); 12] = [
("? a\n: 1\n", |m| m.set("b", 2), "? a\n: 1\n? b\n: 2\n"),
(
source,
|m| m.insert_at_index(0, "b", 2),
"? b\n: 2\n? a\n: 1\n? c\n: 3\n",
),
(
source,
|m| m.insert_at_index(1, "b", 2),
"? a\n: 1\n? b\n: 2\n? c\n: 3\n",
),
(
source,
|m| m.insert_at_index(2, "b", 2),
"? a\n: 1\n? c\n: 3\n? b\n: 2\n",
),
(
source,
|m| m.set_with_field_order("b", 2, ["a", "b", "c"]),
"? a\n: 1\n? b\n: 2\n? c\n: 3\n",
),
(
source,
|m| m.set_with_field_order("b", 2, ["b", "a", "c"]),
"? b\n: 2\n? a\n: 1\n? c\n: 3\n",
),
("? a\n: 1", |m| m.set("b", 2), "? a\n: 1\n? b\n: 2"),
(
"? a\n: 1",
|m| m.set_with_field_order("b", 2, ["a", "b"]),
"? a\n: 1\n? b\n: 2",
),
(
"? b\n: 2\n? a\n: 1",
|m| m.set_with_field_order("c", 3, ["b", "c", "a"]),
"? b\n: 2\n? c\n: 3\n? a\n: 1",
),
(
"? a\n:\n x: 1\n? c\n: 3\n",
|m| m.set("a", 5),
"? a\n: 5\n? c\n: 3\n",
),
(
"? a\n: b # c",
|m| m.set("k", 1),
"? a\n: b # c\n? k\n: 1",
),
(
"? a\n: b # c",
|m| assert!(m.insert_after("a", "k", 1)),
"? a\n: b # c\n? k\n: 1",
),
];
for (source, edit, expected) in cases {
assert_eq!(edit_root(source, edit), expected, "{source:?}");
}
}
#[test]
fn key_after_comment_renamed_from_sequence_at_question_column() {
let rename_first = |m: &Mapping| {
let key = m.keys().next().unwrap();
assert!(m.rename_key(key, "new"));
};
assert_eq!(
edit_root("? # c\n- a\n: 1\n", rename_first),
"? # c\n new\n: 1\n"
);
assert_eq!(
edit_root("? # c\n - a\n: 1\n", rename_first),
"? # c\n new\n: 1\n"
);
assert_eq!(
edit_nested("x:\n ? # c\n - a\n : 1\n", "x", rename_first),
"x:\n ? # c\n new\n : 1\n"
);
}
#[test]
fn value_set_for_unterminated_block_key() {
let set_first = |m: &Mapping| {
let key = m.keys().next().unwrap();
m.set(key, 1);
};
assert_eq!(edit_root("? - a", set_first), "? - a\n: 1");
assert_eq!(edit_root("? x: y", set_first), "? x: y\n: 1");
assert_eq!(edit_root("? a", set_first), "? a\n: 1");
}
fn first_item(source: &str) -> YamlNode {
Document::from_str(source)
.unwrap()
.as_sequence()
.unwrap()
.get(0)
.unwrap()
}
fn value_of_k(source: &str) -> YamlNode {
Document::from_str(source)
.unwrap()
.as_mapping()
.unwrap()
.get("k")
.unwrap()
}
#[test]
fn key_needing_explicit_form_inserted_relative_to_another() {
let key = Document::from_str("- 1 # one\n- 2\n")
.unwrap()
.as_sequence()
.unwrap();
let expected = "a: 1\n? [1, # one\n 2]\n: 2\nc: 3\n";
let edits: [&dyn Fn(&Mapping); 5] = [
&|m| m.insert_at_index(1, &key, 2),
&|m| assert!(m.insert_after("a", &key, 2)),
&|m| assert!(m.move_after("a", &key, 2)),
&|m| assert!(m.insert_before("c", &key, 2)),
&|m| assert!(m.move_before("c", &key, 2)),
];
for edit in edits {
assert_eq!(edit_root("a: 1\nc: 3\n", edit), expected);
}
}
#[test]
fn explicit_key_entry_inserted_and_moved() {
let three = "? a\n: 1\n? b\n: 2\n? c\n: 3\n";
let cases: [(&str, MappingEdit, &str); 14] = [
(
"? a\n: 1\n? c\n: 3\n",
|m| assert!(m.insert_after("a", "b", 2)),
three,
),
(
"? a\n: 1\n? c\n: 3\n",
|m| assert!(m.move_after("a", "b", 2)),
three,
),
(
"? a\n: 1\n? c\n: 3\n",
|m| assert!(m.insert_before("c", "b", 2)),
three,
),
(
"? a\n: 1\n? c\n: 3\n",
|m| assert!(m.move_before("c", "b", 2)),
three,
),
(
"? a\n: 1\n",
|m| assert!(m.insert_after("a", "b", 2)),
"? a\n: 1\n? b\n: 2\n",
),
(
"? a\n: 1",
|m| assert!(m.move_after("a", "b", 2)),
"? a\n: 1\n? b\n: 2",
),
(
"? a\n: 1\n",
|m| assert!(m.insert_before("a", "b", 2)),
"? b\n: 2\n? a\n: 1\n",
),
(
three,
|m| assert!(m.move_after("c", "a", 9)),
"? b\n: 2\n? c\n: 3\n? a\n: 9\n",
),
(
three,
|m| assert!(m.move_before("a", "c", 9)),
"? c\n: 9\n? a\n: 1\n? b\n: 2\n",
),
(
three,
|m| assert!(m.move_after("a", "c", 9)),
"? a\n: 1\n? c\n: 9\n? b\n: 2\n",
),
(
three,
|m| assert!(m.move_before("c", "a", 9)),
"? b\n: 2\n? a\n: 9\n? c\n: 3\n",
),
(
"? a\n: b # c\nz: 1\n",
|m| assert!(m.insert_after("a", "k", 1)),
"? a\n: b # c\n? k\n: 1\nz: 1\n",
),
(
"? a\n: 1\n? b\n: 2\n",
|m| m.insert_at_index_preserving(0, "a", 5),
"? a\n: 5\n? b\n: 2\n",
),
(
"? a\n: 1\n? b\n: 2\n",
|m| m.insert_at_index_preserving(0, "b", 5),
"? a\n: 1\n? b\n: 5\n",
),
];
for (source, edit, expected) in cases {
assert_eq!(edit_root(source, edit), expected, "{source:?}");
}
assert_eq!(
edit_root("? a\n: 1\n? b\n: 2", |m| m
.insert_at_index_preserving(0, "b", 5)),
"? a\n: 1\n? b\n: 5"
);
}
#[test]
fn nested_explicit_key_entry_inserted_and_moved() {
let source = "outer:\n ? a\n : 1\n ? c\n : 3\nz: 0\n";
let cases: [(MappingEdit, &str); 5] = [
(
|m| assert!(m.insert_after("a", "b", 2)),
"outer:\n ? a\n : 1\n ? b\n : 2\n ? c\n : 3\nz: 0\n",
),
(
|m| assert!(m.insert_after("c", "b", 2)),
"outer:\n ? a\n : 1\n ? c\n : 3\n ? b\n : 2\nz: 0\n",
),
(
|m| assert!(m.insert_before("a", "b", 2)),
"outer:\n ? b\n : 2\n ? a\n : 1\n ? c\n : 3\nz: 0\n",
),
(
|m| assert!(m.move_after("c", "a", 9)),
"outer:\n ? c\n : 3\n ? a\n : 9\nz: 0\n",
),
(
|m| assert!(m.move_before("a", "c", 9)),
"outer:\n ? c\n : 9\n ? a\n : 1\nz: 0\n",
),
];
for (edit, expected) in cases {
assert_eq!(edit_nested(source, "outer", edit), expected);
}
}
#[test]
fn entry_replaced_at_index_keeps_mapping_layout() {
for (source, key, expected) in [
("a: 1\nb: 2\n", "a", "a: 5\nb: 2\n"),
("a: 1\nb: 2", "b", "a: 1\nb: 5"),
("m:\n a: 1\n b: 2\n", "a", "m:\n a: 5\n b: 2\n"),
("m:\n a: 1\n b: 2\n", "b", "m:\n a: 1\n b: 5\n"),
("m:\n a: 1\nz: 0\n", "a", "m:\n a: 5\nz: 0\n"),
("{k: v, m: 2}\n", "k", "{k: 5, m: 2}\n"),
("{k: v, m: 2}\n", "m", "{k: v, m: 5}\n"),
("{k: v,\n m: 2}\n", "k", "{k: 5,\n m: 2}\n"),
] {
let file = YamlFile::from_str(source).unwrap();
let root = root_mapping(&file);
let m = root.get_mapping("m").unwrap_or(root);
m.insert_at_index_preserving(1, key, 5);
assert_file_parser_shaped(&file);
assert_eq!(file.to_string(), expected, "{source:?}");
}
}
#[test]
fn explicit_key_commented_value_replaced_by_scalar() {
for (source, expected) in [
("? a\n: # c\n x: 1\n", "? a\n: 5 # c\n"),
("? a\n: # c\n x: 1\nb: 2\n", "? a\n: 5 # c\nb: 2\n"),
("? a\n: 1 # c\n? b\n: 2\n", "? a\n: 5 # c\n? b\n: 2\n"),
] {
assert_eq!(edit_root(source, |m| m.set("a", 5)), expected);
}
assert_eq!(
edit_nested("m:\n ? a\n : # c\n - 1\n b: 2\n", "m", |m| m
.set("a", 5)),
"m:\n ? a\n : 5 # c\n b: 2\n"
);
}
#[test]
fn key_renamed_to_one_spanning_lines_becomes_explicit() {
let quoted = first_item("- \"x\n y\"\n");
let commented = first_item("- - 1 # one\n - 2\n");
for (source, key, expected) in [
("a: 1\nb: 2\n", "ed, "? \"x\n y\"\n: 1\nb: 2\n"),
("b: 2\na: 1\n", "ed, "b: 2\n? \"x\n y\"\n: 1\n"),
(
"a: 1 # c\nb: 2\n",
"ed,
"? \"x\n y\"\n: 1 # c\nb: 2\n",
),
(
"a:\n x: 1\nb: 2\n",
"ed,
"? \"x\n y\"\n:\n x: 1\nb: 2\n",
),
(
"a: 1\nb: 2\n",
&commented,
"? [1, # one\n 2]\n: 1\nb: 2\n",
),
("{a: 1, b: 2}\n", "ed, "{\"x\n y\": 1, b: 2}\n"),
] {
assert_eq!(
edit_root_valid(source, |m| assert!(m.rename_key("a", key))),
expected,
"{source:?}"
);
}
let key = first_item("- \"x\n y\"\n");
assert_eq!(
edit_nested("m:\n a: 1\n b: 2\n", "m", |m| assert!(
m.rename_key("a", &key)
)),
"m:\n ? \"x\n y\"\n : 1\n b: 2\n"
);
}
#[test]
fn new_key_spanning_lines_is_explicit() {
let quoted = first_item("- \"x\n y\"\n");
let flow = first_item("- [1,\n 2]\n");
for (source, key, expected) in [
("a: 1\n", "ed, "a: 1\n? \"x\n y\"\n: 5\n"),
("a: 1\n", &flow, "a: 1\n? [1,\n 2]\n: 5\n"),
("{a: 1}\n", "ed, "{a: 1, \"x\n y\": 5}\n"),
] {
assert_eq!(edit_root_valid(source, |m| m.set(key, 5)), expected);
}
}
#[test]
fn block_key_written_in_flow_form() {
let block = value_of_k("k: |\n x\n");
let tagged_block = value_of_k("k: !!str |\n x\n");
let sequence = value_of_k("k:\n - x\n - y\n");
let tagged_sequence = value_of_k("k: !t\n - x\n");
for (key, expected) in [
(&block, "\"x\\n\": 1\nb: 2\n"),
(&tagged_block, "!!str \"x\\n\": 1\nb: 2\n"),
(&sequence, "[x, y]: 1\nb: 2\n"),
(&tagged_sequence, "!t [x]: 1\nb: 2\n"),
] {
assert_eq!(
edit_root_valid("a: 1\nb: 2\n", |m| assert!(m.rename_key("a", key))),
expected
);
assert_eq!(
edit_root_valid("b: 2\n", |m| m.set(key, 1)),
format!("b: 2\n{}", &expected[..expected.len() - "b: 2\n".len()])
);
}
}
#[test]
fn tagged_key_in_explicit_and_flow_entries() {
let tagged = value_of_k("k: !t\n - x\n");
assert_eq!(
edit_root_valid("? a\n: 1\n", |m| assert!(m.rename_key("a", &tagged))),
"? !t [x]\n: 1\n"
);
assert_eq!(
edit_root_valid("{a: 1}\n", |m| assert!(m.rename_key("a", &tagged))),
"{!t [x]: 1}\n"
);
assert_eq!(
edit_root_valid("{a: 1}\n", |m| m.set(&tagged, 2)),
"{a: 1, !t [x]: 2}\n"
);
}
#[test]
fn set_key_written_in_flow_form() {
let set = |members: &[&str]| -> std::collections::BTreeSet<String> {
members.iter().map(|m| m.to_string()).collect()
};
let nested: std::collections::BTreeSet<_> = [set(&["p", "q"])].into_iter().collect();
assert_eq!(
edit_root_valid("a: 1\n", |m| m.set("s", &nested)),
"a: 1\ns: !!set\n !!set {p: null, q: null}: null\n"
);
assert_eq!(
edit_root_valid("a: 1\n", |m| assert!(m.rename_key("a", set(&["p"])))),
"!!set {p: null}: 1\n"
);
}
#[test]
fn flow_explicit_key_renamed() {
assert_eq!(
edit_root_valid("{? a : 1, b: 2}\n", |m| assert!(m.rename_key("a", "z"))),
"{? z : 1, b: 2}\n"
);
}
#[test]
fn flow_empty_explicit_key_renamed() {
for (source, expected) in [
("{a: 1, ? }\n", "{a: 1, ? z}\n"),
("{a: 1,\n?\n}\n", "{a: 1,\n? z\n}\n"),
("{? : 1}\n", "{? z: 1}\n"),
("{?\n a : 1}\n", "{? z : 1}\n"),
] {
let text = edit_root_valid(source, |m| {
let key = m.keys().last().unwrap();
assert!(m.rename_key(&key, "z"));
});
assert_eq!(text, expected, "{source:?}");
}
}
#[test]
fn renamed_key_keeps_its_anchor() {
for (source, expected) in [
("&x a: 1\nb: *x\n", "&x z: 1\nb: *x\n"),
("&x !!str a: 1\nb: *x\n", "&x !!str z: 1\nb: *x\n"),
("? &x a\n: 1\nb: *x\n", "? &x z\n: 1\nb: *x\n"),
("? !!str &x a\n: 1\nb: *x\n", "? &x !!str z\n: 1\nb: *x\n"),
("{&x a: 1, b: *x}\n", "{&x z: 1, b: *x}\n"),
("{? &x a : 1, b: *x}\n", "{? &x z : 1, b: *x}\n"),
] {
assert_eq!(
edit_root_valid(source, |m| assert!(m.rename_key("a", "z"))),
expected,
"{source:?}"
);
}
let quoted = first_item("- \"x\n y\"\n");
assert_eq!(
edit_root_valid("&x a: 1\nb: *x\n", |m| assert!(m.rename_key("a", "ed))),
"? &x \"x\n y\"\n: 1\nb: *x\n"
);
}
#[test]
fn renamed_key_keeps_its_tag_only_if_still_the_new_key() {
assert_eq!(
edit_root_valid("&x !!str a: 1\nb: *x\n", |m| assert!(m.rename_key("a", 5))),
"&x 5: 1\nb: *x\n"
);
assert_eq!(
edit_root_valid("!t a: 1\n", |m| assert!(
m.rename_key(value_of_k("k: !t a\n"), "z")
)),
"z: 1\n"
);
assert_eq!(
edit_root_valid("{!!str \"a\": 1, \"b\": 2}\n", |m| assert!(
m.rename_key("a", "z")
)),
"{!!str \"z\": 1, \"b\": 2}\n"
);
}
#[test]
fn renamed_key_given_as_annotated_node() {
let tagged = value_of_k("k: !t [q]\n");
let anchored = value_of_k("k: &y q\n");
for (source, key, expected) in [
("&x !!str a: 1\nb: *x\n", &tagged, "&x !t [q]: 1\nb: *x\n"),
("&x a: 1\nb: *x\n", &anchored, "&x q: 1\nb: *x\n"),
("a: 1\n", &anchored, "&y q: 1\n"),
] {
assert_eq!(
edit_root_valid(source, |m| assert!(m.rename_key("a", key))),
expected
);
}
}
#[test]
fn renamed_explicit_key_keeps_layout() {
for (source, expected) in [
("? - a\n: b\n", "? z\n: b\n"),
("? |\n a\n: b\n", "? z\n: b\n"),
("?\n: b\n", "? z\n: b\n"),
("?\n- a\n- b\n:\n- c\n", "? z\n:\n- c\n"),
("? - a\n", "? z\n"),
("? - a\nc: d\n", "? z\nc: d\n"),
("m:\n ? - a\n : b\n", "m:\n ? z\n : b\n"),
("m:\n ?\n : b\n", "m:\n ? z\n : b\n"),
("? # c\n a\n: b\n", "? # c\n z\n: b\n"),
("? &x # c\n a\n: b\n", "? &x # c\n z\n: b\n"),
("? # c\n - a\n: b\n", "? # c\n z\n: b\n"),
("? # c\n: b\n", "? z # c\n: b\n"),
("{? # c\n a : b}\n", "{? # c\n z : b}\n"),
("? - a\n\nc: d\n", "? z\n\nc: d\n"),
("? - a\n\n: b\nc: d\n", "? z\n\n: b\nc: d\n"),
("? x: 1\n\n# c\nc: d\n", "? z\n\n# c\nc: d\n"),
("k:\n ? - a\n\n c: d\n", "k:\n ? z\n\n c: d\n"),
] {
let file = YamlFile::from_str(source).unwrap();
let root = root_mapping(&file);
let m = root
.get_mapping("m")
.or_else(|| root.get_mapping("k"))
.unwrap_or(root);
let key = m.keys().next().unwrap();
assert!(m.rename_key(&key, "z"), "{source:?}");
assert_file_parser_shaped(&file);
assert_eq!(file.to_string(), expected, "{source:?}");
}
for expected in [
"? z\n: b\n",
"? z\n:\n- c\n",
"? z\n",
"? z\nc: d\n",
"m:\n ? z\n : b\n",
"? # c\n z\n: b\n",
"? &x # c\n z\n: b\n",
"? z # c\n: b\n",
"{? # c\n z : b}\n",
] {
assert_valid(expected);
}
}
#[test]
fn set_value_ending_in_block_key_without_colon() {
assert_eq!(
edit_root("complex1:\n ? - a", |m| m
.set("complex1", m.get("complex1").unwrap())),
"complex1:\n ? - a"
);
assert_eq!(
edit_root("complex1:\n ? - a", |m| m.set("b", 1)),
"complex1:\n ? - a\nb: 1"
);
}
#[test]
fn rename_empty_explicit_key_before_tab_line() {
let out = edit_root("?\r\t\n", |m| {
let key = m.keys().next().unwrap();
assert!(m.rename_key(key, ""));
});
assert_eq!(out, "? ''\r\t\n");
}
#[test]
fn explicit_key_entries() {
check_renders(&[
(
"\n? explicit_key\n: explicit_value\n? another_key\n",
"map{explicit_key: explicit_value, another_key: ~}",
),
("? k\n: v\n", "map{k: v}"),
(
"keys: !!set\n ? a\n ? b\n",
"map{keys: !!set map{a: ~, b: ~}}",
),
("map:\n ? complex\n : v\n", "map{map: map{complex: v}}"),
("? a\n: 1\n? b\n: 2\n", "map{a: 1, b: 2}"),
("m:\n ? a\n : 1\nuf: v\n", "map{m: map{a: 1}, uf: v}"),
("- ? k\n : v\n", "seq[map{k: v}]"),
("- x\n- ? k\n : v\n", "seq[x, map{k: v}]"),
("- a: 1\n ? c\n", "seq[map{a: 1, c: ~}]"),
("- ? k\n ? c\n", "seq[map{k: ~, c: ~}]"),
("- ?\n j: 1\n", "seq[map{~: ~, j: 1}]"),
("- ?\n- a\n", "seq[map{~: ~}, a]"),
("?\nk:\n?\na:\n", "map{~: ~, k: ~, ~: ~, a: ~}"),
("?\n::\n?\na:\n", "map{~: ~, :: ~, ~: ~, a: ~}"),
(
"? a\n: 1\nb: 2\n? c\n: 3\nd: 4\n",
"map{a: 1, b: 2, c: 3, d: 4}",
),
]);
}
#[test]
fn explicit_key_holding_a_collection() {
check_renders(&[
(
"\n[1, 2]: sequence_key\n{a: b}: mapping_key\n",
"map{[1, 2]: sequence_key, {a: b}: mapping_key}",
),
(
"? - Detroit Tigers\n - Chicago Cubs\n:\n - 2001-07-23\n",
"map{seq[Detroit Tigers, Chicago Cubs]: seq[2001-07-23]}",
),
(
"---\n?\n- a\n- b\n:\n- c\n- d\n",
"map{seq[a, b]: seq[c, d]}",
),
("?\n- a\n:\n- c\n", "map{seq[a]: seq[c]}"),
("? k\n:\n- c\n", "map{k: seq[c]}"),
("?\n- a\n", "map{seq[a]: ~}"),
("?\n\n- a\n: v\n", "map{seq[a]: v}"),
("?\n\n-\n", "map{seq[~]: ~}"),
("k:\n\n- a\n", "map{k: seq[a]}"),
("?\n\n\n- a\n: v\n", "map{seq[a]: v}"),
("?\n - a\n: v\n", "map{seq[a]: v}"),
("?\n s: 1\n e: 2\n: v\n", "map{map{s: 1, e: 2}: v}"),
("?\n s:\n e:\n", "map{map{s: ~, e: ~}: ~}"),
(
"?\n a: 1\n b: 2\n c: 3\n: v\n",
"map{map{a: 1, b: 2, c: 3}: v}",
),
("?\n j: 1\n", "map{map{j: 1}: ~}"),
("- ?\n - a\n", "seq[map{seq[a]: ~}]"),
("-\n ? - c\n- z\n", "seq[map{seq[c]: ~}, z]"),
("? k: 1\nk2: 2\n", "map{map{k: 1}: ~, k2: 2}"),
("- ? : x\n", "seq[map{map{~: x}: ~}]"),
("? : x\n", "map{map{~: x}: ~}"),
("- ? []: x\n", "seq[map{map{[]: x}: ~}]"),
]);
}
#[test]
fn explicit_key_boundaries() {
for (yaml, kind, count, expected) in [
(
"-\n ? &e\n : &a\n",
SyntaxKind::MAPPING,
1,
"seq[map{~: ~}]",
),
("-\n ? &d\n", SyntaxKind::MAPPING, 1, "seq[map{~: ~}]"),
("? a\n: 1\n", SyntaxKind::MAPPING, 1, "map{a: 1}"),
("-\n &c : &a\n", SyntaxKind::MAPPING, 1, "seq[map{~: ~}]"),
(
"k: &a\n x: 1\n",
SyntaxKind::MAPPING,
2,
"map{k: map{x: 1}}",
),
(
"-\n ? &d\n-\n ? e\n",
SyntaxKind::SEQUENCE,
1,
"seq[map{~: ~}, map{e: ~}]",
),
(
"-\n ? &d\n-\n x\n",
SyntaxKind::SEQUENCE,
1,
"seq[map{~: ~}, x]",
),
(
"-\n ? a\n-\n ? b\n",
SyntaxKind::SEQUENCE,
1,
"seq[map{a: ~}, map{b: ~}]",
),
("k: &a\n- x\n", SyntaxKind::SEQUENCE, 1, "map{k: seq[x]}"),
(
"? a: 1\n: b: 2\n",
SyntaxKind::MAPPING,
3,
"map{map{a: 1}: map{b: 2}}",
),
(
"- ? earth: blue\n : moon: white\n",
SyntaxKind::MAPPING,
3,
"seq[map{map{earth: blue}: map{moon: white}}]",
),
("a: 1\nb: 2\n", SyntaxKind::MAPPING, 1, "map{a: 1, b: 2}"),
(
"{a: 1}:\n# c\n b: 2\n",
SyntaxKind::MAPPING,
3,
"map{{a: 1}: map{b: 2}}",
),
(
"{a: 1}:\n b: 2\n",
SyntaxKind::MAPPING,
3,
"map{{a: 1}: map{b: 2}}",
),
("{a: 1}: v\n", SyntaxKind::MAPPING, 2, "map{{a: 1}: v}"),
] {
assert_eq!(render_documents(yaml), [expected], "{yaml:?}");
assert_eq!(count_nodes(&parse_clean(yaml), kind), count, "{yaml:?}");
}
}
#[test]
fn explicit_key_is_a_mapping_with_an_empty_key() {
for yaml in ["- ? : x\n", "? : x\n", "- ? []: x\n"] {
let file = YamlFile::from_str(yaml).unwrap();
assert_eq!(file.to_string(), yaml);
let doc = file.document().unwrap();
let outer = match doc.as_sequence() {
Some(seq) => seq.get(0).unwrap().as_mapping().unwrap().clone(),
None => doc.as_mapping().unwrap(),
};
assert_eq!(outer.len(), 1, "{yaml:?}");
let entry = outer.entries().next().unwrap();
let key = entry.key_node().unwrap();
let key = key.as_mapping().unwrap();
assert_eq!(key.len(), 1, "{yaml:?}");
let inner = key.entries().next().unwrap();
assert_eq!(inner.value_node().unwrap().to_string(), "x", "{yaml:?}");
let value = entry.value_node().unwrap();
assert!(value.as_scalar().unwrap().is_null(), "{yaml:?}");
}
}
#[test]
fn lines_before_explicit_key_collection_are_the_keys() {
let slot = |text: &str, kind: &str| {
let file = YamlFile::from_str(text).unwrap();
assert_file_cst_ok(&file);
let tree = debug::tree_to_string(file.syntax());
let start = tree.find(&format!(" {kind}\n")).unwrap();
tree[start..]
.lines()
.skip(1)
.take_while(|l| l.starts_with(" "))
.collect::<Vec<_>>()
.join("\n")
};
for rest in [
"\n\n- a\n- b\n",
"\n\n\n- a\n",
"\n# c\n- a\n",
"\n # c\n- a\n",
"\n# c\n\n- a\n",
" # c\n\n- a\n",
"\n\n - a\n",
"\n \n - a\n",
"\n# c\n - a\n",
"\n\n a: 1\n",
"\n# c\n a: 1\n",
" !t\n\n- a\n",
" &x\n\n- a\n",
" &x\n\n a: 1\n",
] {
let value = slot(&format!("k:{rest}"), "VALUE");
assert_eq!(slot(&format!("?{rest}"), "KEY"), value, "{rest:?}");
assert_eq!(slot(&format!("? x\n:{rest}"), "VALUE"), value, "{rest:?}");
}
}
#[test]
fn null_keys() {
check_renders(&[
(": v\n", "map{~: v}"),
(": a\n: b\n", "map{~: a, ~: b}"),
("- : v\n", "seq[map{~: v}]"),
("- : v\n", "seq[map{~: v}]"),
("- : v\n- : w\n", "seq[map{~: v}, map{~: w}]"),
("- : v\n k: w\n", "seq[map{~: v, k: w}]"),
]);
let file = YamlFile::from_str(": v\n").unwrap();
let mapping = root_mapping(&file);
let (key, value) = mapping.iter().next().unwrap();
assert_eq!(scalar(&key), "");
assert_eq!(scalar(&value), "v");
}
#[test]
fn null_key_and_empty_string_key_are_distinct() {
let doc = Document::from_str(": a\n").unwrap();
let mapping = doc.as_mapping().unwrap();
assert_eq!(scalar(&mapping.get(ScalarValue::null()).unwrap()), "a");
assert!(mapping.get("").is_none());
let doc = Document::from_str("\"\": b\n").unwrap();
let mapping = doc.as_mapping().unwrap();
assert_eq!(scalar(&mapping.get("").unwrap()), "b");
assert!(mapping.get(ScalarValue::null()).is_none());
for src in ["null: a\n", "~: a\n"] {
let doc = Document::from_str(src).unwrap();
let mapping = doc.as_mapping().unwrap();
assert!(mapping.get(ScalarValue::null()).is_some(), "{src:?}");
}
}
#[test]
fn properties_on_implicit_key() {
for (yaml, expected) in [
("!!str a: b\n", "map{!!str a: b}"),
("!!str &a1 \"foo\": bar\n", "map{!!str foo: bar}"),
("&anchor a: b\n", "map{a: b}"),
] {
assert_eq!(render_root(yaml), expected, "{yaml:?}");
}
}
#[test]
fn explicit_key_produces_zero_width_null_value() {
let yaml = YamlFile::from_str("? a\n").unwrap();
let doc = yaml.document().unwrap();
let mapping = doc.as_mapping().expect("should parse as a mapping");
assert_eq!(mapping.len(), 1);
let entry = mapping.entries().next().expect("one entry");
let syntax = entry.syntax();
let terminal = syntax
.last_token()
.expect("MAPPING_ENTRY has at least one token");
assert_eq!(terminal.kind(), SyntaxKind::NEWLINE);
assert_eq!(terminal.parent().as_ref(), Some(syntax));
let null = terminal.prev_token().expect("a token before the NEWLINE");
assert!(
null.kind() == SyntaxKind::NULL && null.text().is_empty(),
"expected zero-width NULL before the line break, got {:?} = {:?}",
null.kind(),
null.text()
);
}
fn top_level_keys(src: &str) -> Vec<String> {
keys(&YamlFile::from_str(src).unwrap())
}
#[test]
fn key_after_nested_explicit_key_block() {
for (src, keys) in [
("m:\n ? a\n : 1\nuf: v\n", &["m", "uf"][..]),
("m:\n ? a\n : 1\n ? b\n : 2\nuf: v\n", &["m", "uf"][..]),
("m:\n ? a\n : 1\nn:\n ? b\n : 2\n", &["m", "n"][..]),
("m:\n ? a\n : \n n: 1\nz: 9\n", &["m", "z"][..]),
("m:\n ? a\n : 1\nz: 9\n", &["m", "z"][..]),
(
"m:\n ? a\n : 1\n plain: 2\nouter: 3\n",
&["m", "outer"][..],
),
(
"a:\n b:\n ? c\n : 1\n d: 2\ne: 3\n",
&["a", "e"][..],
),
("keys: !!set\n ? a\n ? b\nz: 9\n", &["keys", "z"][..]),
("? a\n: 1\nplain: v\n", &["a", "plain"][..]),
] {
assert_eq!(top_level_keys(src), keys, "{src:?}");
}
let parsed = YamlFile::from_str("m:\n ? a\n : 1\nuf: v\n").unwrap();
let mapping = root_mapping(&parsed);
assert_eq!(mapping.get("m").unwrap().to_string(), "? a\n : 1\n");
assert_eq!(mapping.get("uf").unwrap().to_string(), "v");
}
#[test]
fn comment_line_after_nested_explicit_key_block() {
for (src, comments) in [
("x:\n a: b\n # c\ny: 1\n", &["# c"][..]),
("x:\n ? a\n : b\n # c\ny: 1\n", &["# c"]),
(
"x:\n ? a\n : b\n\n # c\n # d\n\ny: 1\n",
&["# c", "# d"],
),
] {
let file = YamlFile::from_str(src).unwrap();
let root: Vec<String> = root_mapping(&file)
.syntax()
.children_with_tokens()
.filter(|c| c.kind() == SyntaxKind::COMMENT)
.map(|c| c.to_string())
.collect();
assert_eq!(root, comments, "{src:?}");
assert_eq!(top_level_keys(src), ["x", "y"], "{src:?}");
}
}
#[test]
fn compact_explicit_key_mapping_as_value() {
let src = "? h\n: ? b\n : x\n a: 1\nc: 2\n";
assert_eq!(top_level_keys(src), ["h", "c"]);
let parsed = YamlFile::from_str(src).unwrap();
let h = parsed.document().unwrap().as_mapping().unwrap().get("h");
let keys: Vec<String> = h
.unwrap()
.as_mapping()
.unwrap()
.keys()
.map(|k| k.to_string())
.collect();
assert_eq!(keys, ["b", "a"]);
}
fn checked_value(src: &str) -> yaml_edit::YamlValue {
let file = YamlFile::from_str(src).expect("should parse");
debug::validate_tree(file.syntax()).expect("tree should be valid");
let doc = file.document().expect("should have a document");
assert_eq!(
yaml_edit::validator::Validator::new().validate(&doc),
vec![]
);
yaml_edit::YamlValue::from_document(&doc).expect("should convert")
}
#[test]
fn explicit_value_left_of_nested_explicit_key() {
for (src, flow) in [
("? - ? a\n: b\n", "? [{? a}]\n: b\n"),
("? - ? - a\n: b\n", "? [{? [a]}]\n: b\n"),
(
"x:\n ? - ? -\n : - c\n",
"x:\n ? [{? [null]}]\n : [c]\n",
),
] {
assert_eq!(checked_value(src), checked_value(flow), "{src:?}");
}
}
#[test]
fn compact_sequence_key_after_wide_indicators() {
for (src, compact) in [
("- ? -", "- ? -"),
("- ? -\n", "- ? -\n"),
("- ? - a\n", "- ? - a\n"),
(
"- ? - a\n - b\n : c\n",
"- ? - a\n - b\n : c\n",
),
] {
assert_eq!(checked_value(src), checked_value(compact), "{src:?}");
}
}
#[test]
fn explicit_value_scalar_continuation() {
for (src, content, offset) in [
("? a\n: 1\nb\n", "b", 8),
("? a\n: 1\n[a, b]\n", "[", 8),
("- ? a\n : 1\n b\n", "b", 14),
("? a\n: 1\n\nb\n", "b", 9),
] {
let errors: Vec<(String, u32)> = yaml_edit::Parse::parse_yaml(src)
.positioned_errors()
.iter()
.map(|e| (e.message.clone(), e.range.start))
.collect();
assert_eq!(
errors,
[(
format!("Content after the document could not be parsed: {content:?}"),
offset
)],
"{src:?}"
);
}
for (src, expected) in [
("? a\n: 1\n b\n", "{a: 1 b}"),
("- ? a\n : 1\n b\n", "[{a: 1 b}]"),
("? a\n: 1\n? b\n", "{a: 1, b: }"),
] {
assert_eq!(checked_value(src), checked_value(expected), "{src:?}");
}
}
#[test]
fn flow_collection_key_after_explicit_key_entry() {
for (src, expected) in [
("? a\n: 1\n[a, b]: 2\n", "{a: 1, [a, b]: 2}"),
("? a\n[a, b]: 2\n", "{a: , [a, b]: 2}"),
("? a\n: 1\n{a: 1}: 2\n", "{a: 1, {a: 1}: 2}"),
("? a\n: 1\n&x [a, b]: 2\n", "{a: 1, &x [a, b]: 2}"),
("? a\n: 1\n!!seq [a, b]: 2\n", "{a: 1, !!seq [a, b]: 2}"),
("- ? a\n : 1\n [a, b]: 2\n", "[{a: 1, [a, b]: 2}]"),
("k:\n ? a\n : 1\n [a, b]: 2\n", "{k: {a: 1, [a, b]: 2}}"),
(
"? a\n: 1\n[a, b]: 2\n? c\n: 3\n[d]: 4\n",
"{a: 1, [a, b]: 2, c: 3, [d]: 4}",
),
] {
assert_eq!(checked_value(src), checked_value(expected), "{src:?}");
}
}
#[test]
fn entry_after_compact_mapping_explicit_value() {
for (src, expected) in [
("? a\n: b: c\n? d\n: e\n", "{a: {b: c}, d: e}"),
("? a\n: b: c\nd: e\n", "{a: {b: c}, d: e}"),
("x: 1\n? a\n: b: c\n? d\n: e\n", "{x: 1, a: {b: c}, d: e}"),
("? a\n: b: c\n f: g\n? d\n: e\n", "{a: {b: c, f: g}, d: e}"),
("? a\n: !!map b: c\n? d\n: e\n", "{a: {b: c}, d: e}"),
("? a\n: &x b: c\n? d\n: e\n", "{a: {b: c}, d: e}"),
("? a\n: {x: 1}: c\n? d\n: e\n", "{a: {{x: 1}: c}, d: e}"),
("- ? a\n : b: c\n ? d\n : e\n", "[{a: {b: c}, d: e}]"),
(
"k:\n ? a\n : b: c\n ? d\n : e\n",
"{k: {a: {b: c}, d: e}}",
),
] {
assert_eq!(checked_value(src), checked_value(expected), "{src:?}");
}
}
fn rename_in(src: &str, old: &str, new: &str) -> (bool, String) {
let yaml = YamlFile::from_str(src).unwrap();
let renamed = yaml
.document()
.unwrap()
.as_mapping()
.unwrap()
.rename_key(old, new);
(renamed, yaml.to_string())
}
#[test]
fn rename_key_onto_existing_key_replaces_it() {
assert_eq!(
rename_in("a: 1\nb: 2\nc: 3\n", "a", "b"),
(true, "b: 1\nc: 3\n".to_string())
);
assert_eq!(
rename_in("b: 2\na: 1\nc: 3\n", "a", "b"),
(true, "b: 1\nc: 3\n".to_string())
);
assert_eq!(
rename_in("x: 0\na: 1\nc: 3\nb: 2", "a", "b"),
(true, "x: 0\nb: 1\nc: 3".to_string())
);
assert_eq!(
rename_in("a: 1\nb: 2\nb: 3\n", "a", "b"),
(true, "b: 1\n".to_string())
);
assert_eq!(
rename_in("{a: 1, b: 2, c: 3}", "a", "b"),
(true, "{b: 1, c: 3}".to_string())
);
}
#[test]
fn rename_key_onto_existing_key_keeps_comments_and_anchor() {
let yaml = YamlFile::from_str("top:\n b: 2\n # about a\n a: &x 1\n").unwrap();
let top = yaml
.document()
.unwrap()
.as_mapping()
.unwrap()
.get_mapping("top")
.unwrap();
assert!(top.rename_key("a", "b"));
assert_eq!(yaml.to_string(), "top:\n # about a\n b: &x 1\n");
}
#[test]
fn rename_key_to_itself_keeps_duplicates() {
assert_eq!(
rename_in("a: 1\na: 2\n", "a", "a"),
(true, "a: 1\na: 2\n".to_string())
);
}
#[test]
fn document_rename_key_onto_existing_key() {
let doc = yaml_edit::Document::from_str("a: 1\nb: 2\n").unwrap();
assert!(doc.rename_key("a", "b"));
assert_eq!(doc.to_string(), "b: 1\n");
}
#[test]
fn set_value_of_explicit_key_without_one() {
for (source, expected) in [
("? a", "? a\n: 5"),
("? a\n", "? a\n: 5\n"),
("? a\nb: 1\n", "? a\n: 5\nb: 1\n"),
("? a # c", "? a # c\n: 5"),
("? a # c\nb: 1\n", "? a # c\n: 5\nb: 1\n"),
] {
let yaml = YamlFile::from_str(source).unwrap();
yaml.document().unwrap().as_mapping().unwrap().set("a", 5);
assert_file_edited_valid(&yaml, expected);
}
for (source, expected) in [
("x:\n ? a", "x:\n ? a\n : 5"),
("x:\n ? a\ny: 1\n", "x:\n ? a\n : 5\ny: 1\n"),
] {
let yaml = YamlFile::from_str(source).unwrap();
let doc = yaml.document().unwrap();
doc.as_mapping()
.unwrap()
.get_mapping("x")
.unwrap()
.set("a", 5);
assert_file_edited_valid(&yaml, expected);
}
let seq = || {
yaml_edit::Document::from_str("- 1\n- 2\n")
.unwrap()
.as_sequence()
.unwrap()
};
for (source, expected) in [
("? a", "? a\n:\n - 1\n - 2"),
("? a\n", "? a\n:\n - 1\n - 2\n"),
("? a\nb: 1\n", "? a\n:\n - 1\n - 2\nb: 1\n"),
] {
let yaml = YamlFile::from_str(source).unwrap();
yaml.document()
.unwrap()
.as_mapping()
.unwrap()
.set("a", seq());
assert_file_edited_valid(&yaml, expected);
}
}
#[test]
fn alias_key_is_separated_from_colon() {
let edits: [(&str, MappingEdit, &str); 6] = [
(
"x: &a k\n",
|m| m.set(Alias::new("a"), 1),
"x: &a k\n*a : 1\n",
),
(
"{x: &a k}\n",
|m| m.set(Alias::new("a"), 1),
"{x: &a k, *a : 1}\n",
),
(
"x: &a k\ny: 1\n",
|m| assert!(m.rename_key("y", Alias::new("a"))),
"x: &a k\n*a : 1\n",
),
(
"{x: &a k, y: 1}\n",
|m| assert!(m.rename_key("y", Alias::new("a"))),
"{x: &a k, *a : 1}\n",
),
(
"x: &a k\ny: 1\n",
|m| assert!(m.insert_after("x", Alias::new("a"), 2)),
"x: &a k\n*a : 2\ny: 1\n",
),
(
"x: &a k\nb: [1]\nc:\n *a : 1\n",
|m| {
let c = m.get("c").unwrap();
m.get_sequence("b").unwrap().push(c);
},
"x: &a k\nb: [1, {*a : 1}]\nc:\n *a : 1\n",
),
];
for (source, edit, expected) in edits {
let file = YamlFile::from_str(source).unwrap();
edit(&file.document().unwrap().as_mapping().unwrap());
assert_file_edited_valid(&file, expected);
}
}
#[test]
fn set_explicit_key_value_with_cr_line_breaks() {
let file = YamlFile::from_str("? a\r? b\r").unwrap();
file.document().unwrap().as_mapping().unwrap().set("a", "x");
assert_file_edited_valid(&file, "? a\r: x\r? b\r");
}
#[test]
fn rename_key_holding_two_nodes() {
let doc = YamlFile::from_str("[q] x: 1\n")
.unwrap()
.document()
.unwrap();
let mapping = doc.as_mapping().unwrap();
let old = mapping.keys().next().unwrap();
assert!(mapping.rename_key(old, "z"));
assert_cst_ok(&doc);
assert_parser_shaped(&doc);
assert_eq!(doc.to_string(), "z: 1\n");
}
#[test]
fn set_with_block_collection_key() {
let source = YamlFile::from_str("k:\n - x\n - y\n")
.unwrap()
.document()
.unwrap();
let block = source.as_mapping().unwrap().get("k").unwrap();
let doc = YamlFile::from_str("a: 1\n").unwrap().document().unwrap();
doc.as_mapping().unwrap().set(block, 2);
assert_cst_ok(&doc);
assert_parser_shaped(&doc);
assert_eq!(doc.to_string(), "a: 1\n[x, y]: 2\n");
}
#[test]
fn set_value_of_block_explicit_key_without_one() {
for (src, expected) in [
("? - a\n", "? - a\n: 5\n"),
("? - a\n - b\nc: 1\n", "? - a\n - b\n: 5\nc: 1\n"),
("? a\n", "? a\n: 5\n"),
("? |\n t\n", "? |\n t\n: 5\n"),
("? [a,\n b]\n", "? [a,\n b]\n: 5\n"),
("? - a\n? b\n", "? - a\n: 5\n? b\n"),
] {
let doc = YamlFile::from_str(src).unwrap().document().unwrap();
let mapping = doc.as_mapping().unwrap();
let key = mapping.keys().next().unwrap();
mapping.set(key, 5);
assert_cst_ok(&doc);
assert_eq!(doc.to_string(), expected, "{src:?}");
}
}
#[test]
fn rename_key() {
for (src, old, new, renamed, expected) in [
(
"old_name: value",
"old_name",
"new_name",
true,
"new_name: value",
),
(
"name: my-app\nversion: 1.0\nauthor: Alice",
"version",
"app_version",
true,
"name: my-app\napp_version: 1.0\nauthor: Alice",
),
(
"count: 42\nenabled: true",
"count",
"total",
true,
"total: 42\nenabled: true",
),
("name: Alice", "age", "years", false, "name: Alice"),
(
"name: Alice\nage: 30\ncity: NYC",
"name",
"username",
true,
"username: Alice\nage: 30\ncity: NYC",
),
(
"name: Alice\nage: 30\ncity: NYC",
"age",
"years",
true,
"name: Alice\nyears: 30\ncity: NYC",
),
(
"name: Alice\nage: 30\ncity: NYC",
"city",
"location",
true,
"name: Alice\nage: 30\nlocation: NYC",
),
] {
let text = edit_root(src, |m| {
assert_eq!(m.rename_key(old, new), renamed, "{src:?}")
});
assert_eq!(text, expected, "{src:?}");
}
}