use crate::{lex::SyntaxKind, parse::Parse, PositionedParseError};
use rowan::ast::AstNode;
use rowan::GreenNodeBuilder;
use std::path::Path;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ParsedYaml {
pub(crate) green_node: rowan::GreenNode,
pub(crate) errors: Vec<String>,
pub(crate) positioned_errors: Vec<PositionedParseError>,
}
use crate::nodes::{ast_node, has_child_token};
pub use crate::nodes::{Lang, SyntaxNode};
pub use crate::nodes::{
Alias, Comment, Directive, Document, Entry, Mapping, MappingEntry, OccupiedEntry, Scalar,
ScalarConversionError, Sequence, TaggedNode, VacantEntry,
};
ast_node!(
YamlFile,
ROOT,
"A YAML file containing one or more documents"
);
impl YamlFile {
pub fn snapshot(&self) -> Self {
YamlFile(SyntaxNode::new_root_mut(self.0.green().into_owned()))
}
pub fn tree_eq(&self, other: &Self) -> bool {
let a = self.0.green();
let b = other.0.green();
let a_ref: &rowan::GreenNodeData = &a;
let b_ref: &rowan::GreenNodeData = &b;
std::ptr::eq(a_ref as *const _, b_ref as *const _) || a_ref == b_ref
}
}
pub trait ValueNode: rowan::ast::AstNode<Language = Lang> {
fn is_inline(&self) -> bool;
}
impl ValueNode for Mapping {
fn is_inline(&self) -> bool {
has_child_token(&self.0, |k| {
matches!(k, SyntaxKind::LEFT_BRACE | SyntaxKind::RIGHT_BRACE)
})
}
}
impl ValueNode for Sequence {
fn is_inline(&self) -> bool {
has_child_token(&self.0, |k| {
matches!(k, SyntaxKind::LEFT_BRACKET | SyntaxKind::RIGHT_BRACKET)
})
}
}
impl ValueNode for Scalar {
fn is_inline(&self) -> bool {
!self
.0
.children_with_tokens()
.any(|c| matches!(c.kind(), SyntaxKind::PIPE | SyntaxKind::GREATER))
}
}
impl ValueNode for TaggedNode {
fn is_inline(&self) -> bool {
let mut seen_newline = false;
for child in self.0.children_with_tokens() {
match child {
rowan::NodeOrToken::Token(t) if t.kind() == SyntaxKind::NEWLINE => {
seen_newline = true;
}
rowan::NodeOrToken::Node(n) => {
if seen_newline {
return false;
}
return match n.kind() {
SyntaxKind::MAPPING => Mapping::cast(n).is_some_and(|m| m.is_inline()),
SyntaxKind::SEQUENCE => Sequence::cast(n).is_some_and(|s| s.is_inline()),
SyntaxKind::SCALAR => Scalar::cast(n).is_some_and(|s| s.is_inline()),
_ => true,
};
}
_ => {}
}
}
true
}
}
pub(crate) fn ends_with_newline(node: &SyntaxNode) -> bool {
node.last_token()
.is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
}
pub(crate) fn detach_empty_collection_placeholder_newline(
empty_collection: &SyntaxNode,
new_entry: &SyntaxNode,
) {
if !ends_with_newline(new_entry) {
return;
}
let Some(parent_value) = empty_collection.parent() else {
return;
};
if parent_value.kind() != SyntaxKind::VALUE {
return;
}
let Some(parent_entry) = parent_value.parent() else {
return;
};
if parent_entry.kind() != SyntaxKind::MAPPING_ENTRY {
return;
}
let Some(last) = parent_entry.last_child_or_token() else {
return;
};
if last
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
{
last.detach();
}
}
pub(crate) fn convert_placeholder_to_block(collection: &SyntaxNode) {
let children: Vec<_> = collection.children_with_tokens().collect();
for child in children {
child.detach();
}
let Some(parent) = collection
.parent()
.filter(|p| p.kind() == SyntaxKind::VALUE)
else {
return;
};
let entry = parent
.parent()
.filter(|e| e.kind() == SyntaxKind::MAPPING_ENTRY);
let indent_width = entry
.as_ref()
.and_then(|e| e.parent())
.filter(|m| m.kind() == SyntaxKind::MAPPING)
.and_then(crate::nodes::Mapping::cast)
.map_or(2, |m| m.detect_indentation_level() + 2);
if let Some(entry) = entry {
let sep = entry
.children_with_tokens()
.position(|c| c.as_node() == Some(&parent))
.filter(|&i| i >= 1)
.and_then(|i| entry.children_with_tokens().nth(i - 1))
.filter(|c| {
c.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::WHITESPACE)
});
if let Some(sep) = sep {
sep.detach();
}
}
let pos = parent
.children_with_tokens()
.position(|c| c.as_node() == Some(collection))
.unwrap_or(0);
let already_scaffolded = pos >= 1
&& parent
.children_with_tokens()
.nth(pos - 1)
.and_then(|c| c.into_token())
.is_some_and(|t| matches!(t.kind(), SyntaxKind::INDENT | SyntaxKind::NEWLINE));
if already_scaffolded {
return;
}
let nl = crate::nodes::fresh_token(SyntaxKind::NEWLINE, "\n");
let indent = crate::nodes::fresh_token(SyntaxKind::INDENT, &" ".repeat(indent_width));
parent.splice_children(pos..pos, vec![nl.into(), indent.into()]);
}
pub(crate) fn collapse_empty_child_collection_in_parent(collection: &SyntaxNode) {
let (open_kind, close_kind, open_txt, close_txt) = match collection.kind() {
SyntaxKind::MAPPING => (SyntaxKind::LEFT_BRACE, SyntaxKind::RIGHT_BRACE, "{", "}"),
SyntaxKind::SEQUENCE => (
SyntaxKind::LEFT_BRACKET,
SyntaxKind::RIGHT_BRACKET,
"[",
"]",
),
_ => return,
};
let is_flow = has_child_token(collection, |k| k == open_kind);
if is_flow {
return;
}
let Some(host) = collection
.parent()
.filter(|p| matches!(p.kind(), SyntaxKind::VALUE | SyntaxKind::TAGGED_NODE))
else {
return;
};
let value_node = if host.kind() == SyntaxKind::TAGGED_NODE {
host.parent()
} else {
Some(host.clone())
};
let Some(value_node) = value_node.filter(|v| v.kind() == SyntaxKind::VALUE) else {
return;
};
let Some(entry_node) = value_node
.parent()
.filter(|p| p.kind() == SyntaxKind::MAPPING_ENTRY)
else {
return;
};
let has_other = host.children_with_tokens().any(|el| match el {
rowan::NodeOrToken::Node(n) => &n != collection,
rowan::NodeOrToken::Token(t) => !matches!(
t.kind(),
SyntaxKind::NEWLINE | SyntaxKind::INDENT | SyntaxKind::WHITESPACE | SyntaxKind::TAG
),
});
if has_other {
return;
}
let tag_token = host
.children_with_tokens()
.find_map(|el| el.into_token().filter(|t| t.kind() == SyntaxKind::TAG));
let host_children: Vec<_> = host.children_with_tokens().collect();
for c in &host_children {
c.detach();
}
let ws = crate::nodes::fresh_token(SyntaxKind::WHITESPACE, " ");
let mut inner_builder = rowan::GreenNodeBuilder::new();
inner_builder.start_node(collection.kind().into());
inner_builder.token(open_kind.into(), open_txt);
inner_builder.token(close_kind.into(), close_txt);
inner_builder.finish_node();
let new_collection = SyntaxNode::new_root_mut(inner_builder.finish());
let mut replacement: Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>> = Vec::new();
let tagged = tag_token.is_some();
if let Some(tag) = tag_token {
replacement.push(crate::nodes::fresh_token(SyntaxKind::TAG, tag.text()).into());
replacement.push(ws.clone().into());
}
replacement.push(new_collection.into());
host.splice_children(0..0, replacement);
if !tagged {
let value_index = entry_node
.children_with_tokens()
.position(|el| el.as_node() == Some(&value_node));
if let Some(index) = value_index {
let separated = index
.checked_sub(1)
.and_then(|p| entry_node.children_with_tokens().nth(p))
.is_some_and(|el| {
el.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::WHITESPACE)
});
if !separated {
entry_node.splice_children(index..index, vec![ws.into()]);
}
}
}
crate::nodes::ensure_trailing_newline(&entry_node);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Set(SyntaxNode);
impl Set {
pub fn cast(node: SyntaxNode) -> Option<Self> {
if node.kind() == SyntaxKind::TAGGED_NODE {
if let Some(tagged_node) = TaggedNode::cast(node.clone()) {
if tagged_node.tag().as_deref() == Some("!!set") {
return Some(Set(node));
}
}
}
None
}
fn inner_mapping(&self) -> Option<Mapping> {
self.0.children().find_map(Mapping::cast)
}
pub fn members(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
self.inner_mapping().into_iter().flat_map(|m| {
let mapping_node = m.syntax().clone();
let has_entries = mapping_node
.children()
.any(|n| n.kind() == SyntaxKind::MAPPING_ENTRY);
if has_entries {
mapping_node
.children()
.filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
.filter_map(|entry| {
entry
.children()
.find(|n| n.kind() == SyntaxKind::KEY)
.and_then(|key| key.children().next())
.and_then(crate::as_yaml::YamlNode::from_syntax)
})
.collect::<Vec<_>>()
} else {
mapping_node
.children()
.filter(|n| n.kind() == SyntaxKind::KEY)
.filter_map(|key| {
key.children()
.next()
.and_then(crate::as_yaml::YamlNode::from_syntax)
})
.collect::<Vec<_>>()
}
})
}
pub fn len(&self) -> usize {
self.members().count()
}
pub fn is_empty(&self) -> bool {
self.members().next().is_none()
}
pub fn contains(&self, value: impl crate::AsYaml) -> bool {
self.members()
.any(|member| crate::as_yaml::yaml_eq(&member, &value))
}
}
fn extract_content_node(wrapper: &SyntaxNode) -> Option<SyntaxNode> {
use crate::lex::SyntaxKind;
match wrapper.kind() {
SyntaxKind::VALUE | SyntaxKind::KEY => wrapper.children().next(),
_ => Some(wrapper.clone()),
}
}
fn smart_cast<T: AstNode<Language = Lang>>(node: SyntaxNode) -> Option<T> {
extract_content_node(&node).and_then(T::cast)
}
pub(crate) fn extract_scalar(node: &SyntaxNode) -> Option<Scalar> {
smart_cast(node.clone())
}
pub(crate) fn extract_mapping(node: &SyntaxNode) -> Option<Mapping> {
smart_cast(node.clone())
}
pub(crate) fn extract_sequence(node: &SyntaxNode) -> Option<Sequence> {
smart_cast(node.clone())
}
pub(crate) fn extract_tagged_node(node: &SyntaxNode) -> Option<TaggedNode> {
smart_cast(node.clone())
}
pub(crate) fn copy_node_to_builder(builder: &mut GreenNodeBuilder, node: &SyntaxNode) {
builder.start_node(node.kind().into());
add_node_children_to(builder, node);
builder.finish_node();
}
pub(crate) fn add_node_children_to(builder: &mut GreenNodeBuilder, node: &SyntaxNode) {
for child in node.children_with_tokens() {
match child {
rowan::NodeOrToken::Node(child_node) => {
builder.start_node(child_node.kind().into());
add_node_children_to(builder, &child_node);
builder.finish_node();
}
rowan::NodeOrToken::Token(token) => {
builder.token(token.kind().into(), token.text());
}
}
}
}
pub(crate) fn dump_cst_to_string(node: &SyntaxNode, indent: usize) -> String {
let mut result = String::new();
let indent_str = " ".repeat(indent);
for child in node.children_with_tokens() {
match child {
rowan::NodeOrToken::Node(n) => {
result.push_str(&format!("{}{:?}\n", indent_str, n.kind()));
result.push_str(&dump_cst_to_string(&n, indent + 1));
}
rowan::NodeOrToken::Token(t) => {
result.push_str(&format!("{} {:?} {:?}\n", indent_str, t.kind(), t.text()));
}
}
}
result
}
impl Default for YamlFile {
fn default() -> Self {
Self::new()
}
}
impl YamlFile {
pub fn new() -> YamlFile {
let mut builder = GreenNodeBuilder::new();
builder.start_node(SyntaxKind::ROOT.into());
builder.finish_node();
YamlFile(SyntaxNode::new_root_mut(builder.finish()))
}
pub fn parse(text: &str) -> Parse<YamlFile> {
Parse::parse_yaml(text)
}
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<YamlFile, crate::YamlError> {
let contents = std::fs::read_to_string(path)?;
Self::from_str(&contents)
}
pub fn from_str_relaxed(s: &str) -> (YamlFile, Vec<String>) {
let parsed = YamlFile::parse(s);
let errors = parsed.errors();
(parsed.tree(), errors)
}
pub fn from_path_relaxed<P: AsRef<Path>>(
path: P,
) -> Result<(YamlFile, Vec<String>), std::io::Error> {
let contents = std::fs::read_to_string(path)?;
Ok(Self::from_str_relaxed(&contents))
}
pub fn read_relaxed<R: std::io::Read>(
mut r: R,
) -> Result<(YamlFile, Vec<String>), std::io::Error> {
let mut buf = String::new();
r.read_to_string(&mut buf)?;
Ok(Self::from_str_relaxed(&buf))
}
pub fn documents(&self) -> impl Iterator<Item = Document> {
self.0.children().filter_map(Document::cast)
}
pub fn comments(&self) -> impl Iterator<Item = Comment> {
crate::nodes::comment::comments(&self.0)
}
pub fn document(&self) -> Option<Document> {
self.documents().next()
}
pub fn ensure_document(&self) -> Document {
if self.documents().next().is_none() {
let doc = Document::new_mapping();
self.push_document(doc);
}
self.documents()
.next()
.expect("Document should exist after ensuring")
}
pub fn directives(&self) -> impl Iterator<Item = Directive> {
self.0.children().filter_map(Directive::cast)
}
pub fn add_directive(&self, directive_text: &str) {
let mut builder = GreenNodeBuilder::new();
builder.start_node(SyntaxKind::DIRECTIVE.into());
builder.token(SyntaxKind::DIRECTIVE.into(), directive_text);
builder.finish_node();
let directive_node = SyntaxNode::new_root_mut(builder.finish());
self.0.splice_children(0..0, vec![directive_node.into()]);
}
pub fn push_document(&self, document: Document) {
crate::nodes::append_children(&self.0, vec![document.0.into()]);
}
pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) {
if let Some(doc) = self.document() {
doc.set(key, value);
}
}
pub fn insert_after(
&self,
after_key: impl crate::AsYaml,
key: impl crate::AsYaml,
value: impl crate::AsYaml,
) -> bool {
self.document()
.is_some_and(|doc| doc.insert_after(after_key, key, value))
}
pub fn insert_before(
&self,
before_key: impl crate::AsYaml,
key: impl crate::AsYaml,
value: impl crate::AsYaml,
) -> bool {
self.document()
.is_some_and(|doc| doc.insert_before(before_key, key, value))
}
pub fn move_after(
&self,
after_key: impl crate::AsYaml,
key: impl crate::AsYaml,
value: impl crate::AsYaml,
) -> bool {
self.document()
.is_some_and(|doc| doc.move_after(after_key, key, value))
}
pub fn move_before(
&self,
before_key: impl crate::AsYaml,
key: impl crate::AsYaml,
value: impl crate::AsYaml,
) -> bool {
self.document()
.is_some_and(|doc| doc.move_before(before_key, key, value))
}
pub fn insert_at_index(
&self,
index: usize,
key: impl crate::AsYaml,
value: impl crate::AsYaml,
) {
if let Some(doc) = self.document() {
doc.insert_at_index(index, key, value);
} else {
let mut builder = GreenNodeBuilder::new();
builder.start_node(SyntaxKind::DOCUMENT.into());
builder.start_node(SyntaxKind::MAPPING.into());
builder.finish_node(); builder.finish_node(); let doc = Document(SyntaxNode::new_root_mut(builder.finish()));
self.0.splice_children(0..0, vec![doc.0.into()]);
if let Some(doc) = self.document() {
doc.insert_at_index(index, key, value);
}
}
}
}
impl FromStr for YamlFile {
type Err = crate::YamlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed = YamlFile::parse(s);
if !parsed.positioned_errors().is_empty() {
let first = &parsed.positioned_errors()[0];
let lc = crate::byte_offset_to_line_column(s, first.range.start as usize);
return Err(crate::YamlError::Parse {
message: first.message.clone(),
line: Some(lc.line),
column: Some(lc.column),
});
}
Ok(parsed.tree())
}
}
pub(crate) use crate::parser::parse;
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::{MappingBuilder, SequenceBuilder};
use crate::scalar::ScalarValue;
use crate::value::YamlValue;
#[test]
fn test_simple_mapping() {
let yaml = "key: value";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().unwrap();
let mapping = doc.as_mapping().unwrap();
assert_eq!(parsed.to_string().trim(), "key: value");
let value = mapping.get("key");
assert!(value.is_some());
}
#[test]
fn test_simple_sequence() {
let yaml = "- item1\n- item2";
let parsed = YamlFile::from_str(yaml);
assert!(parsed.is_ok());
}
#[test]
fn test_complex_yaml() {
let yaml = r#"
name: my-app
version: 1.0.0
dependencies:
- serde
- tokio
config:
port: 8080
enabled: true
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
assert_eq!(parsed.documents().count(), 1);
let doc = parsed.document().unwrap();
assert!(doc.as_mapping().is_some());
}
#[test]
fn test_multiple_documents() {
let yaml = r#"---
doc: first
---
doc: second
...
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
assert_eq!(parsed.documents().count(), 2);
}
#[test]
fn test_flow_styles() {
let yaml = r#"
array: [1, 2, 3]
object: {key: value, another: 42}
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
assert!(parsed.document().is_some());
}
#[test]
fn test_scalar_types_parsing() {
let yaml = r#"
string: hello
integer: 42
float: 3.14
bool_true: true
bool_false: false
null_value: null
tilde: ~
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().unwrap();
let mapping = doc.as_mapping().unwrap();
assert!(mapping.get("string").is_some());
assert!(mapping.get("integer").is_some());
assert!(mapping.get("float").is_some());
assert!(mapping.get("bool_true").is_some());
assert!(mapping.get("bool_false").is_some());
assert!(mapping.get("null_value").is_some());
assert!(mapping.get("tilde").is_some());
}
#[test]
fn test_preserve_formatting() {
let yaml = r#"# Comment at start
key: value # inline comment
# Another comment
list:
- item1
- item2
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().unwrap();
let mapping = doc.as_mapping().unwrap();
assert_eq!(
mapping.get("key").unwrap().as_scalar().unwrap().as_string(),
"value"
);
let list = mapping.get_sequence("list").unwrap();
assert_eq!(list.len(), 2);
let items: Vec<String> = list
.values()
.map(|v| v.to_string().trim().to_string())
.collect();
assert_eq!(items, vec!["item1", "item2"]);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_quoted_strings() {
let yaml = r#"
single: 'single quoted'
double: "double quoted"
plain: unquoted
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().unwrap();
let mapping = doc.as_mapping().unwrap();
assert!(mapping.get("single").is_some());
assert!(mapping.get("double").is_some());
assert!(mapping.get("plain").is_some());
}
#[test]
fn test_nested_structures() {
let yaml = r#"
root:
nested:
deeply:
value: 42
list:
- item1
- item2
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
assert!(parsed.document().is_some());
}
#[test]
fn test_empty_values() {
let yaml = r#"
empty_string: ""
empty_after_colon:
another_key: value
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().unwrap();
let mapping = doc.as_mapping().unwrap();
assert!(mapping.get("empty_string").is_some());
assert!(mapping.get("another_key").is_some());
}
#[test]
fn test_special_characters() {
let yaml = r#"
special: "line1\nline2"
unicode: "emoji 😀"
escaped: 'it\'s escaped'
"#;
let result = YamlFile::from_str(yaml);
assert!(result.is_ok());
}
#[test]
fn test_error_handling() {
let yaml = "key: value\n invalid indentation for key";
let result = YamlFile::from_str(yaml);
let _ = result;
}
#[test]
fn test_anchor_exact_output() {
let yaml = "key: &anchor value\nref: *anchor";
let parsed = YamlFile::from_str(yaml).unwrap();
let output = parsed.to_string();
assert_eq!(output, "key: &anchor value\nref: *anchor");
}
#[test]
fn test_anchor_with_different_value_types() {
let yaml = r#"string_anchor: &str_val "hello"
int_anchor: &int_val 42
bool_anchor: &bool_val true
null_anchor: &null_val null
str_ref: *str_val
int_ref: *int_val
bool_ref: *bool_val
null_ref: *null_val"#;
let parsed = YamlFile::from_str(yaml);
assert!(
parsed.is_ok(),
"Should parse anchors with different value types"
);
let yaml_doc = parsed.unwrap();
let doc = yaml_doc.document().unwrap();
let mapping = doc.as_mapping().unwrap();
assert_eq!(
mapping
.get("string_anchor")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"hello"
);
assert_eq!(mapping.get("int_anchor").unwrap().to_i64(), Some(42));
assert_eq!(mapping.get("bool_anchor").unwrap().to_bool(), Some(true));
assert!(mapping.get("null_anchor").unwrap().as_scalar().is_some());
let str_ref = mapping.get("str_ref").unwrap();
assert!(str_ref.is_alias());
assert_eq!(str_ref.as_alias().unwrap().name(), "str_val");
let int_ref = mapping.get("int_ref").unwrap();
assert!(int_ref.is_alias());
assert_eq!(int_ref.as_alias().unwrap().name(), "int_val");
let bool_ref = mapping.get("bool_ref").unwrap();
assert!(bool_ref.is_alias());
assert_eq!(bool_ref.as_alias().unwrap().name(), "bool_val");
let null_ref = mapping.get("null_ref").unwrap();
assert!(null_ref.is_alias());
assert_eq!(null_ref.as_alias().unwrap().name(), "null_val");
let output = yaml_doc.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_undefined_alias_parses_successfully() {
let yaml = "key: *undefined";
let parse_result = Parse::parse_yaml(yaml);
assert!(
!parse_result.has_errors(),
"Parser should not validate undefined aliases"
);
let output = parse_result.tree().to_string();
assert_eq!(output.trim(), "key: *undefined");
}
#[test]
fn test_anchor_names_with_alphanumeric_chars() {
let yaml1 = "key1: &anchor_123 val1\nref1: *anchor_123";
let parsed1 = YamlFile::from_str(yaml1);
assert!(
parsed1.is_ok(),
"Should parse anchors with underscores and numbers"
);
let file1 = parsed1.unwrap();
let doc1 = file1.document().unwrap();
let map1 = doc1.as_mapping().unwrap();
assert_eq!(
map1.get("key1").unwrap().as_scalar().unwrap().as_string(),
"val1"
);
assert!(map1.get("ref1").unwrap().is_alias());
assert_eq!(
map1.get("ref1").unwrap().as_alias().unwrap().name(),
"anchor_123"
);
assert_eq!(file1.to_string(), yaml1);
let yaml2 = "key2: &AnchorName val2\nref2: *AnchorName";
let parsed2 = YamlFile::from_str(yaml2);
assert!(parsed2.is_ok(), "Should parse anchors with mixed case");
let file2 = parsed2.unwrap();
let doc2 = file2.document().unwrap();
let map2 = doc2.as_mapping().unwrap();
assert_eq!(
map2.get("key2").unwrap().as_scalar().unwrap().as_string(),
"val2"
);
assert!(map2.get("ref2").unwrap().is_alias());
assert_eq!(
map2.get("ref2").unwrap().as_alias().unwrap().name(),
"AnchorName"
);
assert_eq!(file2.to_string(), yaml2);
let yaml3 = "key3: &anchor123abc val3\nref3: *anchor123abc";
let parsed3 = YamlFile::from_str(yaml3);
assert!(
parsed3.is_ok(),
"Should parse anchors with letters and numbers"
);
let file3 = parsed3.unwrap();
let doc3 = file3.document().unwrap();
let map3 = doc3.as_mapping().unwrap();
assert_eq!(
map3.get("key3").unwrap().as_scalar().unwrap().as_string(),
"val3"
);
assert!(map3.get("ref3").unwrap().is_alias());
assert_eq!(
map3.get("ref3").unwrap().as_alias().unwrap().name(),
"anchor123abc"
);
assert_eq!(file3.to_string(), yaml3);
}
#[test]
fn test_anchor_in_sequence_detailed() {
let yaml = r#"items:
- &first_item value1
- second_item
- *first_item"#;
let parsed = YamlFile::from_str(yaml);
assert!(parsed.is_ok(), "Should parse anchors in sequences");
let yaml_doc = parsed.unwrap();
let doc = yaml_doc.document().unwrap();
let mapping = doc.as_mapping().unwrap();
let seq = mapping.get_sequence("items").unwrap();
assert_eq!(seq.len(), 3);
let item0 = seq.get(0).unwrap();
let item1 = seq.get(1).unwrap();
let item2 = seq.get(2).unwrap();
assert_eq!(item0.as_scalar().unwrap().as_string(), "value1");
assert_eq!(item1.as_scalar().unwrap().as_string(), "second_item");
assert!(item2.is_alias(), "Third item should be an alias");
assert_eq!(item2.as_alias().unwrap().name(), "first_item");
let output = yaml_doc.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_preserve_whitespace_around_anchors() {
let yaml = "key: &anchor value \nref: *anchor ";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().unwrap();
let mapping = doc.as_mapping().unwrap();
assert_eq!(
mapping
.get("key")
.unwrap()
.as_scalar()
.unwrap()
.as_string()
.trim(),
"value"
);
let ref_node = mapping.get("ref").unwrap();
assert!(ref_node.is_alias());
assert_eq!(ref_node.as_alias().unwrap().name(), "anchor");
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_literal_block_scalar_basic() {
let yaml = r#"literal: |
Line 1
Line 2
Line 3
"#;
let parsed = YamlFile::from_str(yaml);
assert!(parsed.is_ok(), "Should parse basic literal block scalar");
let yaml_doc = parsed.unwrap();
let output = yaml_doc.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_folded_block_scalar_basic() {
let yaml = r#"folded: >
This is a very long line that will be folded
into a single line in the output
but preserves paragraph breaks.
This is a new paragraph.
"#;
let parsed = YamlFile::from_str(yaml);
assert!(parsed.is_ok(), "Should parse basic folded block scalar");
let yaml_doc = parsed.unwrap();
let output = yaml_doc.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_literal_block_scalar_with_chomping_indicators() {
let yaml1 = r#"strip: |-
Line 1
Line 2
"#;
let parsed1 = YamlFile::from_str(yaml1);
assert!(
parsed1.is_ok(),
"Should parse literal block scalar with strip indicator"
);
let file1 = parsed1.unwrap();
let doc1 = file1.document().unwrap();
let mapping1 = doc1.as_mapping().unwrap();
let value1 = mapping1
.get("strip")
.unwrap()
.as_scalar()
.unwrap()
.as_string();
assert_eq!(value1, "Line 1\nLine 2");
let output1 = file1.to_string();
assert_eq!(output1, yaml1);
let yaml2 = r#"keep: |+
Line 1
Line 2
"#;
let parsed2 = YamlFile::from_str(yaml2);
assert!(
parsed2.is_ok(),
"Should parse literal block scalar with keep indicator"
);
let file2 = parsed2.unwrap();
let doc2 = file2.document().unwrap();
let mapping2 = doc2.as_mapping().unwrap();
let value2 = mapping2
.get("keep")
.unwrap()
.as_scalar()
.unwrap()
.as_string();
assert_eq!(value2, "Line 1\nLine 2\n\n");
let output2 = file2.to_string();
assert_eq!(output2, yaml2);
}
#[test]
fn test_folded_block_scalar_with_chomping_indicators() {
let yaml1 = r#"strip: >-
Folded content that should
be stripped of final newlines
"#;
let parsed1 = YamlFile::from_str(yaml1);
assert!(
parsed1.is_ok(),
"Should parse folded block scalar with strip indicator"
);
let file1 = parsed1.unwrap();
let doc1 = file1.document().unwrap();
let mapping1 = doc1.as_mapping().unwrap();
let value1 = mapping1
.get("strip")
.unwrap()
.as_scalar()
.unwrap()
.as_string();
assert_eq!(
value1,
"Folded content that should be stripped of final newlines"
);
let output1 = file1.to_string();
assert_eq!(output1, yaml1);
let yaml2 = r#"keep: >+
Folded content that should
keep all final newlines
"#;
let parsed2 = YamlFile::from_str(yaml2);
assert!(
parsed2.is_ok(),
"Should parse folded block scalar with keep indicator"
);
let file2 = parsed2.unwrap();
let doc2 = file2.document().unwrap();
let mapping2 = doc2.as_mapping().unwrap();
let value2 = mapping2
.get("keep")
.unwrap()
.as_scalar()
.unwrap()
.as_string();
assert_eq!(
value2,
"Folded content that should keep all final newlines\n\n"
);
let output2 = file2.to_string();
assert_eq!(output2, yaml2);
}
#[test]
fn test_block_scalar_with_explicit_indentation() {
let yaml1 = r#"explicit: |2
Two space indent
Another line
"#;
let parsed1 = YamlFile::from_str(yaml1)
.expect("Should parse literal block scalar with explicit indentation");
let doc1 = parsed1.document().expect("Should have document");
let mapping1 = doc1.as_mapping().expect("Should be a mapping");
let scalar1 = mapping1
.get("explicit")
.expect("Should have 'explicit' key");
assert_eq!(
scalar1.as_scalar().unwrap().as_string(),
" Two space indent\n Another line\n"
);
let output1 = parsed1.to_string();
assert_eq!(output1, yaml1);
let yaml2 = r#"folded_explicit: >3
Three space indent
Another folded line
"#;
let parsed2 = YamlFile::from_str(yaml2)
.expect("Should parse folded block scalar with explicit indentation");
let doc2 = parsed2.document().expect("Should have document");
let mapping2 = doc2.as_mapping().expect("Should be a mapping");
let scalar2 = mapping2
.get("folded_explicit")
.expect("Should have 'folded_explicit' key");
assert_eq!(
scalar2.as_scalar().unwrap().as_string(),
" Three space indent\n Another folded line\n"
);
let output2 = parsed2.to_string();
assert_eq!(output2, yaml2);
}
#[test]
fn test_block_scalar_in_mapping() {
let yaml = r#"description: |
This is a multi-line
description that should
preserve line breaks.
It can have multiple paragraphs too.
summary: >
This is a summary that
should be folded into
a single line.
version: "1.0"
"#;
let parsed =
YamlFile::from_str(yaml).expect("Should parse block scalars in mapping context");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let description = mapping
.get("description")
.expect("Should have 'description' key");
assert_eq!(
description.as_scalar().unwrap().as_string(),
"This is a multi-line\ndescription that should\npreserve line breaks.\n\nIt can have multiple paragraphs too.\n"
);
let summary = mapping.get("summary").expect("Should have 'summary' key");
assert_eq!(
summary.as_scalar().unwrap().as_string(),
"This is a summary that should be folded into a single line.\n"
);
let version = mapping.get("version").expect("Should have 'version' key");
assert_eq!(version.as_scalar().unwrap().as_string(), "1.0");
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_mixed_block_and_regular_scalars() {
let yaml = r#"config:
name: "My App"
description: |
This application does many things:
- Feature 1
- Feature 2
- Feature 3
summary: >
A brief summary that spans
multiple lines but should
be folded together.
version: 1.0
enabled: true
"#;
let parsed =
YamlFile::from_str(yaml).expect("Should parse mixed block and regular scalars");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let config_node = mapping.get("config").expect("Should have 'config' key");
let config = config_node
.as_mapping()
.expect("Should be a nested mapping");
assert_eq!(
config.get("name").unwrap().as_scalar().unwrap().as_string(),
"My App"
);
assert_eq!(
config
.get("description")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"This application does many things:\n- Feature 1\n- Feature 2\n- Feature 3\n"
);
assert_eq!(
config
.get("summary")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"A brief summary that spans multiple lines but should be folded together.\n"
);
assert_eq!(
config
.get("version")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"1.0"
);
assert_eq!(config.get("enabled").unwrap().to_bool(), Some(true));
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_edge_cases() {
let yaml1 = r#"empty_literal: |
empty_folded: >
"#;
let parsed1 = YamlFile::from_str(yaml1).expect("Should parse this edge case");
let doc1 = parsed1.document().expect("Should have document");
let mapping1 = doc1.as_mapping().expect("Should be a mapping");
assert_eq!(mapping1.len(), 2, "Both keys are entries of their own");
assert_eq!(
mapping1
.get("empty_literal")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
""
);
assert!(mapping1.get("empty_folded").is_some());
assert_eq!(parsed1.to_string(), yaml1);
let yaml2 = r#"whitespace: |
"#;
let parsed2 =
YamlFile::from_str(yaml2).expect("Should parse block scalar with only whitespace");
assert_eq!(parsed2.to_string(), yaml2);
let yaml3 = r#"first: |
Content
second: value
"#;
let parsed3 =
YamlFile::from_str(yaml3).expect("Should parse block scalar followed by other keys");
let doc3 = parsed3.document().expect("Should have document");
let mapping3 = doc3.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping3
.get("first")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Content\n"
);
assert_eq!(
mapping3
.get("second")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"value"
);
let output3 = parsed3.to_string();
assert_eq!(output3, yaml3);
}
#[test]
fn test_literal_block_scalar_advanced_formatting() {
let yaml = r#"poem: |
Roses are red,
Violets are blue,
YAML is great,
And so are you!
This is another stanza
with different content.
And this line has extra indentation.
Back to normal indentation.
Final stanza.
"#;
let parsed = YamlFile::from_str(yaml).expect("Should parse complex literal block scalar");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let poem = mapping.get("poem").expect("Should have 'poem' key");
let expected_content = "Roses are red,\nViolets are blue,\nYAML is great,\nAnd so are you!\n\nThis is another stanza\nwith different content.\n And this line has extra indentation.\nBack to normal indentation.\n\nFinal stanza.\n";
assert_eq!(poem.as_scalar().unwrap().as_string(), expected_content);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_folded_block_scalar_paragraph_handling() {
let yaml = r#"description: >
This is the first paragraph that should
be folded into a single line when processed
by a YAML parser.
This is a second paragraph that should
also be folded but kept separate from
the first paragraph.
This is a third paragraph after
multiple blank lines.
Final paragraph.
"#;
let parsed =
YamlFile::from_str(yaml).expect("Should parse folded block scalar with paragraphs");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let description = mapping
.get("description")
.expect("Should have 'description' key");
let expected_content = "This is the first paragraph that should be folded into a single line when processed by a YAML parser.\nThis is a second paragraph that should also be folded but kept separate from the first paragraph.\nThis is a third paragraph after multiple blank lines.\nFinal paragraph.\n";
assert_eq!(
description.as_scalar().unwrap().as_string(),
expected_content
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalars_with_special_characters() {
let yaml = r#"special_chars: |
Line with colons: key: value
Line with dashes - and more - dashes
Line with quotes "double" and 'single'
Line with brackets [array] and braces {object}
Line with pipes | and greater than >
Line with at @ and hash # symbols
Line with percent % and exclamation !
backslash_test: >
This line has a backslash \ in it
And this line has multiple \\ backslashes
unicode_test: |
This line has unicode: ä½ å¥½ä¸–ç•Œ
And emojis: 🚀 🎉 ✨
"#;
let parsed =
YamlFile::from_str(yaml).expect("Should parse block scalars with special characters");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let special_chars = mapping
.get("special_chars")
.expect("Should have 'special_chars' key");
assert_eq!(
special_chars.as_scalar().unwrap().as_string(),
"Line with colons: key: value\nLine with dashes - and more - dashes\nLine with quotes \"double\" and 'single'\nLine with brackets [array] and braces {object}\nLine with pipes | and greater than >\nLine with at @ and hash # symbols\nLine with percent % and exclamation !\n"
);
let backslash_test = mapping
.get("backslash_test")
.expect("Should have 'backslash_test' key");
assert_eq!(
backslash_test.as_scalar().unwrap().as_string(),
"This line has a backslash \\ in it And this line has multiple \\\\ backslashes\n"
);
let unicode_test = mapping
.get("unicode_test")
.expect("Should have 'unicode_test' key");
assert_eq!(
unicode_test.as_scalar().unwrap().as_string(),
"This line has unicode: ä½ å¥½ä¸–ç•Œ\nAnd emojis: 🚀 🎉 ✨\n"
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_chomping_detailed() {
let yaml_clip = r#"clip: |
Line 1
Line 2
"#;
let parsed_clip =
YamlFile::from_str(yaml_clip).expect("Should parse block scalar with default clipping");
let doc_clip = parsed_clip.document().expect("Should have document");
let mapping_clip = doc_clip.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping_clip
.get("clip")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Line 1\nLine 2\n"
);
assert_eq!(parsed_clip.to_string(), yaml_clip);
let yaml_strip = r#"strip: |-
Line 1
Line 2
"#;
let parsed_strip =
YamlFile::from_str(yaml_strip).expect("Should parse block scalar with strip indicator");
let doc_strip = parsed_strip.document().expect("Should have document");
let mapping_strip = doc_strip.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping_strip
.get("strip")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Line 1\nLine 2"
);
assert_eq!(parsed_strip.to_string(), yaml_strip);
let yaml_keep = r#"keep: |+
Line 1
Line 2
"#;
let parsed_keep =
YamlFile::from_str(yaml_keep).expect("Should parse block scalar with keep indicator");
let doc_keep = parsed_keep.document().expect("Should have document");
let mapping_keep = doc_keep.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping_keep
.get("keep")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Line 1\nLine 2\n\n\n\n"
);
assert_eq!(parsed_keep.to_string(), yaml_keep);
}
#[test]
fn test_block_scalar_explicit_indentation_detailed() {
let yaml1 = r#"indent1: |1
Single space indent
"#;
let parsed1 = YamlFile::from_str(yaml1);
assert!(parsed1.is_ok(), "Should parse |1 block scalar");
let output1 = parsed1.unwrap().to_string();
assert_eq!(output1, yaml1);
let yaml2 = r#"indent2: |2
Two space indent
"#;
let parsed2 = YamlFile::from_str(yaml2);
assert!(parsed2.is_ok(), "Should parse |2 block scalar");
let output2 = parsed2.unwrap().to_string();
assert_eq!(output2, yaml2);
let yaml3 = r#"folded_indent: >2
Two space folded
content spans lines
"#;
let parsed3 = YamlFile::from_str(yaml3);
assert!(parsed3.is_ok(), "Should parse >2 folded block scalar");
let output3 = parsed3.unwrap().to_string();
assert_eq!(output3, yaml3);
}
#[test]
fn test_block_scalar_combined_indicators() {
let yaml = r#"strip_with_indent: |2-
Content with explicit indent
and strip chomping
keep_with_indent: >3+
Content with explicit indent
and keep chomping
folded_strip: >-
Folded content
with strip indicator
literal_keep: |+
Literal content
with keep indicator
"#;
let parsed =
YamlFile::from_str(yaml).expect("Should parse block scalars with combined indicators");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping
.get("strip_with_indent")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Content with explicit indent\nand strip chomping"
);
assert_eq!(
mapping
.get("keep_with_indent")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Content with explicit indent and keep chomping\n\n\n\n"
);
assert_eq!(
mapping
.get("folded_strip")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Folded content with strip indicator"
);
assert_eq!(
mapping
.get("literal_keep")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Literal content\nwith keep indicator\n\n\n"
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_whitespace_and_empty() {
let yaml1 = r#"whitespace_only: |
"#;
let parsed1 =
YamlFile::from_str(yaml1).expect("Should handle block scalar with only whitespace");
let doc1 = parsed1.document().expect("Should have document");
let mapping1 = doc1.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping1
.get("whitespace_only")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"\n"
);
assert_eq!(parsed1.to_string(), yaml1);
let yaml2 = r#"mixed_indent: |
Normal line
Indented line
Back to normal
More indented
Normal again
"#;
let parsed2 = YamlFile::from_str(yaml2).expect("Should handle mixed indentation levels");
let doc2 = parsed2.document().expect("Should have document");
let mapping2 = doc2.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping2
.get("mixed_indent")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Normal line\n Indented line\nBack to normal\n More indented\nNormal again\n"
);
assert_eq!(parsed2.to_string(), yaml2);
let yaml3 = r#"first: |
Content
immediate: value
another: |
More content
final: end
"#;
let parsed3 =
YamlFile::from_str(yaml3).expect("Should handle multiple block scalars in mapping");
let doc3 = parsed3.document().expect("Should have document");
let mapping3 = doc3.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping3
.get("first")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Content\n"
);
assert_eq!(
mapping3
.get("immediate")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"value"
);
assert_eq!(
mapping3
.get("another")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"More content\n"
);
assert_eq!(
mapping3
.get("final")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"end"
);
let output3 = parsed3.to_string();
assert_eq!(output3, yaml3);
}
#[test]
fn test_block_scalar_with_comments() {
let yaml = r#"# Main configuration
config: | # This is a literal block
# This comment is inside the block
line1: value1
# Another internal comment
line2: value2
# Outside comment
other: > # Folded block comment
This content spans
# This hash is part of the content, not a comment
multiple lines
"#;
let parsed = YamlFile::from_str(yaml).expect("Should parse block scalars with comments");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping.get("config").unwrap().as_scalar().unwrap().as_string(),
"# This comment is inside the block\nline1: value1\n# Another internal comment\nline2: value2\n\n# Outside comment\n"
);
assert_eq!(
mapping
.get("other")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"This content spans # This hash is part of the content, not a comment multiple lines\n"
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_empty_and_minimal() {
let yaml = r#"empty_literal: |
empty_folded: >
minimal_literal: |
x
minimal_folded: >
y
just_newlines: |
just_spaces: |
"#;
let parsed =
YamlFile::from_str(yaml).expect("Should handle empty and minimal block scalars");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping
.get("empty_literal")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"\n"
);
assert_eq!(
mapping
.get("empty_folded")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"\n"
);
assert_eq!(
mapping
.get("minimal_literal")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"x\n"
);
assert_eq!(
mapping
.get("minimal_folded")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"y\n"
);
assert_eq!(
mapping
.get("just_newlines")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"\n"
);
assert_eq!(
mapping
.get("just_spaces")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"\n"
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_with_document_markers() {
let yaml = r#"---
doc1: |
This is the first document
with a literal block scalar.
next_key: value
---
doc2: >
This is the second document
with a folded block scalar.
another_key: another_value
...
"#;
let parsed =
YamlFile::from_str(yaml).expect("Should parse block scalars with document markers");
let doc = parsed.document().expect("Should have first document");
let mapping = doc.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping
.get("doc1")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"This is the first document\nwith a literal block scalar.\n"
);
assert_eq!(
mapping
.get("next_key")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"value"
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_formatting_preservation() {
let original = r#"preserve_me: |
Line with multiple spaces
Line with tabs here
Line with trailing spaces
Empty line above and below
Final line
"#;
let parsed = YamlFile::from_str(original).expect("Should preserve exact formatting");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let preserve_me = mapping
.get("preserve_me")
.expect("Should have 'preserve_me' key");
let expected = "Line with multiple spaces\nLine with\ttabs\there\nLine with trailing spaces\n\nEmpty line above and below\n\nFinal line\n";
assert_eq!(preserve_me.as_scalar().unwrap().as_string(), expected);
let output = parsed.to_string();
assert_eq!(output, original);
}
#[test]
fn test_block_scalar_complex_yaml_content() {
let yaml = r#"yaml_content: |
# This block contains YAML-like content
nested:
- item: value
- item: another
mapping:
key1: |
Even more nested literal content
key2: value
anchors: &anchor
anchor_content: data
reference: *anchor
quoted_yaml: >
This folded block contains
YAML structures: {key: value, array: [1, 2, 3]}
that should be treated as plain text.
"#;
let parsed = YamlFile::from_str(yaml)
.expect("Should parse block scalars containing YAML-like structures");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let expected_yaml_content = "# This block contains YAML-like content\nnested:\n - item: value\n - item: another\n\nmapping:\n key1: |\n Even more nested literal content\n key2: value\n\nanchors: &anchor\n anchor_content: data\n\nreference: *anchor\n";
assert_eq!(
mapping
.get("yaml_content")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
expected_yaml_content
);
let expected_quoted_yaml = "This folded block contains YAML structures: {key: value, array: [1, 2, 3]} that should be treated as plain text.\n";
assert_eq!(
mapping
.get("quoted_yaml")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
expected_quoted_yaml
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_performance_large_content() {
let mut large_content = String::new();
for i in 1..=100 {
large_content.push_str(&format!(
" Line number {} with some content that makes it longer\n",
i
));
}
let yaml = format!(
"large_literal: |\n{}\nlarge_folded: >\n{}\n",
large_content, large_content
);
let parsed =
YamlFile::from_str(&yaml).expect("Should parse large block scalars without errors");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let literal_value = mapping
.get("large_literal")
.expect("Should have large_literal key")
.as_scalar()
.expect("Should be scalar")
.as_string();
let mut expected_literal = String::new();
for i in 1..=100 {
expected_literal.push_str(&format!(
"Line number {} with some content that makes it longer\n",
i
));
}
assert_eq!(literal_value, expected_literal);
let folded_value = mapping
.get("large_folded")
.expect("Should have large_folded key")
.as_scalar()
.expect("Should be scalar")
.as_string();
let mut expected_folded = String::new();
for i in 1..=100 {
if i > 1 {
expected_folded.push(' ');
}
expected_folded.push_str(&format!(
"Line number {} with some content that makes it longer",
i
));
}
expected_folded.push('\n');
assert_eq!(folded_value, expected_folded);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_error_recovery() {
let yaml = r#"good_key: value
bad_block: |
incomplete_key
another_good: works
"#;
let err = YamlFile::from_str(yaml).unwrap_err();
assert!(err.to_string().contains("could not be parsed"), "{err}");
}
#[test]
fn test_block_scalar_with_flow_structures() {
let yaml = r#"mixed_styles: |
This literal block contains:
- A flow sequence: [1, 2, 3]
- A flow mapping: {key: value, other: data}
- Mixed content: [a, {nested: true}, c]
flow_then_block:
flow_seq: [item1, item2]
block_literal: |
This comes after flow style
and should work fine.
flow_map: {after: block}
"#;
let parsed = YamlFile::from_str(yaml).expect("Should parse mixed flow and block styles");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let expected_mixed = "This literal block contains:\n- A flow sequence: [1, 2, 3]\n- A flow mapping: {key: value, other: data}\n- Mixed content: [a, {nested: true}, c]\n";
assert_eq!(
mapping
.get("mixed_styles")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
expected_mixed
);
let flow_then_block_value = mapping.get("flow_then_block").unwrap();
let flow_then_block = flow_then_block_value.as_mapping().unwrap();
let flow_seq_value = flow_then_block.get("flow_seq").unwrap();
let flow_seq = flow_seq_value.as_sequence().unwrap();
assert_eq!(flow_seq.len(), 2);
assert_eq!(
flow_seq.get(0).unwrap().as_scalar().unwrap().as_string(),
"item1"
);
assert_eq!(
flow_seq.get(1).unwrap().as_scalar().unwrap().as_string(),
"item2"
);
assert_eq!(
flow_then_block
.get("block_literal")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"This comes after flow style\nand should work fine.\n"
);
let flow_map_value = flow_then_block.get("flow_map").unwrap();
let flow_map = flow_map_value.as_mapping().unwrap();
assert_eq!(
flow_map
.get("after")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"block"
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_indentation_edge_cases() {
let yaml1 = r#"empty: |
next: value"#;
let parsed1 = YamlFile::from_str(yaml1);
assert!(parsed1.is_ok(), "Should handle empty block followed by key");
let yaml2 = r#"inconsistent: |
normal indent
more indent
back to normal
even more
normal
"#;
let parsed2 = YamlFile::from_str(yaml2);
assert!(
parsed2.is_ok(),
"Should handle inconsistent but valid indentation"
);
let yaml3 = "tabs: |\n\tTab indented line\n\tAnother tab line\n";
let parsed3 = YamlFile::from_str(yaml3);
assert!(
parsed3.is_ok(),
"Should handle tab characters in block scalars"
);
}
#[test]
fn test_block_scalar_with_anchors_and_aliases() {
let yaml = r#"template: &template |
This is a template
with multiple lines
that can be referenced.
instance1: *template
instance2:
content: *template
other: value
modified: |
<<: *template
Additional content here
"#;
let parsed =
YamlFile::from_str(yaml).expect("Should parse block scalars with anchors and aliases");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let expected_template =
"This is a template\nwith multiple lines\nthat can be referenced.\n";
let template_value = mapping.get("template").unwrap();
assert_eq!(
template_value.as_scalar().unwrap().as_string(),
expected_template
);
let instance1 = mapping.get("instance1").unwrap();
assert!(
instance1.is_alias(),
"instance1 should be an alias, not a scalar"
);
assert_eq!(instance1.as_alias().unwrap().name(), "template");
let instance2_value = mapping.get("instance2").unwrap();
let instance2 = instance2_value.as_mapping().unwrap();
let content = instance2.get("content").unwrap();
assert!(
content.is_alias(),
"content should be an alias, not a scalar"
);
assert_eq!(content.as_alias().unwrap().name(), "template");
assert_eq!(
instance2
.get("other")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"value"
);
let modified = mapping.get("modified").unwrap();
assert!(
modified.is_scalar(),
"modified should be a scalar, not an alias"
);
assert_eq!(
modified.as_scalar().unwrap().as_string(),
"<<: *template\nAdditional content here\n"
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_newline_variations() {
let yaml_unix = "unix: |\n Line 1\n Line 2\n";
let parsed_unix = YamlFile::from_str(yaml_unix).expect("Should handle Unix newlines");
let yaml_windows = "windows: |\r\n Line 1\r\n Line 2\r\n";
let parsed_windows =
YamlFile::from_str(yaml_windows).expect("Should handle Windows newlines");
let doc_unix = parsed_unix.document().expect("Should have document");
let mapping_unix = doc_unix.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping_unix
.get("unix")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Line 1\nLine 2\n"
);
let doc_windows = parsed_windows.document().expect("Should have document");
let mapping_windows = doc_windows.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping_windows
.get("windows")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"Line 1\nLine 2\n"
);
assert_eq!(parsed_unix.to_string(), yaml_unix);
assert_eq!(parsed_windows.to_string(), yaml_windows);
}
#[test]
fn test_block_scalar_boundary_detection() {
let yaml = r#"config:
description: |
This is a configuration
with multiple lines.
name: "MyApp"
version: 1.0
settings: >
These are settings that
span multiple lines too.
debug: true
"#;
let parsed =
YamlFile::from_str(yaml).expect("Should properly detect block scalar boundaries");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let config_value = mapping.get("config").unwrap();
let config = config_value.as_mapping().unwrap();
assert_eq!(
config
.get("description")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"This is a configuration\nwith multiple lines.\n"
);
assert_eq!(
config.get("name").unwrap().as_scalar().unwrap().as_string(),
"MyApp"
);
assert_eq!(
config
.get("version")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"1.0"
);
assert_eq!(
config
.get("settings")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"These are settings that span multiple lines too.\n"
);
assert_eq!(
config
.get("debug")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"true"
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_with_numeric_content() {
let yaml = r#"numbers_as_text: |
123
45.67
-89
+100
0xFF
1e5
true
false
null
calculations: >
The result is: 2 + 2 = 4
And 10 * 5 = 50
Also: 100% complete
"#;
let parsed = YamlFile::from_str(yaml)
.expect("Should parse numeric content as text in block scalars");
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let expected_numbers = "123\n45.67\n-89\n+100\n0xFF\n1e5\ntrue\nfalse\nnull\n";
assert_eq!(
mapping
.get("numbers_as_text")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
expected_numbers
);
let expected_calculations =
"The result is: 2 + 2 = 4 And 10 * 5 = 50 Also: 100% complete\n";
assert_eq!(
mapping
.get("calculations")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
expected_calculations
);
let output = parsed.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_block_scalar_exact_preservation() {
let test_cases = [
r#"simple: |
Hello World
"#,
r#"folded: >
Hello World
"#,
r#"strip: |-
Content
keep: |+
Content
"#,
r#"explicit: |2
Two space indent
"#,
r#"config:
script: |
#!/bin/bash
echo "Starting deployment"
for service in api web worker; do
echo "Deploying $service"
kubectl apply -f $service.yaml
done
description: >
This configuration defines a deployment
script that will be executed during
the CI/CD pipeline.
"#,
];
for (i, yaml) in test_cases.iter().enumerate() {
let parsed = YamlFile::from_str(yaml);
assert!(parsed.is_ok(), "Test case {} should parse successfully", i);
let output = parsed.unwrap().to_string();
assert_eq!(
output, *yaml,
"Test case {} should preserve exact formatting",
i
);
}
}
#[test]
fn test_block_scalar_chomping_exact() {
let yaml_strip = r#"strip: |-
Content
"#;
let parsed_strip = YamlFile::from_str(yaml_strip).unwrap();
assert_eq!(parsed_strip.to_string(), yaml_strip);
let yaml_keep = r#"keep: |+
Content
"#;
let parsed_keep = YamlFile::from_str(yaml_keep).unwrap();
assert_eq!(parsed_keep.to_string(), yaml_keep);
let yaml_folded_strip = r#"folded: >-
Content
"#;
let parsed_folded_strip = YamlFile::from_str(yaml_folded_strip).unwrap();
assert_eq!(parsed_folded_strip.to_string(), yaml_folded_strip);
}
#[test]
fn test_block_scalar_indentation_exact() {
let yaml1 = r#"indent1: |1
Single space
"#;
let parsed1 = YamlFile::from_str(yaml1).unwrap();
assert_eq!(parsed1.to_string(), yaml1);
let yaml2 = r#"indent2: |2
Two spaces
"#;
let parsed2 = YamlFile::from_str(yaml2).unwrap();
assert_eq!(parsed2.to_string(), yaml2);
let yaml3 = r#"combined: |3+
Content with keep
"#;
let parsed3 = YamlFile::from_str(yaml3).unwrap();
assert_eq!(parsed3.to_string(), yaml3);
}
#[test]
fn test_block_scalar_mapping_exact() {
let yaml = r#"description: |
Line 1
Line 2
summary: >
Folded content
version: "1.0"
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
assert_eq!(parsed.to_string(), yaml);
}
#[test]
fn test_block_scalar_sequence_exact() {
let yaml = r#"items:
- |
First item content
with multiple lines
- >
Second item folded
content
- regular_item
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
assert_eq!(parsed.to_string(), yaml);
}
#[test]
fn test_block_scalar_empty_exact() {
let yaml1 = r#"empty: |
"#;
let parsed1 = YamlFile::from_str(yaml1).unwrap();
assert_eq!(parsed1.to_string(), yaml1);
let yaml2 = r#"empty_folded: >
"#;
let parsed2 = YamlFile::from_str(yaml2).unwrap();
assert_eq!(parsed2.to_string(), yaml2);
}
#[test]
fn test_empty_documents_in_stream() {
let yaml = "---\n---\nkey: value\n---\n...\n";
let parsed = YamlFile::from_str(yaml).unwrap();
assert_eq!(parsed.documents().count(), 3);
assert_eq!(parsed.to_string(), yaml);
}
#[test]
fn test_mixed_document_end_markers() {
let yaml = "---\nfirst: doc\n...\n---\nsecond: doc\n---\nthird: doc\n...\n";
let parsed = YamlFile::from_str(yaml).unwrap();
assert_eq!(parsed.documents().count(), 3);
assert_eq!(parsed.to_string(), yaml);
}
#[test]
fn test_complex_document_stream() {
let yaml = r#"%YAML 1.2
%TAG ! tag:example.com,2000:app/
---
template: &anchor
key: !custom value
instance:
<<: *anchor
extra: data
...
%YAML 1.2
---
- item1
- item2: nested
...
---
literal: |
Block content
Multiple lines
folded: >
Folded content
on multiple lines
...
"#;
let parsed = YamlFile::from_str(yaml).unwrap();
assert_eq!(parsed.documents().count(), 3);
assert_eq!(parsed.to_string(), yaml);
}
#[test]
fn test_number_format_parsing() {
let yaml = YamlFile::from_str("value: 0b1010").unwrap();
assert_eq!(yaml.to_string().trim(), "value: 0b1010");
let yaml = YamlFile::from_str("value: 0B1111").unwrap();
assert_eq!(yaml.to_string().trim(), "value: 0B1111");
let yaml = YamlFile::from_str("value: 0o755").unwrap();
assert_eq!(yaml.to_string().trim(), "value: 0o755");
let yaml = YamlFile::from_str("value: 0O644").unwrap();
assert_eq!(yaml.to_string().trim(), "value: 0O644");
let yaml = YamlFile::from_str("value: -0b1010").unwrap();
assert_eq!(yaml.to_string().trim(), "value: -0b1010");
let yaml = YamlFile::from_str("value: +0o755").unwrap();
assert_eq!(yaml.to_string().trim(), "value: +0o755");
let yaml = YamlFile::from_str("value: 0755").unwrap();
assert_eq!(yaml.to_string().trim(), "value: 0755");
let yaml = YamlFile::from_str("value: 0xFF").unwrap();
assert_eq!(yaml.to_string().trim(), "value: 0xFF");
}
#[test]
fn test_invalid_number_formats_as_strings() {
let yaml = YamlFile::from_str("value: 0b2").unwrap();
assert_eq!(yaml.to_string().trim(), "value: 0b2");
let yaml = YamlFile::from_str("value: 0o9").unwrap();
assert_eq!(yaml.to_string().trim(), "value: 0o9");
let yaml = YamlFile::from_str("value: 0xGH").unwrap();
assert_eq!(yaml.to_string().trim(), "value: 0xGH");
}
#[test]
fn test_number_formats_in_complex_structures() {
let input = r#"
config:
permissions: 0o755
flags: 0b11010
color: 0xFF00FF
count: 42"#;
let yaml = YamlFile::from_str(input).unwrap();
let doc = yaml.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
let config_value = mapping.get("config").unwrap();
let config = config_value.as_mapping().unwrap();
assert_eq!(
config
.get("permissions")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"0o755"
);
assert_eq!(
config
.get("flags")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"0b11010"
);
assert_eq!(
config
.get("color")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"0xFF00FF"
);
assert_eq!(
config
.get("count")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"42"
);
let output = yaml.to_string();
assert_eq!(output, input);
}
#[test]
fn test_editing_operations() {
let yaml = YamlFile::from_str("name: old-name\nversion: 1.0.0").unwrap();
if let Some(doc) = yaml.document() {
doc.set("name", "new-name");
doc.set("version", "2.0.0");
assert_eq!(doc.get_string("name"), Some("new-name".to_string()));
assert_eq!(doc.get_string("version"), Some("2.0.0".to_string()));
let output = doc.to_string();
assert_eq!(output, "name: new-name\nversion: 2.0.0");
}
}
#[test]
fn test_timestamp_parsing_and_validation() {
use crate::scalar::{ScalarType, ScalarValue};
let test_cases = vec![
("2001-12-14 21:59:43.10 -5", true), ("2001-12-15T02:59:43.1Z", true), ("2002-12-14", true), ("2001-12-14t21:59:43.10-05:00", true), ("2001-12-14 21:59:43.10", true), ("2001-12-14T21:59:43", true), ("not-a-timestamp", false), ("2001-13-14", false), ("2001-12-32", false), ];
for (timestamp_str, should_be_valid) in test_cases {
let scalar = ScalarValue::parse(timestamp_str);
if should_be_valid {
assert_eq!(
scalar.scalar_type(),
ScalarType::Timestamp,
"Failed to recognize '{}' as timestamp",
timestamp_str
);
assert!(scalar.is_timestamp());
assert_eq!(scalar.value(), timestamp_str);
let yaml = format!("timestamp: {}", timestamp_str);
let parsed = YamlFile::from_str(&yaml).unwrap();
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping
.get("timestamp")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
timestamp_str,
"Timestamp '{}' not preserved",
timestamp_str
);
let output = parsed.to_string();
assert_eq!(output, yaml);
} else {
assert_ne!(
scalar.scalar_type(),
ScalarType::Timestamp,
"'{}' should not be recognized as timestamp",
timestamp_str
);
}
}
let yaml_with_timestamps = r#"
created_at: 2001-12-14 21:59:43.10 -5
updated_at: 2001-12-15T02:59:43.1Z
date_only: 2002-12-14
timestamps_in_array:
- 2001-12-14 21:59:43.10 -5
- 2001-12-15T02:59:43.1Z
- 2002-12-14"#;
let parsed = YamlFile::from_str(yaml_with_timestamps).unwrap();
let doc = parsed.document().expect("Should have document");
let mapping = doc.as_mapping().expect("Should be a mapping");
assert_eq!(
mapping
.get("created_at")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"2001-12-14 21:59:43.10 -5"
);
assert_eq!(
mapping
.get("updated_at")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"2001-12-15T02:59:43.1Z"
);
assert_eq!(
mapping
.get("date_only")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"2002-12-14"
);
let array_value = mapping.get("timestamps_in_array").unwrap();
let array = array_value.as_sequence().unwrap();
assert_eq!(
array.get(0).unwrap().as_scalar().unwrap().as_string(),
"2001-12-14 21:59:43.10 -5"
);
assert_eq!(
array.get(1).unwrap().as_scalar().unwrap().as_string(),
"2001-12-15T02:59:43.1Z"
);
assert_eq!(
array.get(2).unwrap().as_scalar().unwrap().as_string(),
"2002-12-14"
);
let output = parsed.to_string();
assert_eq!(output, yaml_with_timestamps);
}
#[test]
fn test_regex_support_in_yaml() {
use crate::scalar::{ScalarType, ScalarValue};
let yaml_with_regex = r#"
patterns:
digits: !!regex '\d+'
word: !!regex '\w+'
simple: !!regex 'test'"#;
let parsed = YamlFile::from_str(yaml_with_regex).unwrap();
let output = parsed.to_string();
assert_eq!(output, yaml_with_regex);
let regex_scalar = ScalarValue::regex(r"^\d{4}-\d{2}-\d{2}$");
assert_eq!(regex_scalar.scalar_type(), ScalarType::Regex);
assert!(regex_scalar.is_regex());
assert_eq!(regex_scalar.value(), r"^\d{4}-\d{2}-\d{2}$");
assert_eq!(
regex_scalar.to_yaml_string(),
r"!!regex ^\d{4}-\d{2}-\d{2}$"
);
let yaml_simple = "pattern: !!regex '\\d+'";
let parsed_simple = YamlFile::from_str(yaml_simple).unwrap();
let doc_simple = parsed_simple.document().expect("Should have document");
let mapping_simple = doc_simple.as_mapping().expect("Should be a mapping");
let pattern_value = mapping_simple
.get("pattern")
.expect("Should have pattern key");
assert!(pattern_value.is_tagged(), "pattern should be a tagged node");
let tagged = pattern_value.as_tagged().expect("Should be tagged");
assert_eq!(tagged.tag(), Some("!!regex".to_string()));
assert_eq!(tagged.as_string(), Some("\\d+".to_string()));
let output_simple = parsed_simple.to_string();
assert_eq!(output_simple, yaml_simple);
let complex_regex = r#"validation: !!regex '^https?://(?:[-\w.])+(?:\:[0-9]+)?'"#;
let parsed_complex = YamlFile::from_str(complex_regex).unwrap();
let doc_complex = parsed_complex.document().expect("Should have document");
let mapping_complex = doc_complex.as_mapping().expect("Should be a mapping");
let validation_value = mapping_complex
.get("validation")
.expect("Should have validation key");
assert!(
validation_value.is_tagged(),
"validation should be a tagged node"
);
let tagged_complex = validation_value.as_tagged().expect("Should be tagged");
assert_eq!(tagged_complex.tag(), Some("!!regex".to_string()));
assert_eq!(
tagged_complex.as_string(),
Some("^https?://(?:[-\\w.])+(?:\\:[0-9]+)?".to_string())
);
let output_complex = parsed_complex.to_string();
assert_eq!(output_complex, complex_regex);
}
#[test]
fn test_regex_in_different_contexts() {
let yaml_sequence = r#"
patterns:
- !!regex '\d+'
- !!regex '[a-z]+'
- normal_string
- !!regex '.*@.*\..*'
"#;
let parsed_seq = YamlFile::from_str(yaml_sequence).unwrap();
let doc_seq = parsed_seq.document().expect("Should have document");
let mapping_seq = doc_seq.as_mapping().expect("Should be a mapping");
let patterns_value = mapping_seq
.get("patterns")
.expect("Should have patterns key");
let patterns = patterns_value.as_sequence().expect("Should be a sequence");
assert!(patterns.get(0).unwrap().is_tagged());
assert_eq!(
patterns.get(0).unwrap().as_tagged().unwrap().tag(),
Some("!!regex".to_string())
);
assert!(patterns.get(1).unwrap().is_tagged());
assert_eq!(
patterns.get(1).unwrap().as_tagged().unwrap().tag(),
Some("!!regex".to_string())
);
assert!(!patterns.get(2).unwrap().is_tagged());
assert_eq!(
patterns.get(2).unwrap().as_scalar().unwrap().as_string(),
"normal_string"
);
assert!(patterns.get(3).unwrap().is_tagged());
assert_eq!(
patterns.get(3).unwrap().as_tagged().unwrap().tag(),
Some("!!regex".to_string())
);
let output_seq = parsed_seq.to_string();
assert_eq!(output_seq, yaml_sequence);
let yaml_nested = r#"
validation:
email: !!regex '[^@]+@[^@]+\.[a-z]+'
phone: !!regex '\d{3}-\d{3}-\d{4}'
config:
debug_pattern: !!regex 'DEBUG:.*'
nested:
deep_pattern: !!regex 'ERROR'
"#;
let parsed_nested = YamlFile::from_str(yaml_nested).unwrap();
let output_nested = parsed_nested.to_string();
assert_eq!(output_nested, yaml_nested);
let yaml_mixed = r#"
mixed_collection:
- name: "test"
patterns: [!!regex '\d+', !!regex '\w+']
- patterns:
simple: !!regex 'test'
complex: !!regex '^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$'
"#;
let parsed_mixed = YamlFile::from_str(yaml_mixed).unwrap();
let output_mixed = parsed_mixed.to_string();
assert_eq!(output_mixed, yaml_mixed);
let yaml_flow =
r#"inline_patterns: {email: !!regex '[^@]+@[^@]+', phone: !!regex '\d{3}-\d{4}'}"#;
let parsed_flow = YamlFile::from_str(yaml_flow).unwrap();
let output_flow = parsed_flow.to_string();
assert_eq!(output_flow, yaml_flow);
}
#[test]
fn test_regex_parsing_edge_cases() {
let yaml_quotes = r#"
patterns:
single_quoted: !!regex 'pattern with spaces'
double_quoted: !!regex "pattern_without_escapes"
unquoted: !!regex simple_pattern
"#;
let parsed_quotes = YamlFile::from_str(yaml_quotes).unwrap();
let output_quotes = parsed_quotes.to_string();
assert_eq!(output_quotes, yaml_quotes);
let yaml_empty = r#"
empty: !!regex ''
whitespace: !!regex ' '
tabs: !!regex ' '
"#;
let parsed_empty =
YamlFile::from_str(yaml_empty).expect("Should parse empty/whitespace regex patterns");
let output_empty = parsed_empty.to_string();
assert_eq!(output_empty, yaml_empty);
let yaml_special = r#"special: !!regex 'pattern_with_underscores_and_123'"#;
let parsed_special = YamlFile::from_str(yaml_special)
.expect("Should parse regex with safe special characters");
let output_special = parsed_special.to_string();
assert_eq!(output_special, yaml_special);
let yaml_verify = r#"test_pattern: !!regex '\d{4}-\d{2}-\d{2}'"#;
let parsed_verify = YamlFile::from_str(yaml_verify).unwrap();
let output_verify = parsed_verify.to_string();
assert_eq!(output_verify, yaml_verify);
let yaml_multiple = r#"
patterns:
email: !!regex '^[^\s@]+@[^\s@]+\.[^\s@]+$'
phone: !!regex '^\+?[\d\s\-\(\)]{10,}$'
url: !!regex '^https?://[^\s]+$'
ipv4: !!regex '^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$'
uuid: !!regex '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
"#;
let parsed_multiple =
YamlFile::from_str(yaml_multiple).expect("Should parse multiple regex patterns");
let output_multiple = parsed_multiple.to_string();
assert_eq!(output_multiple, yaml_multiple);
}
#[test]
fn test_enhanced_comment_support() {
let yaml1 = r#"flow_seq: [
item1, # comment after item1
item2, # comment after item2
item3 # comment after item3
]"#;
let parsed1 = YamlFile::from_str(yaml1).unwrap();
let output1 = parsed1.to_string();
assert_eq!(output1, yaml1);
let yaml2 = r#"flow_map: {
key1: val1, # comment after first pair
key2: val2, # comment after second pair
key3: val3 # comment after third pair
}"#;
let parsed2 = YamlFile::from_str(yaml2).unwrap();
let output2 = parsed2.to_string();
assert_eq!(output2, yaml2);
let yaml3 = r#"config:
servers: [
{name: web1, port: 80}, # Web server 1
{name: web2, port: 80}, # Web server 2
{name: db1, port: 5432} # Database server
] # End servers array"#;
let parsed3 = YamlFile::from_str(yaml3).unwrap();
let output3 = parsed3.to_string();
assert_eq!(output3, yaml3);
let yaml4 = r#"items:
- first # First item comment
- second # Second item comment
# Comment between items
- third # Third item comment"#;
let parsed4 = YamlFile::from_str(yaml4).unwrap();
let output4 = parsed4.to_string();
assert_eq!(output4, yaml4);
for yaml in [yaml1, yaml2, yaml3, yaml4] {
let parsed = YamlFile::from_str(yaml).unwrap();
let output = parsed.to_string();
let reparsed = YamlFile::from_str(&output);
assert!(reparsed.is_ok(), "Round-trip parsing should succeed");
}
}
#[test]
fn test_insert_after_with_sequence() {
let yaml = "name: project\nversion: 1.0.0";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
let features = SequenceBuilder::new()
.item("feature1")
.item("feature2")
.item("feature3")
.build_document()
.as_sequence()
.unwrap();
let success = doc.insert_after("name", "features", features);
assert!(success, "insert_after should succeed");
let output = doc.to_string();
let expected = r#"name: project
features:
- feature1
- feature2
- feature3
version: 1.0.0"#;
assert_eq!(output.trim(), expected);
let reparsed = YamlFile::from_str(&output);
assert!(reparsed.is_ok(), "Output should be valid YAML");
}
#[test]
fn test_insert_before_with_mapping() {
let yaml = "name: project\nversion: 1.0.0";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
let database = MappingBuilder::new()
.pair("host", "localhost")
.pair("port", 5432)
.pair("database", "mydb")
.build_document()
.as_mapping()
.unwrap();
let success = doc.insert_before("version", "database", database);
assert!(success, "insert_before should succeed");
let output = doc.to_string();
let expected = r#"name: project
database:
host: localhost
port: 5432
database: mydb
version: 1.0.0"#;
assert_eq!(output.trim(), expected);
let reparsed = YamlFile::from_str(&output).expect("Output should be valid YAML");
let reparsed_doc = reparsed.document().expect("Should have document");
let reparsed_mapping = reparsed_doc.as_mapping().expect("Should be mapping");
let db_value = reparsed_mapping
.get("database")
.expect("Should have database key");
let db_mapping = db_value.as_mapping().expect("database should be mapping");
assert_eq!(
db_mapping
.get("host")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"localhost"
);
assert_eq!(
db_mapping
.get("port")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"5432"
);
}
#[test]
fn test_insert_at_index_with_mixed_types() {
let yaml = "name: project";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
doc.insert_at_index(1, "version", "1.0.0");
doc.insert_at_index(2, "active", true);
doc.insert_at_index(3, "count", 42);
let features = SequenceBuilder::new()
.item("auth")
.item("logging")
.build_document()
.as_sequence()
.unwrap();
doc.insert_at_index(4, "features", features);
let output = doc.to_string();
let expected = r#"name: project
version: 1.0.0
active: true
count: 42
features:
- auth
- logging"#;
assert_eq!(output.trim(), expected);
let reparsed = YamlFile::from_str(&output).expect("Output should be valid YAML");
let reparsed_doc = reparsed.document().expect("Should have document");
let reparsed_mapping = reparsed_doc.as_mapping().expect("Should be mapping");
assert_eq!(
reparsed_mapping
.get("version")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"1.0.0"
);
assert_eq!(
reparsed_mapping.get("active").unwrap().to_bool(),
Some(true)
);
assert_eq!(reparsed_mapping.get("count").unwrap().to_i64(), Some(42));
let features_value = reparsed_mapping.get("features").unwrap();
let features = features_value.as_sequence().unwrap();
assert_eq!(
features.get(0).unwrap().as_scalar().unwrap().as_string(),
"auth"
);
assert_eq!(
features.get(1).unwrap().as_scalar().unwrap().as_string(),
"logging"
);
}
#[test]
fn test_insert_with_null_and_special_scalars() {
let yaml = "name: project";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
doc.insert_after("name", "null_value", ScalarValue::null());
doc.insert_after("null_value", "empty_string", "");
doc.insert_after("empty_string", "number", 1.234);
doc.insert_after("number", "boolean", false);
let output = doc.to_string();
let expected = r#"name: project
null_value: null
empty_string: ''
number: 1.234
boolean: false"#;
assert_eq!(output.trim(), expected);
let reparsed = YamlFile::from_str(&output).expect("Output should be valid YAML");
let reparsed_doc = reparsed.document().expect("Should have document");
let reparsed_mapping = reparsed_doc.as_mapping().expect("Should be mapping");
assert_eq!(
reparsed_mapping
.get("name")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"project"
);
assert!(reparsed_mapping
.get("null_value")
.unwrap()
.as_scalar()
.unwrap()
.is_null());
assert_eq!(
reparsed_mapping
.get("empty_string")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
""
);
assert_eq!(
reparsed_mapping.get("number").unwrap().to_f64(),
Some(1.234)
);
assert_eq!(
reparsed_mapping.get("boolean").unwrap().to_bool(),
Some(false)
);
}
#[test]
fn test_insert_ordering_preservation() {
let yaml = "first: 1\nthird: 3\nfifth: 5";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
doc.insert_after("first", "second", 2);
doc.insert_before("fifth", "fourth", 4);
let output = doc.to_string();
let expected = r#"first: 1
second: 2
third: 3
fourth: 4
fifth: 5"#;
assert_eq!(output.trim(), expected);
let reparsed = YamlFile::from_str(&output);
assert!(reparsed.is_ok(), "Output should be valid YAML");
}
#[test]
fn test_insert_with_yamlvalue_positioning() {
let yaml = "name: project\nversion: 1.0\nactive: true";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
let success = doc.insert_after("name", "description", "A sample project");
assert!(success, "Should find string key");
let success = doc.insert_after(1.0, "build", "gradle");
assert!(
!success,
"Should not find numeric key (1.0) when actual key is string 'version'"
);
let success = doc.insert_after(true, "test", "enabled");
assert!(
!success,
"Should not find boolean key (true) when actual key is string 'active'"
);
let bool_string_key = "true";
let success = doc.insert_after(bool_string_key, "test_mode", "development");
assert!(!success, "Should not find 'true' key when value is true");
let output = doc.to_string();
let expected = "name: project\ndescription: A sample project\nversion: 1.0\nactive: true";
assert_eq!(output, expected);
}
#[test]
fn test_insert_complex_nested_structure() {
let yaml = "name: project";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
let config = MappingBuilder::new()
.mapping("server", |m| m.pair("host", "0.0.0.0").pair("port", 8080))
.pair("debug", true)
.sequence("features", |s| s.item("api").item("web").item("cli"))
.build_document()
.as_mapping()
.unwrap();
doc.insert_after("name", "config", config);
let output = doc.to_string();
let expected = "name: project\nconfig:\n server: \n host: 0.0.0.0\n port: 8080\n debug: true\n features: \n - api\n - web\n - cli\n";
assert_eq!(output, expected);
let reparsed = YamlFile::from_str(&output).expect("Output should be valid YAML");
let reparsed_doc = reparsed.document().expect("Should have document");
let reparsed_mapping = reparsed_doc.as_mapping().expect("Should be mapping");
let config_value = reparsed_mapping
.get("config")
.expect("Should have config key");
let config_mapping = config_value.as_mapping().expect("config should be mapping");
let server_value = config_mapping
.get("server")
.expect("Should have server key");
let server = server_value.as_mapping().expect("server should be mapping");
assert_eq!(
server.get("host").unwrap().as_scalar().unwrap().as_string(),
"0.0.0.0"
);
assert_eq!(server.get("port").unwrap().to_i64(), Some(8080));
assert_eq!(config_mapping.get("debug").unwrap().to_bool(), Some(true));
let features_value = config_mapping
.get("features")
.expect("Should have features key");
let features = features_value
.as_sequence()
.expect("features should be sequence");
assert_eq!(features.len(), 3);
assert_eq!(
features.get(0).unwrap().as_scalar().unwrap().as_string(),
"api"
);
assert_eq!(
features.get(1).unwrap().as_scalar().unwrap().as_string(),
"web"
);
assert_eq!(
features.get(2).unwrap().as_scalar().unwrap().as_string(),
"cli"
);
}
#[test]
fn test_insert_with_yaml_sets() {
let yaml = "name: project";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
let mut tags = std::collections::BTreeSet::new();
tags.insert("production".to_string());
tags.insert("database".to_string());
tags.insert("web".to_string());
let yaml_set = YamlValue::from_set(tags);
doc.insert_after("name", "tags", yaml_set);
let output = doc.to_string();
let expected =
"name: project\ntags: !!set\n database: null\n production: null\n web: null\n";
assert_eq!(output, expected);
let reparsed = YamlFile::from_str(&output).expect("Output should be valid YAML");
let reparsed_doc = reparsed.document().expect("Should have document");
let reparsed_mapping = reparsed_doc.as_mapping().expect("Should be mapping");
let tags_value = reparsed_mapping.get("tags").expect("Should have tags key");
assert!(tags_value.is_tagged(), "tags should be tagged");
let tagged = tags_value.as_tagged().expect("Should be tagged node");
assert_eq!(tagged.tag(), Some("!!set".to_string()));
let tags_syntax = tagged.syntax();
let tags_mapping = tags_syntax
.children()
.find_map(Mapping::cast)
.expect("Set should have mapping child");
assert!(tags_mapping
.get("database")
.unwrap()
.as_scalar()
.unwrap()
.is_null());
assert!(tags_mapping
.get("production")
.unwrap()
.as_scalar()
.unwrap()
.is_null());
assert!(tags_mapping
.get("web")
.unwrap()
.as_scalar()
.unwrap()
.is_null());
}
#[test]
fn test_insert_with_ordered_mappings() {
let yaml = "name: project";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
let ordered_steps = vec![
("compile".to_string(), YamlValue::from("gcc main.c")),
("test".to_string(), YamlValue::from("./a.out test")),
(
"package".to_string(),
YamlValue::from("tar -czf app.tar.gz ."),
),
];
let yaml_omap = YamlValue::from_ordered_mapping(ordered_steps);
doc.insert_after("name", "build_steps", yaml_omap);
let output = doc.to_string();
let expected = "name: project\nbuild_steps: !!omap\n - compile: gcc main.c\n - test: ./a.out test\n - package: tar -czf app.tar.gz .\n";
assert_eq!(output, expected);
let reparsed = YamlFile::from_str(&output).expect("Output should be valid YAML");
let reparsed_doc = reparsed.document().expect("Should have document");
let reparsed_mapping = reparsed_doc.as_mapping().expect("Should be mapping");
let steps_value = reparsed_mapping
.get("build_steps")
.expect("Should have build_steps key");
assert!(steps_value.is_tagged(), "build_steps should be tagged");
let tagged = steps_value.as_tagged().expect("Should be tagged node");
assert_eq!(tagged.tag(), Some("!!omap".to_string()));
let steps_syntax = tagged.syntax();
let steps_seq = steps_syntax
.children()
.find_map(Sequence::cast)
.expect("Omap should have sequence child");
assert_eq!(steps_seq.len(), 3);
let compile_value = steps_seq.get(0).unwrap();
let compile_item = compile_value.as_mapping().expect("Should be mapping");
assert_eq!(
compile_item
.get("compile")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"gcc main.c"
);
let test_value = steps_seq.get(1).unwrap();
let test_item = test_value.as_mapping().expect("Should be mapping");
assert_eq!(
test_item
.get("test")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"./a.out test"
);
let package_value = steps_seq.get(2).unwrap();
let package_item = package_value.as_mapping().expect("Should be mapping");
assert_eq!(
package_item
.get("package")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"tar -czf app.tar.gz ."
);
}
#[test]
fn test_insert_with_pairs() {
let yaml = "name: project";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
let connection_attempts = vec![
("server".to_string(), YamlValue::from("primary.db")),
("server".to_string(), YamlValue::from("secondary.db")), ("server".to_string(), YamlValue::from("tertiary.db")), ("timeout".to_string(), YamlValue::from(30)),
];
let yaml_pairs = YamlValue::from_pairs(connection_attempts);
doc.insert_after("name", "connections", yaml_pairs);
let output = doc.to_string();
let expected = "name: project\nconnections: !!pairs\n - server: primary.db\n - server: secondary.db\n - server: tertiary.db\n - timeout: 30\n";
assert_eq!(output, expected);
let reparsed = YamlFile::from_str(&output).expect("Output should be valid YAML");
let reparsed_doc = reparsed.document().expect("Should have document");
let reparsed_mapping = reparsed_doc.as_mapping().expect("Should be mapping");
let connections_value = reparsed_mapping
.get("connections")
.expect("Should have connections key");
assert!(
connections_value.is_tagged(),
"connections should be tagged"
);
let tagged = connections_value
.as_tagged()
.expect("Should be tagged node");
assert_eq!(tagged.tag(), Some("!!pairs".to_string()));
let connections_syntax = tagged.syntax();
let connections_seq = connections_syntax
.children()
.find_map(Sequence::cast)
.expect("Pairs should have sequence child");
assert_eq!(connections_seq.len(), 4);
let server1_val = connections_seq.get(0).unwrap();
let server1 = server1_val.as_mapping().expect("Should be mapping");
assert_eq!(
server1
.get("server")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"primary.db"
);
let server2_val = connections_seq.get(1).unwrap();
let server2 = server2_val.as_mapping().expect("Should be mapping");
assert_eq!(
server2
.get("server")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"secondary.db"
);
let server3_val = connections_seq.get(2).unwrap();
let server3 = server3_val.as_mapping().expect("Should be mapping");
assert_eq!(
server3
.get("server")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"tertiary.db"
);
let timeout_val = connections_seq.get(3).unwrap();
let timeout = timeout_val.as_mapping().expect("Should be mapping");
assert_eq!(timeout.get("timeout").unwrap().to_i64(), Some(30));
}
#[test]
fn test_insert_with_empty_collections() {
let yaml1 = "name: project";
let parsed1 = YamlFile::from_str(yaml1).unwrap();
let doc1 = parsed1.document().expect("Should have a document");
let empty_seq_yaml = YamlFile::from_str("[]").unwrap();
let empty_list = empty_seq_yaml.document().unwrap().as_sequence().unwrap();
doc1.insert_after("name", "empty_list", empty_list);
let output1 = doc1.to_string();
assert_eq!(output1, "name: project\nempty_list: []\n");
let yaml2 = "name: project";
let parsed2 = YamlFile::from_str(yaml2).unwrap();
let doc2 = parsed2.document().expect("Should have a document");
let empty_map_yaml = YamlFile::from_str("{}").unwrap();
let empty_map = empty_map_yaml.document().unwrap().as_mapping().unwrap();
doc2.insert_after("name", "empty_map", empty_map);
let output2 = doc2.to_string();
assert_eq!(output2, "name: project\nempty_map: {}\n");
let yaml3 = "name: project";
let parsed3 = YamlFile::from_str(yaml3).unwrap();
let doc3 = parsed3.document().expect("Should have a document");
doc3.insert_after("name", "empty_set", YamlValue::set());
let output3 = doc3.to_string();
assert_eq!(output3, "name: project\nempty_set: !!set {}\n");
assert!(
YamlFile::from_str(&output1).is_ok(),
"Empty sequence output should be valid YAML"
);
assert!(
YamlFile::from_str(&output2).is_ok(),
"Empty mapping output should be valid YAML"
);
assert!(
YamlFile::from_str(&output3).is_ok(),
"Empty set output should be valid YAML"
);
}
#[test]
fn test_insert_with_deeply_nested_sequences() {
let yaml = "name: project";
let parsed = YamlFile::from_str(yaml).unwrap();
let doc = parsed.document().expect("Should have a document");
let nested_data = SequenceBuilder::new()
.item("item1")
.mapping(|m| {
m.sequence("config", |s| s.item("debug").item(true).item(8080))
.pair("name", "service")
})
.item(42)
.build_document()
.as_sequence()
.unwrap();
doc.insert_after("name", "nested_data", nested_data);
let output = doc.to_string();
let expected = "name: project\nnested_data:\n - item1\n - \n config: \n - debug\n - true\n - 8080\n name: service- 42\n";
assert_eq!(output, expected);
let reparsed = YamlFile::from_str(&output).expect("Output should be valid YAML");
let reparsed_doc = reparsed.document().expect("Should have document");
let reparsed_mapping = reparsed_doc.as_mapping().expect("Should be mapping");
let nested_value = reparsed_mapping
.get("nested_data")
.expect("Should have nested_data key");
let nested_seq = nested_value.as_sequence().expect("Should be sequence");
assert_eq!(nested_seq.len(), 2);
assert_eq!(
nested_seq.get(0).unwrap().as_scalar().unwrap().as_string(),
"item1"
);
let second_item = nested_seq.get(1).unwrap();
let second_mapping = second_item.as_mapping().expect("Should be mapping");
let config_value = second_mapping.get("config").expect("Should have config");
let config_seq = config_value
.as_sequence()
.expect("config should be sequence");
assert_eq!(config_seq.len(), 3);
assert_eq!(
config_seq.get(0).unwrap().as_scalar().unwrap().as_string(),
"debug"
);
assert_eq!(config_seq.get(1).unwrap().to_bool(), Some(true));
assert_eq!(config_seq.get(2).unwrap().to_i64(), Some(8080));
assert_eq!(
second_mapping
.get("name")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"service- 42"
);
}
#[test]
fn test_ast_preservation_comments_in_mapping() {
let yaml = r#"---
# Header comment
key1: value1 # Inline comment 1
# Middle comment
key2: value2 # Inline comment 2
# Footer comment
"#;
let doc = YamlFile::from_str(yaml).unwrap().document().unwrap();
if let Some(mapping) = doc.as_mapping() {
mapping.move_after("key1", "new_key", "new_value");
}
let result = doc.to_string();
let expected = r#"---
# Header comment
key1: value1 # Inline comment 1
new_key: new_value
# Middle comment
key2: value2 # Inline comment 2
# Footer comment
"#;
assert_eq!(result, expected);
}
#[test]
fn test_ast_preservation_whitespace_in_mapping() {
let yaml = r#"---
key1: value1
key2: value2
"#;
let doc = YamlFile::from_str(yaml).unwrap().document().unwrap();
if let Some(mapping) = doc.as_mapping() {
mapping.move_after("key1", "new_key", "new_value");
}
let result = doc.to_string();
let expected = r#"---
key1: value1
new_key: new_value
key2: value2
"#;
assert_eq!(result, expected);
}
#[test]
fn test_ast_preservation_complex_structure() {
let yaml = r#"---
# Configuration file
database: # Database settings
host: localhost # Default host
port: 5432 # Default port
# Connection pool settings
pool:
min: 1 # Minimum connections
max: 10 # Maximum connections
# Server configuration
server:
port: 8080 # HTTP port
"#;
let doc = YamlFile::from_str(yaml).unwrap().document().unwrap();
eprintln!("===========\n");
if let Some(mapping) = doc.as_mapping() {
mapping.move_after("database", "logging", "debug");
}
let result = doc.to_string();
let expected = r#"---
# Configuration file
database: # Database settings
host: localhost # Default host
port: 5432 # Default port
# Connection pool settings
pool:
min: 1 # Minimum connections
max: 10 # Maximum connections
logging: debug
# Server configuration
server:
port: 8080 # HTTP port
"#;
assert_eq!(result, expected);
}
mod lookahead_tests {
use crate::lex::lex;
use crate::parser::has_implicit_mapping_pattern;
fn check_implicit_mapping(yaml: &str) -> bool {
let flow = format!("[{yaml}]");
let tokens = lex(&flow);
has_implicit_mapping_pattern(tokens[1..tokens.len() - 1].iter().map(|(kind, _)| *kind))
}
#[test]
fn test_simple_implicit_mapping() {
assert!(check_implicit_mapping("'key' : value"));
}
#[test]
fn test_simple_value_no_mapping() {
assert!(!check_implicit_mapping("value ,"));
}
#[test]
fn test_value_with_comma() {
assert!(!check_implicit_mapping("value , more"));
}
#[test]
fn test_nested_sequence_as_key() {
assert!(check_implicit_mapping("[a, b]: value"));
}
#[test]
fn test_nested_mapping_not_implicit() {
assert!(!check_implicit_mapping("{a: 1}, b"));
}
#[test]
fn test_deeply_nested_key() {
assert!(check_implicit_mapping("[[a]]: value"));
}
#[test]
fn test_with_whitespace() {
assert!(check_implicit_mapping("'key' : value"));
}
#[test]
fn test_mapping_value_in_sequence() {
assert!(!check_implicit_mapping("a, {key: value}"));
}
#[test]
fn test_multiple_colons() {
assert!(check_implicit_mapping("key1: value1, key2: value2"));
}
#[test]
fn test_url_not_mapping() {
assert!(!check_implicit_mapping("http://example.com"));
}
}
#[test]
fn test_from_str_relaxed_valid() {
let (yaml_file, errors) = YamlFile::from_str_relaxed("key: value\n");
assert!(errors.is_empty());
let doc = yaml_file.document().unwrap();
let mapping = doc.as_mapping().unwrap();
assert_eq!(
mapping.get("key").unwrap().as_scalar().unwrap().to_string(),
"value"
);
}
#[test]
fn test_from_str_relaxed_unclosed_flow_sequence() {
let (yaml_file, errors) = YamlFile::from_str_relaxed("key: [a, b");
assert!(!errors.is_empty());
let doc = yaml_file.document().unwrap();
assert!(doc.as_mapping().is_some());
}
#[test]
fn test_from_str_relaxed_unclosed_flow_mapping() {
let (yaml_file, errors) = YamlFile::from_str_relaxed("key: {a: 1");
assert!(!errors.is_empty());
let doc = yaml_file.document().unwrap();
assert!(doc.as_mapping().is_some());
}
#[test]
fn test_from_str_relaxed_preserves_valid_content() {
let input = "good: value\nbad: [unclosed\nalso_good: 42\n";
let (yaml_file, errors) = YamlFile::from_str_relaxed(input);
assert!(!errors.is_empty());
let doc = yaml_file.document().unwrap();
let mapping = doc.as_mapping().unwrap();
assert_eq!(
mapping
.get("good")
.unwrap()
.as_scalar()
.unwrap()
.to_string(),
"value"
);
}
#[test]
fn test_from_str_relaxed_empty_input() {
let (yaml_file, errors) = YamlFile::from_str_relaxed("");
assert!(errors.is_empty());
assert!(yaml_file.document().is_none());
}
#[test]
fn test_parse_flow_sequence_with_stray_close_brace_does_not_hang() {
let parse = crate::Parse::parse_yaml("[}9[Z");
let _ = parse.tree();
assert!(parse.has_errors());
}
#[test]
fn test_parse_flow_mapping_with_stray_close_bracket_does_not_hang() {
let parse = crate::Parse::parse_yaml("{]");
let _ = parse.tree();
assert!(parse.has_errors());
}
#[test]
fn test_parse_explicit_key_multiline_with_comment_does_not_hang() {
let input = "?\u{18}\0\0\0\0\0\0\n\t#\t\t?\t";
let parse = crate::Parse::parse_yaml(input);
let _ = parse.tree();
}
#[test]
fn test_parse_deeply_nested_flow_mapping_does_not_blow_up() {
let input = "{".repeat(2000);
let parse = crate::Parse::parse_yaml(&input);
let _ = parse.tree();
assert!(parse.has_errors());
}
#[test]
fn test_parse_deeply_nested_flow_sequence_does_not_blow_up() {
let input = "[".repeat(2000);
let parse = crate::Parse::parse_yaml(&input);
let _ = parse.tree();
assert!(parse.has_errors());
}
#[test]
fn test_parse_deeply_nested_block_mapping_does_not_blow_up() {
let mut input = String::new();
for i in 0..2000 {
input.push_str(&" ".repeat(i));
input.push_str("k:\n");
}
input.push_str(&" ".repeat(2000));
input.push_str("v\n");
let parse = crate::Parse::parse_yaml(&input);
let _ = parse.tree();
assert!(parse.has_errors());
}
#[test]
fn test_parse_stacked_anchors_does_not_blow_up() {
let input = format!("{}v", "&x ".repeat(2000));
let parse = crate::Parse::parse_yaml(&input);
let _ = parse.tree();
assert!(parse.has_errors());
}
#[test]
fn test_parse_explicit_key_followed_by_question_in_value_does_not_hang() {
let input = "<<?: ?: \n<<\n ?: : 0\n:@";
let parse = crate::Parse::parse_yaml(input);
let _ = parse.tree();
}
#[test]
fn test_parse_complex_key_mapping_with_stray_close_bracket_does_not_hang() {
let input = "[%:%:]: \n]: \n\n";
let parse = crate::Parse::parse_yaml(input);
let _ = parse.tree();
}
#[test]
fn test_parse_mapping_with_stray_close_brace_does_not_hang() {
let input = "\0:\n\n}:\n\n{";
let parse = crate::Parse::parse_yaml(input);
let _ = parse.tree();
assert!(parse.has_errors());
}
#[test]
fn test_tag_only_key() {
let yaml = YamlFile::from_str("!!str a: b\n").unwrap();
let doc = yaml.document().unwrap();
let mapping = doc.as_mapping().expect("root should be a mapping");
assert_eq!(mapping.len(), 1);
assert_eq!(
mapping.get("a").unwrap().to_string().trim(),
"b",
"value for 'a' should be 'b'"
);
}
#[test]
fn test_tag_and_anchor_key() {
let yaml = YamlFile::from_str("!!str &a1 \"foo\": bar\n").unwrap();
let doc = yaml.document().unwrap();
let mapping = doc.as_mapping().expect("root should be a mapping");
assert_eq!(mapping.len(), 1);
assert_eq!(mapping.get("foo").unwrap().to_string().trim(), "bar");
}
#[test]
fn test_anchor_only_key() {
let yaml = YamlFile::from_str("&anchor a: b\n").unwrap();
let doc = yaml.document().unwrap();
let mapping = doc.as_mapping().expect("root should be a mapping");
assert_eq!(mapping.len(), 1);
assert_eq!(mapping.get("a").unwrap().to_string().trim(), "b");
}
#[test]
fn test_tag_annotated_value_still_wraps() {
let yaml = YamlFile::from_str("c: !!int 42\n").unwrap();
let doc = yaml.document().unwrap();
let mapping = doc.as_mapping().unwrap();
let value = mapping.get("c").unwrap();
assert!(
value.to_string().contains("42"),
"value for 'c' should contain the scalar 42: {:?}",
value.to_string()
);
}
#[test]
fn test_explicit_key_produces_zero_width_null_value() {
use crate::lex::SyntaxKind;
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!(
terminal.kind() == SyntaxKind::NULL && terminal.text().is_empty(),
"expected zero-width NULL terminal, got {:?} = {:?}",
terminal.kind(),
terminal.text()
);
}
#[test]
fn test_block_scalar_under_tagged_node_validates() {
let yaml_text = "k: !!str >\n wrapped\n paragraph\n";
let yaml = YamlFile::from_str(yaml_text).unwrap();
let doc = yaml.document().unwrap();
let mapping = doc.as_mapping().unwrap();
let value = mapping.get("k").expect("k should exist");
assert!(
value.to_string().contains("wrapped"),
"tagged block scalar value should include 'wrapped': {:?}",
value.to_string()
);
}
}