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" "bg.glow" "bg.rim"
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", Some("bg.glow")),
131 Some(&PolicyVerb::Allow)
132 );
133 assert_eq!(
134 policy.verb_for("layout.off_canvas", Some("bg.rim")),
135 Some(&PolicyVerb::Allow)
136 );
137 assert_eq!(policy.verb_for("layout.off_canvas", Some("shape.1")), None);
138 assert_eq!(policy.verb_for("font.local", None), Some(&PolicyVerb::Deny));
139 assert_eq!(
140 policy.verb_for("node.unknown_property", None),
141 Some(&PolicyVerb::Warn)
142 );
143 }
144
145 #[test]
146 fn empty_source_is_default_policy() {
147 let policy = parse_diagnostic_policy(b"").expect("empty must parse");
148 assert!(policy.entries.is_empty());
149 }
150
151 #[test]
152 fn no_diagnostics_node_is_default_policy() {
153 let src = br#"something else=1
155 other "node""#;
156 let policy = parse_diagnostic_policy(src).expect("must parse");
157 assert!(policy.entries.is_empty());
158 }
159
160 #[test]
161 fn malformed_kdl_is_error() {
162 let src = b"diagnostics {{{ not valid kdl";
163 let err = parse_diagnostic_policy(src).expect_err("must fail");
164 assert_eq!(err.code, ParseErrorCode::InvalidKdl);
165 }
166
167 #[test]
168 fn entry_missing_code_is_error() {
169 let src = br#"diagnostics {
171 deny
172 }"#;
173 let err = parse_diagnostic_policy(src).expect_err("missing code must fail");
174 assert_eq!(err.code, ParseErrorCode::InvalidPropertyValue);
175 }
176
177 #[test]
178 fn subject_argument_must_be_string() {
179 let src = br#"diagnostics {
180 allow "layout.off_canvas" 1
181 }"#;
182 let err = parse_diagnostic_policy(src).expect_err("invalid subject must fail");
183 assert_eq!(err.code, ParseErrorCode::InvalidPropertyValue);
184 }
185
186 #[test]
187 fn subject_property_is_rejected() {
188 let src = br#"diagnostics {
189 allow "layout.off_canvas" subject="bg.glow"
190 }"#;
191 let err = parse_diagnostic_policy(src).expect_err("subject property must fail");
192 assert_eq!(err.code, ParseErrorCode::InvalidPropertyValue);
193 }
194
195 #[test]
196 fn last_wins_across_entries() {
197 let src = br#"diagnostics {
198 deny "node.unknown_property"
199 warn "node.unknown_property"
200 }"#;
201 let policy = parse_diagnostic_policy(src).expect("must parse");
202 assert_eq!(
203 policy.verb_for("node.unknown_property", None),
204 Some(&PolicyVerb::Warn)
205 );
206 }
207
208 #[test]
211 fn brand_contract_parses_all_categories() {
212 let src = br##"brand {
213 colors "#0b1f33" "#ffffff"
214 fonts "Noto Sans" "Roboto"
215 weights 400 700
216 }"##;
217 let contract = parse_brand_contract(src).expect("must parse");
218 assert_eq!(
219 contract.allowed_colors,
220 Some(vec!["#0b1f33".to_owned(), "#ffffff".to_owned()])
221 );
222 assert_eq!(
223 contract.allowed_fonts,
224 Some(vec!["Noto Sans".to_owned(), "Roboto".to_owned()])
225 );
226 assert_eq!(contract.allowed_weights, Some(vec![400u32, 700u32]));
227 }
228
229 #[test]
230 fn brand_contract_absent_node_is_default() {
231 let contract = parse_brand_contract(b"").expect("empty must parse");
232 assert!(contract.is_empty(), "absent brand node must yield default");
233 }
234
235 #[test]
236 fn brand_contract_no_brand_node_is_default() {
237 let src = br#"diagnostics {
238 allow "token.unused"
239 }"#;
240 let contract = parse_brand_contract(src).expect("must parse");
241 assert!(
242 contract.is_empty(),
243 "source with only diagnostics node must yield default brand contract"
244 );
245 }
246
247 #[test]
248 fn brand_contract_malformed_kdl_is_error() {
249 let src = b"brand {{{ not valid kdl";
250 let err = parse_brand_contract(src).expect_err("must fail");
251 assert_eq!(err.code, ParseErrorCode::InvalidKdl);
252 }
253
254 #[test]
255 fn brand_contract_partial_categories_only_colors() {
256 let src = br##"brand {
257 colors "#ff0000"
258 }"##;
259 let contract = parse_brand_contract(src).expect("must parse");
260 assert_eq!(contract.allowed_colors, Some(vec!["#ff0000".to_owned()]));
261 assert!(
262 contract.allowed_fonts.is_none(),
263 "absent fonts must remain None (unconstrained)"
264 );
265 assert!(
266 contract.allowed_weights.is_none(),
267 "absent weights must remain None (unconstrained)"
268 );
269 }
270}