use crate::ast::brand::BrandContract;
use crate::ast::policy::DiagnosticPolicy;
use crate::error::{ParseError, ParseErrorCode};
use crate::parse::transform::{transform_brand_contract, transform_diagnostic_policy};
pub fn parse_diagnostic_policy(source: &[u8]) -> Result<DiagnosticPolicy, ParseError> {
let text = std::str::from_utf8(source).map_err(|e| {
ParseError::spanless(
ParseErrorCode::NotUtf8,
format!("config source is not valid UTF-8: {e}"),
)
})?;
let kdl_doc: kdl::KdlDocument = text.parse().map_err(|e: kdl::KdlError| {
ParseError::spanless(
ParseErrorCode::InvalidKdl,
format!("config KDL parse error: {e}"),
)
})?;
match kdl_doc
.nodes()
.iter()
.find(|n| n.name().value() == "diagnostics")
{
Some(node) => transform_diagnostic_policy(node),
None => Ok(DiagnosticPolicy::default()),
}
}
pub fn parse_brand_contract(source: &[u8]) -> Result<BrandContract, ParseError> {
let text = std::str::from_utf8(source).map_err(|e| {
ParseError::spanless(
ParseErrorCode::NotUtf8,
format!("config source is not valid UTF-8: {e}"),
)
})?;
let kdl_doc: kdl::KdlDocument = text.parse().map_err(|e: kdl::KdlError| {
ParseError::spanless(
ParseErrorCode::InvalidKdl,
format!("config KDL parse error: {e}"),
)
})?;
match kdl_doc.nodes().iter().find(|n| n.name().value() == "brand") {
Some(node) => transform_brand_contract(node),
None => Ok(BrandContract::default()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::policy::PolicyVerb;
#[test]
fn parses_allow_deny_warn_block() {
let src = br#"diagnostics {
allow "layout.off_canvas" "bg.glow" "bg.rim"
deny "font.local"
warn "node.unknown_property"
}"#;
let policy = parse_diagnostic_policy(src).expect("must parse");
assert_eq!(policy.entries.len(), 3);
assert_eq!(
policy.verb_for("layout.off_canvas", Some("bg.glow")),
Some(&PolicyVerb::Allow)
);
assert_eq!(
policy.verb_for("layout.off_canvas", Some("bg.rim")),
Some(&PolicyVerb::Allow)
);
assert_eq!(policy.verb_for("layout.off_canvas", Some("shape.1")), None);
assert_eq!(policy.verb_for("font.local", None), Some(&PolicyVerb::Deny));
assert_eq!(
policy.verb_for("node.unknown_property", None),
Some(&PolicyVerb::Warn)
);
}
#[test]
fn empty_source_is_default_policy() {
let policy = parse_diagnostic_policy(b"").expect("empty must parse");
assert!(policy.entries.is_empty());
}
#[test]
fn no_diagnostics_node_is_default_policy() {
let src = br#"something else=1
other "node""#;
let policy = parse_diagnostic_policy(src).expect("must parse");
assert!(policy.entries.is_empty());
}
#[test]
fn malformed_kdl_is_error() {
let src = b"diagnostics {{{ not valid kdl";
let err = parse_diagnostic_policy(src).expect_err("must fail");
assert_eq!(err.code, ParseErrorCode::InvalidKdl);
}
#[test]
fn entry_missing_code_is_error() {
let src = br#"diagnostics {
deny
}"#;
let err = parse_diagnostic_policy(src).expect_err("missing code must fail");
assert_eq!(err.code, ParseErrorCode::InvalidPropertyValue);
}
#[test]
fn subject_argument_must_be_string() {
let src = br#"diagnostics {
allow "layout.off_canvas" 1
}"#;
let err = parse_diagnostic_policy(src).expect_err("invalid subject must fail");
assert_eq!(err.code, ParseErrorCode::InvalidPropertyValue);
}
#[test]
fn subject_property_is_rejected() {
let src = br#"diagnostics {
allow "layout.off_canvas" subject="bg.glow"
}"#;
let err = parse_diagnostic_policy(src).expect_err("subject property must fail");
assert_eq!(err.code, ParseErrorCode::InvalidPropertyValue);
}
#[test]
fn last_wins_across_entries() {
let src = br#"diagnostics {
deny "node.unknown_property"
warn "node.unknown_property"
}"#;
let policy = parse_diagnostic_policy(src).expect("must parse");
assert_eq!(
policy.verb_for("node.unknown_property", None),
Some(&PolicyVerb::Warn)
);
}
#[test]
fn brand_contract_parses_all_categories() {
let src = br##"brand {
colors "#0b1f33" "#ffffff"
fonts "Noto Sans" "Roboto"
weights 400 700
}"##;
let contract = parse_brand_contract(src).expect("must parse");
assert_eq!(
contract.allowed_colors,
Some(vec!["#0b1f33".to_owned(), "#ffffff".to_owned()])
);
assert_eq!(
contract.allowed_fonts,
Some(vec!["Noto Sans".to_owned(), "Roboto".to_owned()])
);
assert_eq!(contract.allowed_weights, Some(vec![400u32, 700u32]));
}
#[test]
fn brand_contract_absent_node_is_default() {
let contract = parse_brand_contract(b"").expect("empty must parse");
assert!(contract.is_empty(), "absent brand node must yield default");
}
#[test]
fn brand_contract_no_brand_node_is_default() {
let src = br#"diagnostics {
allow "token.unused"
}"#;
let contract = parse_brand_contract(src).expect("must parse");
assert!(
contract.is_empty(),
"source with only diagnostics node must yield default brand contract"
);
}
#[test]
fn brand_contract_malformed_kdl_is_error() {
let src = b"brand {{{ not valid kdl";
let err = parse_brand_contract(src).expect_err("must fail");
assert_eq!(err.code, ParseErrorCode::InvalidKdl);
}
#[test]
fn brand_contract_partial_categories_only_colors() {
let src = br##"brand {
colors "#ff0000"
}"##;
let contract = parse_brand_contract(src).expect("must parse");
assert_eq!(contract.allowed_colors, Some(vec!["#ff0000".to_owned()]));
assert!(
contract.allowed_fonts.is_none(),
"absent fonts must remain None (unconstrained)"
);
assert!(
contract.allowed_weights.is_none(),
"absent weights must remain None (unconstrained)"
);
}
}