1use crate::{RawRdNode, RawRdValue, RdNode};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum RawNodeClassification {
14 ExpectedUserMacroDefinition,
19 Unexpected(UnexpectedRawNode),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct UnexpectedRawNode {
26 tag: Option<String>,
27 reason: RawNodeReason,
28 offending_attributes: Vec<String>,
29}
30
31impl UnexpectedRawNode {
32 pub fn tag(&self) -> Option<&str> {
33 self.tag.as_deref()
34 }
35 pub fn reason(&self) -> &RawNodeReason {
36 &self.reason
37 }
38 pub fn offending_attributes(&self) -> &[String] {
39 &self.offending_attributes
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum RawNodeReason {
47 WrongTag,
48 OptionPresent,
49 ChildrenShape,
50 PayloadPresent,
51 AttributeSet,
52 AttributeValueShape,
53}
54
55pub fn classify_raw_node(raw: &RawRdNode) -> RawNodeClassification {
58 let names: Vec<&str> = raw
59 .attributes()
60 .iter()
61 .map(|attribute| attribute.name())
62 .collect();
63 let unexpected_names: Vec<String> = names
64 .iter()
65 .filter(|name| **name != "srcref" && **name != "macro")
66 .map(|name| (*name).to_string())
67 .collect();
68
69 if raw.tag() != Some("USERMACRO") {
70 let all_names = names.iter().map(|name| (*name).to_string()).collect();
71 return unexpected(raw, RawNodeReason::WrongTag, all_names);
72 }
73 if raw.option().is_some() {
74 let mut offenders = unexpected_names;
75 offenders.push("Rd_option".to_string());
76 return unexpected(raw, RawNodeReason::OptionPresent, offenders);
77 }
78 if raw.payload().is_some() {
79 return unexpected(raw, RawNodeReason::PayloadPresent, unexpected_names);
80 }
81 if raw.children().len() != 1 || !matches!(raw.children()[0], RdNode::Text(_)) {
82 return unexpected(raw, RawNodeReason::ChildrenShape, unexpected_names);
83 }
84 if names.len() != 2
85 || names.iter().filter(|name| **name == "srcref").count() != 1
86 || names.iter().filter(|name| **name == "macro").count() != 1
87 {
88 let mut offenders = unexpected_names;
89 for name in ["srcref", "macro"] {
90 let count = names.iter().filter(|candidate| **candidate == name).count();
91 if count == 0 {
92 offenders.push(format!("missing:{name}"));
93 } else if count > 1 {
94 offenders.extend(std::iter::repeat_n(name.to_string(), count));
95 }
96 }
97 return unexpected(raw, RawNodeReason::AttributeSet, offenders);
98 }
99
100 let srcref = raw
101 .attributes()
102 .iter()
103 .find(|attribute| attribute.name() == "srcref");
104 let macro_attribute = raw
105 .attributes()
106 .iter()
107 .find(|attribute| attribute.name() == "macro");
108 let valid_srcref = srcref.is_some_and(|attribute| valid_srcref_value(attribute.value()));
109 let valid_macro = macro_attribute.is_some_and(|attribute| {
110 matches!(attribute.value().value(), RawRdValue::Character(values) if values.len() == 1 && values[0].is_some())
111 && attribute.value().attributes().is_empty()
112 });
113 if !valid_srcref || !valid_macro {
114 let malformed = raw
115 .attributes()
116 .iter()
117 .filter(|attribute| {
118 (attribute.name() == "srcref" && !valid_srcref)
119 || (attribute.name() == "macro" && !valid_macro)
120 })
121 .map(|attribute| attribute.name().to_string())
122 .collect();
123 return unexpected(raw, RawNodeReason::AttributeValueShape, malformed);
124 }
125
126 RawNodeClassification::ExpectedUserMacroDefinition
127}
128
129fn valid_srcref_value(object: &crate::RawRdObject) -> bool {
130 let valid_integer = matches!(object.value(), RawRdValue::Integer(values) if values.len() == 6 && values.iter().all(Option::is_some));
131 let attributes = object.attributes();
132 if !valid_integer || attributes.len() != 2 {
133 return false;
134 }
135 let srcfile = attributes
136 .iter()
137 .find(|attribute| attribute.name() == "srcfile");
138 let class = attributes
139 .iter()
140 .find(|attribute| attribute.name() == "class");
141 srcfile.is_some_and(|attribute| {
151 attribute.value().attributes().is_empty()
152 && (matches!(attribute.value().value(), RawRdValue::Persisted(values) if values.len() == 1 && values[0].is_some())
153 || matches!(
154 attribute.value().value(),
155 RawRdValue::Environment(crate::RawRdEnvironment::Other)
156 ))
157 }) && class.is_some_and(|attribute| {
158 attribute.value().attributes().is_empty()
159 && matches!(attribute.value().value(), RawRdValue::Character(values) if values.len() == 1 && values[0].as_deref() == Some("srcref"))
160 })
161}
162
163fn unexpected(
164 raw: &RawRdNode,
165 reason: RawNodeReason,
166 offending_attributes: Vec<String>,
167) -> RawNodeClassification {
168 RawNodeClassification::Unexpected(UnexpectedRawNode {
169 tag: raw.tag().map(str::to_string),
170 reason,
171 offending_attributes,
172 })
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::{RawRdValue, producer};
179
180 fn text() -> RdNode {
181 RdNode::Text("body".to_string())
182 }
183 fn attr(name: &str, value: RawRdValue) -> crate::RdAttribute {
184 producer::raw_attribute(name.to_string(), producer::raw_object(value, Vec::new()))
185 }
186 fn srcref_with(srcfile_value: RawRdValue) -> crate::RdAttribute {
187 let srcfile = producer::raw_attribute(
188 "srcfile".into(),
189 producer::raw_object(srcfile_value, Vec::new()),
190 );
191 let class = producer::raw_attribute(
192 "class".into(),
193 producer::raw_object(
194 RawRdValue::Character(vec![Some("srcref".into())]),
195 Vec::new(),
196 ),
197 );
198 producer::raw_attribute(
199 "srcref".into(),
200 producer::raw_object(RawRdValue::Integer(vec![Some(1); 6]), vec![srcfile, class]),
201 )
202 }
203 fn srcref() -> crate::RdAttribute {
204 srcref_with(RawRdValue::Persisted(vec![Some("env::1".into())]))
205 }
206 fn usermacro(
207 attributes: Vec<crate::RdAttribute>,
208 option: Option<Vec<RdNode>>,
209 children: Vec<RdNode>,
210 ) -> RawRdNode {
211 producer::raw_node(Some("USERMACRO".into()), option, children, None, attributes)
212 }
213
214 #[test]
215 fn accepts_pinned_usermacro_shape() {
216 let raw = usermacro(
217 vec![
218 srcref(),
219 attr("macro", RawRdValue::Character(vec![Some(r"\eval".into())])),
220 ],
221 None,
222 vec![text()],
223 );
224 assert_eq!(
225 classify_raw_node(&raw),
226 RawNodeClassification::ExpectedUserMacroDefinition
227 );
228 }
229
230 #[test]
231 fn accepts_both_supported_srcfile_provenances() {
232 use crate::RawRdEnvironment;
233 let environment = usermacro(
234 vec![
235 srcref_with(RawRdValue::Environment(RawRdEnvironment::Other)),
236 attr("macro", RawRdValue::Character(vec![Some(r"\eval".into())])),
237 ],
238 None,
239 vec![text()],
240 );
241 assert_eq!(
242 classify_raw_node(&environment),
243 RawNodeClassification::ExpectedUserMacroDefinition
244 );
245 for singleton in [
246 RawRdEnvironment::Global,
247 RawRdEnvironment::Base,
248 RawRdEnvironment::Empty,
249 ] {
250 let rejected = usermacro(
251 vec![
252 srcref_with(RawRdValue::Environment(singleton)),
253 attr("macro", RawRdValue::Character(vec![Some(r"\eval".into())])),
254 ],
255 None,
256 vec![text()],
257 );
258 assert!(matches!(
259 classify_raw_node(&rejected),
260 RawNodeClassification::Unexpected(UnexpectedRawNode {
261 reason: RawNodeReason::AttributeValueShape,
262 ..
263 })
264 ));
265 }
266 }
267
268 #[test]
269 fn payload_is_rejected_from_pinned_usermacro_shape() {
270 let raw = producer::raw_node(
271 Some("USERMACRO".into()),
272 None,
273 vec![text()],
274 Some(RawRdValue::Null),
275 vec![
276 srcref(),
277 attr("macro", RawRdValue::Character(vec![Some(r"\eval".into())])),
278 ],
279 );
280 assert!(matches!(
281 classify_raw_node(&raw),
282 RawNodeClassification::Unexpected(UnexpectedRawNode {
283 reason: RawNodeReason::PayloadPresent,
284 ..
285 })
286 ));
287 }
288
289 #[test]
290 fn rejects_wrong_tag_extra_attribute_wrong_value_and_option() {
291 let base = || {
292 vec![
293 srcref(),
294 attr("macro", RawRdValue::Character(vec![Some(r"\eval".into())])),
295 ]
296 };
297 let wrong_tag = producer::raw_node(Some("OTHER".into()), None, vec![text()], None, base());
298 assert!(matches!(
299 classify_raw_node(&wrong_tag),
300 RawNodeClassification::Unexpected(UnexpectedRawNode {
301 reason: RawNodeReason::WrongTag,
302 ..
303 })
304 ));
305 let extra = usermacro(
306 vec![
307 srcref(),
308 attr("macro", RawRdValue::Character(vec![Some(r"\eval".into())])),
309 attr("extra", RawRdValue::Null),
310 ],
311 None,
312 vec![text()],
313 );
314 assert!(matches!(
315 classify_raw_node(&extra),
316 RawNodeClassification::Unexpected(UnexpectedRawNode {
317 reason: RawNodeReason::AttributeSet,
318 ..
319 })
320 ));
321 let wrong_value = usermacro(
322 vec![srcref(), attr("macro", RawRdValue::Integer(vec![Some(1)]))],
323 None,
324 vec![text()],
325 );
326 assert!(matches!(
327 classify_raw_node(&wrong_value),
328 RawNodeClassification::Unexpected(UnexpectedRawNode {
329 reason: RawNodeReason::AttributeValueShape,
330 ..
331 })
332 ));
333 let with_option = usermacro(base(), Some(vec![text()]), vec![text()]);
334 let RawNodeClassification::Unexpected(details) = classify_raw_node(&with_option) else {
335 unreachable!("option should be unexpected")
336 };
337 assert!(
338 details
339 .offending_attributes()
340 .contains(&"Rd_option".to_string())
341 );
342 }
343
344 #[test]
345 fn reports_missing_and_duplicate_required_attributes() {
346 let missing = usermacro(
347 vec![attr("macro", RawRdValue::Character(vec![Some("x".into())]))],
348 None,
349 vec![text()],
350 );
351 let RawNodeClassification::Unexpected(details) = classify_raw_node(&missing) else {
352 unreachable!("missing attribute should be unexpected")
353 };
354 assert!(
355 details
356 .offending_attributes()
357 .contains(&"missing:srcref".to_string())
358 );
359
360 let duplicate = usermacro(
361 vec![
362 srcref(),
363 attr("macro", RawRdValue::Character(vec![Some("x".into())])),
364 attr("macro", RawRdValue::Character(vec![Some("y".into())])),
365 ],
366 None,
367 vec![text()],
368 );
369 let RawNodeClassification::Unexpected(details) = classify_raw_node(&duplicate) else {
370 unreachable!("duplicate attribute should be unexpected")
371 };
372 assert_eq!(
373 details.offending_attributes(),
374 &["macro".to_string(), "macro".to_string()]
375 );
376 }
377}