use std::fmt;
use deser_value::{Kind, Map, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct YamlError(String);
impl fmt::Display for YamlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for YamlError {}
#[derive(Debug)]
pub struct YamlDocument(Value);
impl YamlDocument {
#[must_use]
pub fn root(&self) -> YamlNode<'_> {
YamlNode(&self.0)
}
}
pub fn parse(source: &str) -> Result<YamlDocument, YamlError> {
deser_yaml::from_str::<Value>(source)
.map(YamlDocument)
.map_err(|error| YamlError(error.to_string()))
}
pub fn parse_documents(source: &str) -> Result<Vec<YamlDocument>, YamlError> {
let mut deserializer = deser_yaml::Deserializer::from_str(source);
deserializer
.iter::<Value>()
.map(|document| {
document
.map(YamlDocument)
.map_err(|error| YamlError(error.to_string()))
})
.collect()
}
#[derive(Debug, Clone, Copy)]
pub struct YamlNode<'a>(&'a Value);
impl<'a> YamlNode<'a> {
#[must_use]
pub fn is_null(self) -> bool {
self.0.is_null()
}
#[must_use]
pub fn as_bool(self) -> Option<bool> {
self.0.as_bool()
}
#[must_use]
pub fn as_str(self) -> Option<&'a str> {
self.0.as_str()
}
#[must_use]
pub fn scalar_text(self) -> Option<&'a str> {
match self.0.kind() {
Kind::Implicit(implicit) if !self.0.is_null() => Some(implicit.text().as_str()),
_ => self.as_str(),
}
}
#[must_use]
pub fn as_mapping(self) -> Option<YamlMapping<'a>> {
self.0.as_map().map(YamlMapping)
}
#[must_use]
pub fn as_sequence(self) -> Option<impl Iterator<Item = YamlNode<'a>>> {
self.0.as_seq().map(|seq| seq.iter().map(YamlNode))
}
#[must_use]
pub fn get(self, key: &str) -> Option<YamlNode<'a>> {
self.as_mapping().and_then(|mapping| mapping.get(key))
}
#[must_use]
pub fn to_yaml_string(self) -> String {
deser_yaml::to_string(self.0).unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy)]
pub struct YamlMapping<'a>(&'a Map);
impl<'a> YamlMapping<'a> {
#[must_use]
pub fn len(self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn get(self, key: &str) -> Option<YamlNode<'a>> {
self.0.get(key).map(YamlNode)
}
#[must_use]
pub fn contains_key(self, key: &str) -> bool {
self.0.contains_key(key)
}
pub fn iter(self) -> impl Iterator<Item = (YamlNode<'a>, YamlNode<'a>)> {
self.0
.iter()
.map(|(key, value)| (YamlNode(key), YamlNode(value)))
}
pub fn keys(self) -> impl Iterator<Item = YamlNode<'a>> {
self.0.keys().map(YamlNode)
}
pub fn str_keys(self) -> impl Iterator<Item = &'a str> {
self.0.keys().filter_map(|key| key.as_str())
}
pub fn values(self) -> impl Iterator<Item = YamlNode<'a>> {
self.0.values().map(YamlNode)
}
}
#[cfg(test)]
mod tests {
use super::{parse, parse_documents};
#[test]
fn reads_nested_mappings_and_sequences() {
let document = parse("catalog:\n react: ^18.2.0\nplugins:\n - a\n - b\n").unwrap();
let root = document.root();
let react = root.get("catalog").and_then(|catalog| catalog.get("react"));
assert_eq!(react.and_then(|node| node.as_str()), Some("^18.2.0"));
let plugins: Vec<_> = root
.get("plugins")
.and_then(|node| node.as_sequence())
.unwrap()
.filter_map(|node| node.as_str())
.collect();
assert_eq!(plugins, ["a", "b"]);
}
#[test]
fn keeps_the_source_text_of_plain_scalars() {
let document = parse("a: 1.10\nb: 0755\nc: true\nd: '1.10'\ne: ~\n").unwrap();
let root = document.root();
let text = |key: &str| root.get(key).and_then(|node| node.scalar_text());
assert_eq!(text("a"), Some("1.10"));
assert_eq!(text("b"), Some("0755"));
assert_eq!(text("c"), Some("true"));
assert_eq!(text("d"), Some("1.10"));
assert_eq!(text("e"), None);
assert!(root.get("e").unwrap().is_null());
assert_eq!(root.get("a").and_then(|node| node.as_str()), None);
}
#[test]
fn keeps_mapping_order_and_skips_non_string_keys() {
let document = parse("b: 1\n1: x\na: 2\n").unwrap();
let mapping = document.root().as_mapping().unwrap();
assert_eq!(mapping.len(), 3);
assert_eq!(mapping.str_keys().collect::<Vec<_>>(), ["b", "a"]);
}
#[test]
fn rejects_invalid_yaml_and_duplicate_keys() {
assert!(parse("a: [1, 2\n").is_err());
assert!(parse("a:\n b: 1\n c: 2\n").is_err());
assert!(parse("a: 1\na: 2\n").is_err());
}
#[test]
fn empty_input_is_one_null_document_or_no_documents() {
assert!(parse("").unwrap().root().is_null());
assert!(parse("# only a comment\n").unwrap().root().is_null());
assert!(parse_documents("").unwrap().is_empty());
}
#[test]
fn reads_every_document_of_a_stream() {
let documents = parse_documents("---\na: 1\n---\nb: 2\n").unwrap();
assert_eq!(documents.len(), 2);
assert!(documents[1].root().get("b").is_some());
assert!(parse("---\na: 1\n---\nb: 2\n").is_err());
}
#[test]
fn writes_collections_as_yaml() {
let document = parse("a:\n - x\n - y\n").unwrap();
let text = document.root().get("a").unwrap().to_yaml_string();
assert!(text.contains("- x"), "{text}");
}
#[test]
fn deep_nesting_parses_writes_and_drops_without_recursion() {
const DEPTH: usize = 1_000_000;
let source = format!("a: {}{}\n", "[".repeat(DEPTH), "]".repeat(DEPTH));
let document = parse(&source).unwrap();
let text = document.root().get("a").unwrap().to_yaml_string();
assert!(text.starts_with('['), "{}", &text[..text.len().min(20)]);
}
}