#![allow(missing_docs)]
use noyalib::cst::{
CommentPosition, Document, RepairScope, parse_document, parse_document_with_config,
parse_stream, parse_stream_with_config,
};
use noyalib::{BudgetBreach, DuplicateKeyPolicy, Error, ParserConfig};
fn merge_heavy(anchors: usize, aliases: usize) -> String {
let mut src = String::from("defaults:\n");
for a in 0..anchors {
src.push_str(&format!(" d{a}: &a{a}\n k: v{a}\n"));
}
src.push_str("tenants:\n");
for t in 0..aliases {
src.push_str(&format!(" t{t}:\n <<: *a{}\n", t % anchors));
}
src
}
fn relaxed() -> ParserConfig {
ParserConfig::new().alias_anchor_ratio(None)
}
fn is_ratio_breach(err: &Error) -> bool {
matches!(err, Error::Budget(BudgetBreach::AliasAnchorRatio { .. }))
}
#[test]
fn default_entry_points_keep_the_ratio_heuristic() {
let src = merge_heavy(22, 221);
assert!(is_ratio_breach(&parse_document(&src).unwrap_err()));
assert!(is_ratio_breach(&parse_stream(&src).unwrap_err()));
assert!(parse_document(&merge_heavy(22, 220)).is_ok());
}
#[test]
fn parse_document_with_config_disables_the_ratio_heuristic() {
let src = merge_heavy(22, 221);
let doc = parse_document_with_config(&src, &relaxed()).unwrap();
assert_eq!(doc.to_string(), src);
let v = doc.as_value();
assert_eq!(v["tenants"]["t0"]["k"].as_str(), Some("v0"));
assert_eq!(v["tenants"]["t21"]["k"].as_str(), Some("v21"));
assert_eq!(v["tenants"]["t220"]["k"].as_str(), Some("v0"));
}
#[test]
fn parse_document_with_config_keeps_the_absolute_alias_budget() {
let err = parse_document_with_config(&merge_heavy(22, 1025), &relaxed()).unwrap_err();
assert!(
matches!(err, Error::RepetitionLimitExceeded),
"expected the alias-expansion cap, got {err:?}"
);
}
#[test]
fn parse_document_with_config_honours_every_knob_not_just_the_ratio() {
let src = "a: 1\na: 2\n";
assert_eq!(
parse_document(src).unwrap().as_value()["a"].as_i64(),
Some(2)
);
let cfg = ParserConfig::new().duplicate_key_policy(DuplicateKeyPolicy::Error);
assert!(matches!(
parse_document_with_config(src, &cfg),
Err(Error::DuplicateKeyAt { .. })
));
}
#[test]
fn a_default_config_behaves_like_the_bare_entry_point() {
let src = merge_heavy(22, 221);
assert!(is_ratio_breach(
&parse_document_with_config(&src, &ParserConfig::default()).unwrap_err()
));
let ok = merge_heavy(22, 220);
let bare = parse_document(&ok).unwrap();
let configured = parse_document_with_config(&ok, &ParserConfig::default()).unwrap();
assert_eq!(bare.to_string(), configured.to_string());
assert_eq!(*bare.as_value(), *configured.as_value());
}
#[test]
fn a_document_keeps_its_configuration_across_local_repair_edits() {
let src = merge_heavy(22, 221);
let mut doc = parse_document_with_config(&src, &relaxed()).unwrap();
doc.set("defaults.d0.k", "changed").unwrap();
assert_eq!(
doc.as_value()["tenants"]["t0"]["k"].as_str(),
Some("changed")
);
assert!(doc.validate().is_ok());
assert_eq!(doc.to_string(), src.replacen("k: v0\n", "k: changed\n", 1));
}
#[test]
fn a_document_keeps_its_configuration_across_the_full_reparse_safety_net() {
let src = merge_heavy(22, 221);
let next = merge_heavy(22, 230);
let mut doc = parse_document_with_config(&src, &relaxed()).unwrap();
doc.replace_span(0, src.len(), &next).unwrap();
assert_eq!(doc.last_repair_scope(), Some(RepairScope::Document));
assert_eq!(doc.to_string(), next);
assert_eq!(doc.as_value()["tenants"]["t229"]["k"].as_str(), Some("v9"));
}
#[test]
fn a_clone_keeps_the_configuration() {
let src = merge_heavy(22, 221);
let doc = parse_document_with_config(&src, &relaxed()).unwrap();
let mut copy = doc.clone();
copy.set("defaults.d1.k", "x").unwrap();
assert_eq!(copy.as_value()["tenants"]["t1"]["k"].as_str(), Some("x"));
assert_eq!(doc.as_value()["tenants"]["t1"]["k"].as_str(), Some("v1"));
}
#[test]
fn comment_edits_stay_guarded_under_the_configuration() {
let mut src = merge_heavy(22, 221);
src.push_str("note: |\n text\n");
let mut doc = parse_document_with_config(&src, &relaxed()).unwrap();
doc.set_inline_comment("defaults.d0.k", "the default")
.unwrap();
assert_eq!(
doc.to_string(),
src.replacen("k: v0\n", "k: v0 # the default\n", 1)
);
assert_eq!(doc.as_value()["tenants"]["t0"]["k"].as_str(), Some("v0"));
let before = doc.to_string();
let err = doc
.set_comment("note", CommentPosition::Inline, "x")
.unwrap_err();
assert!(
err.to_string()
.contains("would change the document's value"),
"unexpected error: {err}"
);
assert_eq!(doc.to_string(), before);
assert!(
parse_document("note: |\n text\n")
.unwrap()
.set_comment("note", CommentPosition::Inline, "x")
.is_err()
);
}
#[test]
fn parse_stream_with_config_applies_to_every_document() {
let first = merge_heavy(22, 221);
let second = merge_heavy(3, 40);
let src = format!("---\n{first}---\n{second}");
assert!(is_ratio_breach(&parse_stream(&src).unwrap_err()));
let docs = parse_stream_with_config(&src, &relaxed()).unwrap();
assert_eq!(docs.len(), 2);
let joined: String = docs.iter().map(Document::source).collect();
assert_eq!(joined, src);
assert_eq!(
docs[0].as_value()["tenants"]["t220"]["k"].as_str(),
Some("v0")
);
assert_eq!(
docs[1].as_value()["tenants"]["t39"]["k"].as_str(),
Some("v0")
);
let mut second_doc = docs.into_iter().nth(1).unwrap();
second_doc.set("defaults.d0.k", "z").unwrap();
assert_eq!(
second_doc.as_value()["tenants"]["t0"]["k"].as_str(),
Some("z")
);
}
#[test]
fn parse_stream_with_config_single_document_matches_parse_document() {
let src = merge_heavy(22, 221);
let docs = parse_stream_with_config(&src, &relaxed()).unwrap();
assert_eq!(docs.len(), 1);
assert_eq!(docs[0].source(), src);
let single = parse_document_with_config(&src, &relaxed()).unwrap();
assert_eq!(*docs[0].as_value(), *single.as_value());
}