zenith_core/parse/
policy.rs1use crate::ast::brand::BrandContract;
31use crate::ast::policy::DiagnosticPolicy;
32use crate::error::{ParseError, ParseErrorCode};
33use crate::parse::transform::{transform_brand_contract, transform_diagnostic_policy};
34
35pub fn parse_diagnostic_policy(source: &[u8]) -> Result<DiagnosticPolicy, ParseError> {
49 let text = std::str::from_utf8(source).map_err(|e| {
51 ParseError::spanless(
52 ParseErrorCode::NotUtf8,
53 format!("config source is not valid UTF-8: {e}"),
54 )
55 })?;
56
57 let kdl_doc: kdl::KdlDocument = text.parse().map_err(|e: kdl::KdlError| {
59 ParseError::spanless(
60 ParseErrorCode::InvalidKdl,
61 format!("config KDL parse error: {e}"),
62 )
63 })?;
64
65 match kdl_doc
68 .nodes()
69 .iter()
70 .find(|n| n.name().value() == "diagnostics")
71 {
72 Some(node) => transform_diagnostic_policy(node),
73 None => Ok(DiagnosticPolicy::default()),
74 }
75}
76
77pub fn parse_brand_contract(source: &[u8]) -> Result<BrandContract, ParseError> {
91 let text = std::str::from_utf8(source).map_err(|e| {
93 ParseError::spanless(
94 ParseErrorCode::NotUtf8,
95 format!("config source is not valid UTF-8: {e}"),
96 )
97 })?;
98
99 let kdl_doc: kdl::KdlDocument = text.parse().map_err(|e: kdl::KdlError| {
101 ParseError::spanless(
102 ParseErrorCode::InvalidKdl,
103 format!("config KDL parse error: {e}"),
104 )
105 })?;
106
107 match kdl_doc.nodes().iter().find(|n| n.name().value() == "brand") {
110 Some(node) => transform_brand_contract(node),
111 None => Ok(BrandContract::default()),
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::ast::policy::PolicyVerb;
119
120 #[test]
121 fn parses_allow_deny_warn_block() {
122 let src = br#"diagnostics {
123 allow "layout.off_canvas"
124 deny "font.local"
125 warn "node.unknown_property"
126 }"#;
127 let policy = parse_diagnostic_policy(src).expect("must parse");
128 assert_eq!(policy.entries.len(), 3);
129 assert_eq!(
130 policy.verb_for("layout.off_canvas"),
131 Some(&PolicyVerb::Allow)
132 );
133 assert_eq!(policy.verb_for("font.local"), Some(&PolicyVerb::Deny));
134 assert_eq!(
135 policy.verb_for("node.unknown_property"),
136 Some(&PolicyVerb::Warn)
137 );
138 }
139
140 #[test]
141 fn empty_source_is_default_policy() {
142 let policy = parse_diagnostic_policy(b"").expect("empty must parse");
143 assert!(policy.entries.is_empty());
144 }
145
146 #[test]
147 fn no_diagnostics_node_is_default_policy() {
148 let src = br#"something else=1
150 other "node""#;
151 let policy = parse_diagnostic_policy(src).expect("must parse");
152 assert!(policy.entries.is_empty());
153 }
154
155 #[test]
156 fn malformed_kdl_is_error() {
157 let src = b"diagnostics {{{ not valid kdl";
158 let err = parse_diagnostic_policy(src).expect_err("must fail");
159 assert_eq!(err.code, ParseErrorCode::InvalidKdl);
160 }
161
162 #[test]
163 fn entry_missing_code_is_error() {
164 let src = br#"diagnostics {
166 deny
167 }"#;
168 let err = parse_diagnostic_policy(src).expect_err("missing code must fail");
169 assert_eq!(err.code, ParseErrorCode::InvalidPropertyValue);
170 }
171
172 #[test]
173 fn last_wins_across_entries() {
174 let src = br#"diagnostics {
175 deny "node.unknown_property"
176 warn "node.unknown_property"
177 }"#;
178 let policy = parse_diagnostic_policy(src).expect("must parse");
179 assert_eq!(
180 policy.verb_for("node.unknown_property"),
181 Some(&PolicyVerb::Warn)
182 );
183 }
184
185 #[test]
188 fn brand_contract_parses_all_categories() {
189 let src = br##"brand {
190 colors "#0b1f33" "#ffffff"
191 fonts "Noto Sans" "Roboto"
192 weights 400 700
193 }"##;
194 let contract = parse_brand_contract(src).expect("must parse");
195 assert_eq!(
196 contract.allowed_colors,
197 Some(vec!["#0b1f33".to_owned(), "#ffffff".to_owned()])
198 );
199 assert_eq!(
200 contract.allowed_fonts,
201 Some(vec!["Noto Sans".to_owned(), "Roboto".to_owned()])
202 );
203 assert_eq!(contract.allowed_weights, Some(vec![400u32, 700u32]));
204 }
205
206 #[test]
207 fn brand_contract_absent_node_is_default() {
208 let contract = parse_brand_contract(b"").expect("empty must parse");
209 assert!(contract.is_empty(), "absent brand node must yield default");
210 }
211
212 #[test]
213 fn brand_contract_no_brand_node_is_default() {
214 let src = br#"diagnostics {
215 allow "token.unused"
216 }"#;
217 let contract = parse_brand_contract(src).expect("must parse");
218 assert!(
219 contract.is_empty(),
220 "source with only diagnostics node must yield default brand contract"
221 );
222 }
223
224 #[test]
225 fn brand_contract_malformed_kdl_is_error() {
226 let src = b"brand {{{ not valid kdl";
227 let err = parse_brand_contract(src).expect_err("must fail");
228 assert_eq!(err.code, ParseErrorCode::InvalidKdl);
229 }
230
231 #[test]
232 fn brand_contract_partial_categories_only_colors() {
233 let src = br##"brand {
234 colors "#ff0000"
235 }"##;
236 let contract = parse_brand_contract(src).expect("must parse");
237 assert_eq!(contract.allowed_colors, Some(vec!["#ff0000".to_owned()]));
238 assert!(
239 contract.allowed_fonts.is_none(),
240 "absent fonts must remain None (unconstrained)"
241 );
242 assert!(
243 contract.allowed_weights.is_none(),
244 "absent weights must remain None (unconstrained)"
245 );
246 }
247}