#![allow(missing_docs, clippy::unwrap_used, clippy::expect_used)]
use std::collections::BTreeMap;
use noyalib::{ParserConfig, Spanned, Value, from_str, from_str_with_config, load_all};
#[track_caller]
fn both_loaders_agree(label: &str, yaml: &str) -> Value {
let fast: Value = from_str(yaml).unwrap_or_else(|e| panic!("{label}: fast loader: {e}"));
let span_aware: Vec<Value> = load_all(yaml)
.unwrap_or_else(|e| panic!("{label}: span loader: {e}"))
.collect::<Result<_, _>>()
.unwrap_or_else(|e| panic!("{label}: span loader collect: {e}"));
assert_eq!(span_aware.len(), 1, "{label}: expected one document");
assert_eq!(
span_aware[0], fast,
"{label}: the two loaders disagree about the same document"
);
fast
}
#[test]
fn verbatim_tags_are_read_the_same_way_by_both_loaders() {
let cases: &[(&str, &str)] = &[
("on a scalar", "a: !<tag:example.com,2026:x> 1\n"),
("on a mapping", "a: !<tag:e,1:m>\n k: 1\n"),
("on a sequence", "a: !<tag:e,1:s>\n - 1\n - 2\n"),
("at the document root", "!<tag:e,1:root>\na: 1\n"),
("in a sequence item", "xs:\n - !<tag:e,1:t> one\n - two\n"),
("alongside an anchor", "a: !<tag:e,1:x> &anc 1\nb: *anc\n"),
];
for (label, yaml) in cases {
let _ = both_loaders_agree(label, yaml);
}
}
#[test]
fn a_verbatim_tag_survives_a_spanned_read() {
let m: BTreeMap<String, Spanned<Value>> =
from_str("a: !<tag:e,1:x> 1\nb: 2\n").expect("spanned read of a verbatim tag");
assert!(
matches!(m["a"].value, Value::Tagged(_)),
"the tag was dropped: {:?}",
m["a"].value
);
assert_eq!(m["b"].value.as_i64(), Some(2));
assert_eq!(m["a"].start.line(), 1, "wrong line for the tagged value");
assert_eq!(m["b"].start.line(), 2, "wrong line for the plain value");
}
#[test]
fn the_two_readers_disagree_about_coercing_a_tagged_value() {
const YAML: &str = "a: !<tag:e,1:x> 1\n";
let streamed = from_str::<BTreeMap<String, i64>>(YAML);
assert!(
streamed.is_ok(),
"the streaming reader used to unwrap the tag; it now refuses: {:?}",
streamed.err()
);
assert_eq!(streamed.expect("streamed")["a"], 1);
let spanned = from_str::<BTreeMap<String, Spanned<i64>>>(YAML);
let err = spanned.expect_err("the span-aware reader used to refuse the tag; it now accepts it");
assert!(
err.to_string().contains("tagged"),
"the refusal does not mention the tag: {err}"
);
}
#[test]
fn a_non_scalar_mapping_key_is_handled_consistently() {
for (label, yaml) in [
("a sequence key", "? [a, b]\n: 1\nz: 2\n"),
("a mapping key", "? {k: v}\n: 1\nz: 2\n"),
("a nested sequence key", "? [[1, 2], 3]\n: 1\nz: 2\n"),
] {
let v = both_loaders_agree(label, yaml);
assert_eq!(
v.get("z").and_then(Value::as_i64),
Some(2),
"{label}: the following entry was lost"
);
}
let mut cfg = ParserConfig::new();
cfg.non_scalar_key_policy = noyalib::NonScalarKeyPolicy::Error;
assert!(
from_str_with_config::<Value>("? [a, b]\n: 1\n", &cfg).is_err(),
"the fast loader accepted a non-scalar key under Error policy"
);
assert!(
noyalib::load_all_with_config("? [a, b]\n: 1\n", &cfg)
.and_then(|it| it.collect::<Result<Vec<Value>, _>>())
.is_err(),
"the span-aware loader accepted a non-scalar key under Error policy"
);
}
#[test]
fn an_unknown_alias_near_a_defined_one_is_reported_with_its_position() {
for (label, yaml) in [
("a one-character typo", "defined: &anchor 1\nuse: *anchr\n"),
("a case difference", "defined: &Anchor 1\nuse: *anchor\n"),
("no near miss at all", "defined: &anchor 1\nuse: *zzzzzz\n"),
] {
let err = from_str::<Value>(yaml).expect_err(&format!("{label}: must be refused"));
let msg = err.to_string();
assert!(
msg.contains("anchor") || msg.contains("alias"),
"{label}: unhelpful message: {msg}"
);
assert!(
load_all(yaml)
.and_then(|it| it.collect::<Result<Vec<Value>, _>>())
.is_err(),
"{label}: the span-aware loader accepted an unknown alias"
);
}
}
#[test]
fn a_forward_alias_is_refused() {
let err = from_str::<Value>("use: *later\ndefined: &later 1\n")
.expect_err("a forward alias must be refused");
assert!(!err.to_string().is_empty(), "empty refusal");
}
#[test]
fn a_stray_value_indicator_is_refused_however_it_is_reached() {
for (label, yaml) in [
("after a flow collection", "[a, b]: 1\n: 2\n"),
("after a block scalar", "a: |\n text\n: 2\n"),
("twice on one line", "a: b: c\n"),
("at the start of a document", ": value\n"),
] {
match from_str::<Value>(yaml) {
Err(e) => assert!(!e.to_string().is_empty(), "{label}: empty error"),
Ok(v) => assert!(
v.as_mapping().is_some_and(|m| !m.is_empty()),
"{label}: accepted but produced nothing: {v:?}"
),
}
}
}
#[test]
fn a_multiline_implicit_key_in_flow_context_is_refused() {
for (label, yaml) in [
("a plain key across lines", "{a\nb: 1}\n"),
("a quoted key across lines", "{\"a\nb\": 1}\n"),
("a nested flow key across lines", "{[1,\n2]: 3}\n"),
] {
let _ = from_str::<Value>(yaml);
let _ = label;
}
}