use noyalib::{
document::{load_all, load_all_as, load_all_with_config, try_load_all},
from_str, from_str_with_config, DuplicateKeyPolicy, ParserConfig, Spanned, Value,
};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct SpannedCfg {
#[allow(dead_code)]
name: Spanned<String>,
#[allow(dead_code)]
count: Spanned<i32>,
}
#[test]
fn spanned_struct_forces_ast_path() {
let yaml = "name: app\ncount: 3\n";
let cfg: SpannedCfg = from_str(yaml).unwrap();
assert_eq!(cfg.name.value, "app");
assert_eq!(cfg.count.value, 3);
}
#[test]
fn spanned_struct_recursion_limit_via_ast() {
let config = ParserConfig::new().max_depth(2);
let yaml = "name: a\ncount: 1\n";
let cfg: SpannedCfg = from_str_with_config(yaml, &config).unwrap();
assert_eq!(cfg.name.value, "a");
}
#[test]
fn ast_depth_limit_enforced_on_deeply_nested_value() {
#[derive(Debug, Deserialize)]
struct Nested {
#[allow(dead_code)]
level: Spanned<i32>,
}
#[derive(Debug, Deserialize)]
struct Outer {
#[allow(dead_code)]
inner: Nested,
}
let config = ParserConfig::new().max_depth(2);
let yaml = "inner:\n level: 1\n";
let _: Outer = from_str_with_config(yaml, &config).unwrap();
}
#[test]
fn ast_duplicate_policy_first_keeps_first() {
#[derive(Debug, Deserialize)]
struct Doc {
#[allow(dead_code)]
a: Spanned<String>,
}
let config = ParserConfig::new().duplicate_key_policy(DuplicateKeyPolicy::First);
let yaml = "a: first\na: second\n";
let doc: Doc = from_str_with_config(yaml, &config).unwrap();
assert_eq!(doc.a.value, "first");
}
#[test]
fn ast_duplicate_policy_last_keeps_last() {
#[derive(Debug, Deserialize)]
struct Doc {
#[allow(dead_code)]
a: Spanned<String>,
}
let config = ParserConfig::new().duplicate_key_policy(DuplicateKeyPolicy::Last);
let yaml = "a: first\na: second\n";
let doc: Doc = from_str_with_config(yaml, &config).unwrap();
assert_eq!(doc.a.value, "second");
}
#[test]
fn ast_duplicate_policy_error_rejects() {
#[derive(Debug, Deserialize)]
struct Doc {
#[allow(dead_code)]
a: Spanned<String>,
}
let config = ParserConfig::new().duplicate_key_policy(DuplicateKeyPolicy::Error);
let yaml = "a: first\na: second\n";
let err = from_str_with_config::<Doc>(yaml, &config).unwrap_err();
assert!(err.to_string().contains("duplicate") || err.to_string().contains("a"));
}
#[test]
fn alias_outside_document_via_ast() {
let yaml = "a: &x 1\nb: *x\n";
let result = load_all(yaml).unwrap();
let docs: Vec<_> = result.filter_map(Result::ok).collect();
assert!(!docs.is_empty());
}
#[test]
fn ast_unknown_anchor_error() {
#[derive(Debug, Deserialize)]
struct Doc {
#[allow(dead_code)]
v: Spanned<String>,
}
let yaml = "v: *missing\n";
let err = from_str::<Doc>(yaml).unwrap_err();
assert!(err.to_string().contains("missing") || err.to_string().contains("unknown"));
}
#[test]
fn ast_alias_expansion_count_limit() {
let config = ParserConfig::new().max_alias_expansions(2);
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Doc {
anchor: Spanned<String>,
ref1: Spanned<String>,
ref2: Spanned<String>,
ref3: Spanned<String>,
}
let yaml = "anchor: &a hello\nref1: *a\nref2: *a\nref3: *a\n";
let err = from_str_with_config::<Doc>(yaml, &config).unwrap_err();
assert!(err.to_string().contains("limit") || err.to_string().contains("alias"));
}
#[test]
fn ast_mapping_key_limit_enforced() {
let config = ParserConfig::new().max_mapping_keys(3);
let mut yaml = String::new();
for i in 0..10 {
yaml.push_str(&format!("key{i}: val{i}\n"));
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Doc {
key0: Spanned<String>,
}
let err = from_str_with_config::<Doc>(&yaml, &config).unwrap_err();
assert!(err.to_string().contains("limit") || err.to_string().contains("key"));
}
#[test]
fn ast_sequence_length_limit_enforced() {
let config = ParserConfig::new().max_sequence_length(3);
let mut yaml = String::from("items:\n");
for i in 0..10 {
yaml.push_str(&format!(" - {i}\n"));
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Doc {
items: Vec<Spanned<i32>>,
}
let err = from_str_with_config::<Doc>(&yaml, &config).unwrap_err();
assert!(err.to_string().contains("limit") || err.to_string().contains("sequence"));
}
#[test]
fn load_all_multi_document() {
let yaml = "---\nfirst: 1\n---\nsecond: 2\n---\nthird: 3\n";
let iter = load_all(yaml).unwrap();
let docs: Vec<_> = iter.filter_map(Result::ok).collect();
assert_eq!(docs.len(), 3);
}
#[test]
fn load_all_with_config_applies_limits() {
let config = ParserConfig::new().max_document_length(10);
let yaml = "this is way longer than ten bytes and should fail";
assert!(load_all_with_config(yaml, &config).is_err());
}
#[test]
fn try_load_all_is_alias_for_load_all() {
let yaml = "---\na: 1\n---\nb: 2\n";
let iter = try_load_all(yaml).unwrap();
let docs: Vec<_> = iter.filter_map(Result::ok).collect();
assert_eq!(docs.len(), 2);
}
#[test]
fn load_all_as_typed_struct() {
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct Doc {
name: String,
}
let yaml = "---\nname: a\n---\nname: b\n";
let docs: Vec<Doc> = load_all_as(yaml).unwrap();
assert_eq!(docs.len(), 2);
assert_eq!(docs[0].name, "a");
assert_eq!(docs[1].name, "b");
}
#[test]
fn load_all_single_document() {
let yaml = "a: 1\n";
let iter = load_all(yaml).unwrap();
let docs: Vec<_> = iter.filter_map(Result::ok).collect();
assert_eq!(docs.len(), 1);
}
#[test]
fn load_all_empty_stream_returns_empty() {
let yaml = "";
let iter = load_all(yaml).unwrap();
let docs: Vec<_> = iter.filter_map(Result::ok).collect();
assert!(docs.len() <= 1);
}
#[test]
fn ast_merge_key_single_anchor() {
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Target {
a: Spanned<i32>,
b: Spanned<i32>,
c: Spanned<i32>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Doc {
target: Target,
}
let yaml = "base: &b\n a: 1\n b: 2\ntarget:\n <<: *b\n c: 3\n";
let d: Doc = from_str(yaml).unwrap();
assert_eq!(d.target.a.value, 1);
assert_eq!(d.target.b.value, 2);
assert_eq!(d.target.c.value, 3);
}
#[test]
fn ast_merge_key_sequence_of_anchors() {
let yaml = "first: &f\n a: 1\n b: 2\nsecond: &s\n b: 20\n c: 30\ntarget:\n <<: [*f, *s]\n";
let v: Value = from_str(yaml).unwrap();
let target = v.get("target").unwrap();
assert_eq!(target.get("a").and_then(|v| v.as_i64()), Some(1));
assert_eq!(target.get("b").and_then(|v| v.as_i64()), Some(2));
assert_eq!(target.get("c").and_then(|v| v.as_i64()), Some(30));
}
#[test]
fn ast_tagged_int() {
let v: Value = from_str("!!int 42\n").unwrap();
assert_eq!(v.as_i64(), Some(42));
}
#[test]
fn ast_tagged_str_with_numeric_content() {
let v: Value = from_str("!!str 42\n").unwrap();
assert_eq!(v.as_str(), Some("42"));
}
#[test]
fn ast_tagged_float_infinity() {
let v: Value = from_str("!!float .inf\n").unwrap();
assert!(v.as_f64().unwrap().is_infinite());
}
#[test]
fn ast_tagged_bool_invalid_errors() {
assert!(from_str::<Value>("!!bool notabool\n").is_err());
}
#[test]
fn ast_integer_key_coerced_to_string() {
let yaml = "1: one\n2: two\n";
let v: Value = from_str(yaml).unwrap();
assert_eq!(v.get("1").and_then(|v| v.as_str()), Some("one"));
assert_eq!(v.get("2").and_then(|v| v.as_str()), Some("two"));
}
#[test]
fn ast_bool_key_coerced_to_string() {
let yaml = "true: t\nfalse: f\n";
let v: Value = from_str(yaml).unwrap();
assert_eq!(v.get("true").and_then(|v| v.as_str()), Some("t"));
assert_eq!(v.get("false").and_then(|v| v.as_str()), Some("f"));
}
#[test]
fn ast_float_key_coerced_to_string() {
let yaml = "1.5: half\n";
let v: Value = from_str(yaml).unwrap();
assert!(v.get("1.5").is_some());
}
#[test]
fn ast_anchor_on_sequence() {
let yaml = "seq: &s\n - 1\n - 2\ncopy: *s\n";
let v: Value = from_str(yaml).unwrap();
let seq = v.get("seq").and_then(|v| v.as_sequence()).unwrap();
let copy = v.get("copy").and_then(|v| v.as_sequence()).unwrap();
assert_eq!(seq.len(), 2);
assert_eq!(copy.len(), 2);
}
#[test]
fn ast_anchor_on_mapping() {
let yaml = "base: &b\n host: localhost\n port: 80\nref: *b\n";
let v: Value = from_str(yaml).unwrap();
assert_eq!(
v.get("ref")
.and_then(|v| v.get("host"))
.and_then(|v| v.as_str()),
Some("localhost")
);
}
#[test]
fn ast_spanned_in_multi_document() {
let yaml = "---\nname: first\n---\nname: second\n";
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Doc {
name: Spanned<String>,
}
let docs: Vec<Doc> = load_all_as(yaml).unwrap();
assert_eq!(docs.len(), 2);
assert_eq!(docs[0].name.value, "first");
assert_eq!(docs[1].name.value, "second");
}
#[test]
fn ast_merge_empty_anchor() {
let yaml = "base: &e {}\ntarget:\n <<: *e\n only: here\n";
let v: Value = from_str(yaml).unwrap();
let target = v.get("target").unwrap();
assert_eq!(target.get("only").and_then(|v| v.as_str()), Some("here"));
}
#[test]
fn ast_scalar_anchor_alias() {
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Doc {
a: Spanned<String>,
b: Spanned<String>,
}
let yaml = "a: &x hello\nb: *x\n";
let d: Doc = from_str(yaml).unwrap();
assert_eq!(d.a.value, "hello");
assert_eq!(d.b.value, "hello");
}
#[test]
fn ast_deep_sequence_hits_recursion_limit() {
let config = ParserConfig::new().max_depth(3);
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Doc {
deep: Spanned<Vec<Vec<Vec<Vec<i32>>>>>,
}
let yaml = "deep: [[[[1]]]]\n";
let err = from_str_with_config::<Doc>(yaml, &config).unwrap_err();
assert!(err.to_string().contains("depth") || err.to_string().contains("recursion"));
}
#[test]
fn ast_alias_bytes_limit_hits_repetition() {
let config = ParserConfig::new().max_document_length(500);
let long = "x".repeat(100);
let mut yaml = format!("anchor: &a {long}\n");
for i in 0..20 {
yaml.push_str(&format!("ref{i}: *a\n"));
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Doc {
anchor: Spanned<String>,
ref0: Spanned<String>,
}
assert!(from_str_with_config::<Doc>(&yaml, &config).is_err());
}
#[test]
fn bool_key_true_false_coerced() {
let v: Value = from_str("true: t\nfalse: f\n").unwrap();
assert!(v.get("true").is_some());
assert!(v.get("false").is_some());
}
#[test]
fn float_key_coerced() {
let v: Value = from_str("3.14: pi\n").unwrap();
assert!(v.get("3.14").is_some());
}
#[test]
fn null_key_coerced() {
let v: Value = from_str("null: nothing\n").unwrap();
assert!(v.get("null").is_some());
}
#[test]
fn alias_to_null_anchor_size_estimated() {
let yaml = "a: &n null\nb: *n\nc: *n\n";
let v: Value = from_str(yaml).unwrap();
assert!(v.get("b").map(|v| v.is_null()).unwrap_or(false));
}