1use std::collections::{HashMap, HashSet};
6use std::fmt;
7
8use roxmltree::Node;
9
10use crate::schema::{
11 Check, CheckKind, Diagnostic, LetBinding, MessagePart, NamespaceBinding, Pattern, Phase, Rule,
12 Schema,
13};
14
15const SCHEMATRON_NAMESPACE: &str = "http://purl.oclc.org/dsdl/schematron";
16
17const SCHEMA_ALLOWED_CHILDREN: &[&str] =
26 &["title", "p", "ns", "let", "phase", "pattern", "diagnostics"];
27const PATTERN_ALLOWED_CHILDREN: &[&str] = &["title", "p", "let", "rule"];
28const IS_A_USE_ALLOWED_CHILDREN: &[&str] = &["p", "param"];
31const RULE_ALLOWED_CHILDREN: &[&str] = &["let", "assert", "report", "extends", "p"];
32const PHASE_ALLOWED_CHILDREN: &[&str] = &["title", "p", "let", "active"];
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub struct Position {
37 pub row: u32,
38 pub col: u32,
39}
40
41impl fmt::Display for Position {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(formatter, "{}:{}", self.row, self.col)
44 }
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
49pub enum ParseError {
50 InvalidXml(String),
52 WrongRootNamespace {
54 found: Option<String>,
55 position: Position,
56 },
57 NoPatterns,
59 MissingAttribute {
61 element: &'static str,
62 attribute: &'static str,
63 position: Position,
64 },
65 UnknownAbstractPattern { pattern: String, position: Position },
68 UnknownRuleReference { rule: String, position: Position },
73 CyclicRuleExtends { rule: String, position: Position },
76 UnexpectedElement { element: String, position: Position },
84 DuplicateId {
89 element: &'static str,
90 id: String,
91 position: Position,
92 },
93 UnknownActivePattern { pattern: String, position: Position },
99 UnknownDiagnosticReference { id: String, position: Position },
102}
103
104impl fmt::Display for ParseError {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 ParseError::InvalidXml(message) => {
108 write!(formatter, "invalid XML: {message}")
109 }
110 ParseError::WrongRootNamespace { found, position } => write!(
111 formatter,
112 "schema root must be in the Schematron namespace `{SCHEMATRON_NAMESPACE}`, found `{}` at {position}",
113 found.as_deref().unwrap_or("(none)")
114 ),
115 ParseError::NoPatterns => {
116 write!(formatter, "schema must contain at least one <pattern>")
117 }
118 ParseError::MissingAttribute {
119 element,
120 attribute,
121 position,
122 } => write!(
123 formatter,
124 "<{element}> is missing required attribute `{attribute}` at {position}"
125 ),
126 ParseError::UnknownAbstractPattern { pattern, position } => write!(
127 formatter,
128 "is-a references unknown abstract pattern `{pattern}` at {position}"
129 ),
130 ParseError::UnknownRuleReference { rule, position } => write!(
131 formatter,
132 "extends references unknown rule `{rule}` (not found in the same pattern) at {position}"
133 ),
134 ParseError::CyclicRuleExtends { rule, position } => write!(
135 formatter,
136 "cyclic extends involving rule `{rule}` at {position}"
137 ),
138 ParseError::UnexpectedElement { element, position } => write!(
139 formatter,
140 "unexpected <{element}> at {position} (unknown, or not supported here — see README.md)"
141 ),
142 ParseError::DuplicateId {
143 element,
144 id,
145 position,
146 } => write!(
147 formatter,
148 "duplicate {element} id `{id}` at {position} (already declared earlier in the schema)"
149 ),
150 ParseError::UnknownActivePattern { pattern, position } => write!(
151 formatter,
152 "active references unknown pattern `{pattern}` at {position}"
153 ),
154 ParseError::UnknownDiagnosticReference { id, position } => write!(
155 formatter,
156 "diagnostics references unknown diagnostic `{id}` at {position}"
157 ),
158 }
159 }
160}
161
162impl std::error::Error for ParseError {}
163
164pub fn parse(xml: &str) -> Result<Schema, ParseError> {
166 let document = roxmltree::Document::parse(xml)
167 .map_err(|error| ParseError::InvalidXml(error.to_string()))?;
168 let root = document.root_element();
169
170 if root.tag_name().namespace() != Some(SCHEMATRON_NAMESPACE) {
171 return Err(ParseError::WrongRootNamespace {
172 found: root.tag_name().namespace().map(str::to_owned),
173 position: position_of(root),
174 });
175 }
176
177 validate_known_children(root, SCHEMA_ALLOWED_CHILDREN)?;
178
179 let ns = collect_children(root, "ns", parse_ns)?;
180
181 let lets = collect_lets(root)?;
182
183 let default_phase = root.attribute("defaultPhase").map(str::to_owned);
184
185 let diagnostics = root
189 .children()
190 .find(|node| node.has_tag_name((SCHEMATRON_NAMESPACE, "diagnostics")))
191 .map(parse_diagnostics_element)
192 .transpose()?
193 .unwrap_or_default();
194 let diagnostic_ids: HashSet<String> = diagnostics.iter().map(|d| d.id.clone()).collect();
195
196 let abstract_patterns: HashMap<String, Node<'_, '_>> = root
200 .children()
201 .filter(|node| {
202 node.has_tag_name((SCHEMATRON_NAMESPACE, "pattern"))
203 && node.attribute("abstract") == Some("true")
204 })
205 .map(|node| {
206 node.attribute("id")
207 .map(|id| (id.to_owned(), node))
208 .ok_or_else(|| ParseError::MissingAttribute {
209 element: "pattern",
210 attribute: "id",
211 position: position_of(node),
212 })
213 })
214 .collect::<Result<_, _>>()?;
215
216 let mut pattern_ids: HashSet<String> = HashSet::new();
221
222 let patterns: Vec<Pattern> = root
223 .children()
224 .filter(|node| {
225 node.has_tag_name((SCHEMATRON_NAMESPACE, "pattern"))
226 && node.attribute("abstract") != Some("true")
227 })
228 .map(|node| {
229 let id = node.attribute("id").map(str::to_owned);
230 if let Some(id) = &id
231 && !pattern_ids.insert(id.clone())
232 {
233 return Err(ParseError::DuplicateId {
234 element: "pattern",
235 id: id.clone(),
236 position: position_of(node),
237 });
238 }
239
240 if let Some(is_a) = node.attribute("is-a") {
241 validate_known_children(node, IS_A_USE_ALLOWED_CHILDREN)?;
242 let abstract_node = abstract_patterns.get(is_a).copied().ok_or_else(|| {
243 ParseError::UnknownAbstractPattern {
244 pattern: is_a.to_owned(),
245 position: position_of(node),
246 }
247 })?;
248 let params = collect_children(node, "param", parse_param)?;
249 parse_pattern(abstract_node, id, ¶ms, &diagnostic_ids)
250 } else {
251 parse_pattern(node, id, &[], &diagnostic_ids)
252 }
253 })
254 .collect::<Result<_, _>>()?;
255
256 if patterns.is_empty() {
257 return Err(ParseError::NoPatterns);
258 }
259
260 let phases = collect_children(root, "phase", |node| parse_phase(node, &pattern_ids))?;
261
262 Ok(Schema {
263 ns,
264 lets,
265 default_phase,
266 phases,
267 patterns,
268 diagnostics,
269 })
270}
271
272fn parse_diagnostics_element(node: Node<'_, '_>) -> Result<Vec<Diagnostic>, ParseError> {
277 validate_known_children(node, &["diagnostic"])?;
278 let mut seen = HashSet::new();
279 node.children()
280 .filter(|child| child.has_tag_name((SCHEMATRON_NAMESPACE, "diagnostic")))
281 .map(|child| {
282 let diagnostic = parse_diagnostic(child)?;
283 if !seen.insert(diagnostic.id.clone()) {
284 return Err(ParseError::DuplicateId {
285 element: "diagnostic",
286 id: diagnostic.id,
287 position: position_of(child),
288 });
289 }
290 Ok(diagnostic)
291 })
292 .collect()
293}
294
295fn parse_diagnostic(node: Node<'_, '_>) -> Result<Diagnostic, ParseError> {
300 let id = required_attribute(node, "diagnostic", "id")?.to_owned();
301 let (role, message) = parse_role_and_message(node, &[])?;
302 Ok(Diagnostic { id, role, message })
303}
304
305fn parse_role_and_message(
309 node: Node<'_, '_>,
310 params: &[(String, String)],
311) -> Result<(Option<String>, Vec<MessagePart>), ParseError> {
312 let role = node.attribute("role").map(str::to_owned);
313 let mut message = Vec::new();
314 collect_message_parts(node, &mut message, params)?;
315 Ok((role, message))
316}
317
318fn parse_ns(node: Node<'_, '_>) -> Result<NamespaceBinding, ParseError> {
323 let prefix = required_attribute(node, "ns", "prefix")?.to_owned();
324 let uri = required_attribute(node, "ns", "uri")?.to_owned();
325 Ok(NamespaceBinding { prefix, uri })
326}
327
328fn parse_let(node: Node<'_, '_>) -> Result<LetBinding, ParseError> {
334 let name = required_attribute(node, "let", "name")?.to_owned();
335 let value = required_attribute(node, "let", "value")?.to_owned();
336 Ok(LetBinding { name, value })
337}
338
339fn parse_phase(node: Node<'_, '_>, pattern_ids: &HashSet<String>) -> Result<Phase, ParseError> {
346 validate_known_children(node, PHASE_ALLOWED_CHILDREN)?;
347 let id = required_attribute(node, "phase", "id")?.to_owned();
348 let lets = collect_lets(node)?;
349 let active = collect_children(node, "active", |child| parse_active(child, pattern_ids))?;
350 Ok(Phase { id, lets, active })
351}
352
353fn parse_active(node: Node<'_, '_>, pattern_ids: &HashSet<String>) -> Result<String, ParseError> {
354 let pattern = required_attribute(node, "active", "pattern")?;
355 if !pattern_ids.contains(pattern) {
356 return Err(ParseError::UnknownActivePattern {
357 pattern: pattern.to_owned(),
358 position: position_of(node),
359 });
360 }
361 Ok(pattern.to_owned())
362}
363
364fn parse_param(node: Node<'_, '_>) -> Result<(String, String), ParseError> {
367 let name = required_attribute(node, "param", "name")?.to_owned();
368 let value = required_attribute(node, "param", "value")?.to_owned();
369 Ok((name, value))
370}
371
372fn substitute(text: &str, params: &[(String, String)]) -> String {
379 params.iter().fold(text.to_owned(), |acc, (name, value)| {
380 acc.replace(&format!("${name}"), value)
381 })
382}
383
384fn parse_pattern(
392 node: Node<'_, '_>,
393 id: Option<String>,
394 params: &[(String, String)],
395 diagnostic_ids: &HashSet<String>,
396) -> Result<Pattern, ParseError> {
397 validate_known_children(node, PATTERN_ALLOWED_CHILDREN)?;
398
399 let lets = collect_lets(node)?;
400
401 let rule_index: HashMap<String, Node<'_, '_>> = node
402 .children()
403 .filter(|child| child.has_tag_name((SCHEMATRON_NAMESPACE, "rule")))
404 .filter_map(|child| child.attribute("id").map(|id| (id.to_owned(), child)))
405 .collect();
406
407 let rules = node
408 .children()
409 .filter(|child| {
410 child.has_tag_name((SCHEMATRON_NAMESPACE, "rule"))
411 && child.attribute("abstract") != Some("true")
412 })
413 .map(|child| parse_rule(child, &rule_index, params, diagnostic_ids))
414 .collect::<Result<_, _>>()?;
415
416 Ok(Pattern { id, lets, rules })
417}
418
419fn parse_rule(
423 node: Node<'_, '_>,
424 rule_index: &HashMap<String, Node<'_, '_>>,
425 params: &[(String, String)],
426 diagnostic_ids: &HashSet<String>,
427) -> Result<Rule, ParseError> {
428 validate_known_children(node, RULE_ALLOWED_CHILDREN)?;
429
430 let context = substitute(required_attribute(node, "rule", "context")?, params);
431
432 let lets = collect_lets(node)?;
433
434 let mut visiting: Vec<String> = node
437 .attribute("id")
438 .map(str::to_owned)
439 .into_iter()
440 .collect();
441
442 let mut checks = Vec::new();
443 collect_checks(
444 node,
445 rule_index,
446 params,
447 &mut visiting,
448 &mut checks,
449 diagnostic_ids,
450 )?;
451
452 Ok(Rule {
453 context,
454 lets,
455 checks,
456 })
457}
458
459fn collect_checks(
466 node: Node<'_, '_>,
467 rule_index: &HashMap<String, Node<'_, '_>>,
468 params: &[(String, String)],
469 visiting: &mut Vec<String>,
470 checks: &mut Vec<Check>,
471 diagnostic_ids: &HashSet<String>,
472) -> Result<(), ParseError> {
473 for child in node.children() {
474 if let Some(kind) = check_kind(child) {
475 checks.push(parse_check(kind, child, params, diagnostic_ids)?);
476 } else if child.has_tag_name((SCHEMATRON_NAMESPACE, "extends")) {
477 let target_id =
478 child
479 .attribute("rule")
480 .ok_or_else(|| ParseError::MissingAttribute {
481 element: "extends",
482 attribute: "rule",
483 position: position_of(child),
484 })?;
485 let target = rule_index.get(target_id).copied().ok_or_else(|| {
486 ParseError::UnknownRuleReference {
487 rule: target_id.to_owned(),
488 position: position_of(child),
489 }
490 })?;
491 if visiting.iter().any(|id| id == target_id) {
492 return Err(ParseError::CyclicRuleExtends {
493 rule: target_id.to_owned(),
494 position: position_of(child),
495 });
496 }
497 visiting.push(target_id.to_owned());
498 collect_checks(target, rule_index, params, visiting, checks, diagnostic_ids)?;
499 visiting.pop();
500 }
501 }
502 Ok(())
503}
504
505fn check_kind(node: Node<'_, '_>) -> Option<CheckKind> {
506 if node.has_tag_name((SCHEMATRON_NAMESPACE, "assert")) {
507 Some(CheckKind::Assert)
508 } else if node.has_tag_name((SCHEMATRON_NAMESPACE, "report")) {
509 Some(CheckKind::Report)
510 } else {
511 None
512 }
513}
514
515fn parse_check(
516 kind: CheckKind,
517 node: Node<'_, '_>,
518 params: &[(String, String)],
519 diagnostic_ids: &HashSet<String>,
520) -> Result<Check, ParseError> {
521 let element = match kind {
522 CheckKind::Assert => "assert",
523 CheckKind::Report => "report",
524 };
525 let test = substitute(required_attribute(node, element, "test")?, params);
526 let id = node.attribute("id").map(str::to_owned);
527 let (role, message) = parse_role_and_message(node, params)?;
528 let diagnostics: Vec<String> = node
529 .attribute("diagnostics")
530 .map(|ids| ids.split_whitespace().map(str::to_owned).collect())
531 .unwrap_or_default();
532 for diagnostic_id in &diagnostics {
533 if !diagnostic_ids.contains(diagnostic_id) {
534 return Err(ParseError::UnknownDiagnosticReference {
535 id: diagnostic_id.clone(),
536 position: position_of(node),
537 });
538 }
539 }
540
541 Ok(Check {
542 kind,
543 test,
544 id,
545 role,
546 message,
547 diagnostics,
548 })
549}
550
551fn collect_message_parts(
563 node: Node<'_, '_>,
564 parts: &mut Vec<MessagePart>,
565 params: &[(String, String)],
566) -> Result<(), ParseError> {
567 for child in node.children() {
568 if child.is_text() {
569 if let Some(text) = child.text() {
570 parts.push(MessagePart::Text(text.to_owned()));
571 }
572 } else if child.has_tag_name((SCHEMATRON_NAMESPACE, "value-of")) {
573 let select = required_attribute(child, "value-of", "select")?;
574 parts.push(MessagePart::ValueOf(substitute(select, params)));
575 } else if child.is_element() {
576 collect_message_parts(child, parts, params)?;
577 }
578 }
579 Ok(())
580}
581
582fn required_attribute<'a>(
586 node: Node<'a, '_>,
587 element: &'static str,
588 attribute: &'static str,
589) -> Result<&'a str, ParseError> {
590 node.attribute(attribute)
591 .ok_or_else(|| ParseError::MissingAttribute {
592 element,
593 attribute,
594 position: position_of(node),
595 })
596}
597
598fn collect_lets(node: Node<'_, '_>) -> Result<Vec<LetBinding>, ParseError> {
602 collect_children(node, "let", parse_let)
603}
604
605fn collect_children<T>(
609 node: Node<'_, '_>,
610 tag: &str,
611 f: impl FnMut(Node<'_, '_>) -> Result<T, ParseError>,
612) -> Result<Vec<T>, ParseError> {
613 node.children()
614 .filter(|child| child.has_tag_name((SCHEMATRON_NAMESPACE, tag)))
615 .map(f)
616 .collect()
617}
618
619fn validate_known_children(node: Node<'_, '_>, allowed: &[&str]) -> Result<(), ParseError> {
626 for child in node.children() {
627 if child.is_element()
628 && child.tag_name().namespace() == Some(SCHEMATRON_NAMESPACE)
629 && !allowed.contains(&child.tag_name().name())
630 {
631 return Err(ParseError::UnexpectedElement {
632 element: child.tag_name().name().to_owned(),
633 position: position_of(child),
634 });
635 }
636 }
637 Ok(())
638}
639
640fn position_of(node: Node<'_, '_>) -> Position {
641 let text_pos = node.document().text_pos_at(node.range().start);
642 Position {
643 row: text_pos.row,
644 col: text_pos.col,
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651
652 #[test]
653 fn minimal_valid_schema() {
654 let schema = parse(
655 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
656 <pattern>
657 <rule context="/root">
658 <assert test="foo">message</assert>
659 </rule>
660 </pattern>
661 </schema>"#,
662 )
663 .unwrap();
664
665 assert_eq!(schema.ns, vec![]);
666 assert_eq!(schema.patterns.len(), 1);
667 let pattern = &schema.patterns[0];
668 assert_eq!(pattern.id, None);
669 assert_eq!(pattern.rules.len(), 1);
670 let rule = &pattern.rules[0];
671 assert_eq!(rule.context, "/root");
672 assert_eq!(rule.checks.len(), 1);
673 let check = &rule.checks[0];
674 assert_eq!(check.kind, CheckKind::Assert);
675 assert_eq!(check.test, "foo");
676 assert_eq!(check.message, vec![MessagePart::Text("message".to_owned())]);
677 }
678
679 #[test]
680 fn multiple_patterns_rules_and_checks() {
681 let schema = parse(
682 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
683 <pattern id="p1">
684 <rule context="/a">
685 <assert test="t1">m1</assert>
686 <assert test="t2">m2</assert>
687 </rule>
688 <rule context="/b">
689 <assert test="t3">m3</assert>
690 </rule>
691 </pattern>
692 <pattern id="p2">
693 <rule context="/c">
694 <assert test="t4">m4</assert>
695 </rule>
696 </pattern>
697 </schema>"#,
698 )
699 .unwrap();
700
701 assert_eq!(schema.patterns.len(), 2);
702 assert_eq!(schema.patterns[0].id.as_deref(), Some("p1"));
703 assert_eq!(schema.patterns[0].rules.len(), 2);
704 assert_eq!(schema.patterns[0].rules[0].checks.len(), 2);
705 assert_eq!(schema.patterns[0].rules[1].checks.len(), 1);
706 assert_eq!(schema.patterns[1].id.as_deref(), Some("p2"));
707 assert_eq!(schema.patterns[1].rules.len(), 1);
708 }
709
710 #[test]
711 fn report_alongside_assert() {
712 let schema = parse(
713 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
714 <pattern>
715 <rule context="/root">
716 <assert test="a-test">assert message</assert>
717 <report test="r-test">report message</report>
718 </rule>
719 </pattern>
720 </schema>"#,
721 )
722 .unwrap();
723
724 let checks = &schema.patterns[0].rules[0].checks;
725 assert_eq!(checks.len(), 2);
726 assert_eq!(checks[0].kind, CheckKind::Assert);
727 assert_eq!(checks[0].test, "a-test");
728 assert_eq!(checks[1].kind, CheckKind::Report);
729 assert_eq!(checks[1].test, "r-test");
730 }
731
732 #[test]
733 fn ns_bindings_are_captured() {
734 let schema = parse(
735 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
736 <ns prefix="ex" uri="http://example.com/ns"/>
737 <ns prefix="other" uri="http://example.com/other"/>
738 <pattern>
739 <rule context="/root">
740 <assert test="foo">message</assert>
741 </rule>
742 </pattern>
743 </schema>"#,
744 )
745 .unwrap();
746
747 assert_eq!(
748 schema.ns,
749 vec![
750 NamespaceBinding {
751 prefix: "ex".to_owned(),
752 uri: "http://example.com/ns".to_owned(),
753 },
754 NamespaceBinding {
755 prefix: "other".to_owned(),
756 uri: "http://example.com/other".to_owned(),
757 },
758 ]
759 );
760 }
761
762 #[test]
763 fn ns_missing_prefix_is_an_error() {
764 let error = parse(
765 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
766 <ns uri="http://example.com/ns"/>
767 <pattern>
768 <rule context="/root">
769 <assert test="foo">message</assert>
770 </rule>
771 </pattern>
772 </schema>"#,
773 )
774 .unwrap_err();
775
776 assert_eq!(
777 error,
778 ParseError::MissingAttribute {
779 element: "ns",
780 attribute: "prefix",
781 position: Position { row: 2, col: 17 },
782 }
783 );
784 }
785
786 #[test]
787 fn ns_missing_uri_is_an_error() {
788 let error = parse(
789 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
790 <ns prefix="ex"/>
791 <pattern>
792 <rule context="/root">
793 <assert test="foo">message</assert>
794 </rule>
795 </pattern>
796 </schema>"#,
797 )
798 .unwrap_err();
799
800 assert_eq!(
801 error,
802 ParseError::MissingAttribute {
803 element: "ns",
804 attribute: "uri",
805 position: Position { row: 2, col: 17 },
806 }
807 );
808 }
809
810 #[test]
811 fn id_and_role_present() {
812 let schema = parse(
813 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
814 <pattern>
815 <rule context="/root">
816 <assert test="foo" id="my-id" role="warning">message</assert>
817 </rule>
818 </pattern>
819 </schema>"#,
820 )
821 .unwrap();
822
823 let check = &schema.patterns[0].rules[0].checks[0];
824 assert_eq!(check.id.as_deref(), Some("my-id"));
825 assert_eq!(check.role.as_deref(), Some("warning"));
826 }
827
828 #[test]
829 fn id_and_role_absent() {
830 let schema = parse(
831 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
832 <pattern>
833 <rule context="/root">
834 <assert test="foo">message</assert>
835 </rule>
836 </pattern>
837 </schema>"#,
838 )
839 .unwrap();
840
841 let check = &schema.patterns[0].rules[0].checks[0];
842 assert_eq!(check.id, None);
843 assert_eq!(check.role, None);
844 }
845
846 #[test]
850 fn message_text_is_captured_untrimmed() {
851 let schema = parse(
852 "<schema xmlns=\"http://purl.oclc.org/dsdl/schematron\">\
853 <pattern>\
854 <rule context=\"/root\">\
855 <assert test=\"foo\">\n first second \n</assert>\
856 </rule>\
857 </pattern>\
858 </schema>",
859 )
860 .unwrap();
861
862 let check = &schema.patterns[0].rules[0].checks[0];
863 assert_eq!(
864 check.message,
865 vec![MessagePart::Text("\n first second \n".to_owned())]
866 );
867 }
868
869 #[test]
870 fn message_text_without_surrounding_whitespace_is_unchanged() {
871 let schema = parse(
872 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
873 <pattern>
874 <rule context="/root">
875 <assert test="foo">exact</assert>
876 </rule>
877 </pattern>
878 </schema>"#,
879 )
880 .unwrap();
881
882 let check = &schema.patterns[0].rules[0].checks[0];
883 assert_eq!(check.message, vec![MessagePart::Text("exact".to_owned())]);
884 }
885
886 #[test]
887 fn value_of_and_wrapper_element_text_are_captured() {
888 let schema = parse(
889 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
890 <pattern>
891 <rule context="/root">
892 <assert test="foo">before <value-of select="@x"/> after <emph>wrapped</emph></assert>
893 </rule>
894 </pattern>
895 </schema>"#,
896 )
897 .unwrap();
898
899 let check = &schema.patterns[0].rules[0].checks[0];
900 assert_eq!(
901 check.message,
902 vec![
903 MessagePart::Text("before ".to_owned()),
904 MessagePart::ValueOf("@x".to_owned()),
905 MessagePart::Text(" after ".to_owned()),
906 MessagePart::Text("wrapped".to_owned()),
907 ]
908 );
909 }
910
911 #[test]
912 fn value_of_missing_select_is_an_error() {
913 let error = parse(
914 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
915 <pattern>
916 <rule context="/root">
917 <assert test="foo"><value-of/></assert>
918 </rule>
919 </pattern>
920 </schema>"#,
921 )
922 .unwrap_err();
923
924 assert_eq!(
925 error,
926 ParseError::MissingAttribute {
927 element: "value-of",
928 attribute: "select",
929 position: Position { row: 4, col: 44 },
930 }
931 );
932 }
933
934 #[test]
935 fn missing_context_is_an_error() {
936 let error = parse(
937 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
938 <pattern>
939 <rule>
940 <assert test="foo">message</assert>
941 </rule>
942 </pattern>
943 </schema>"#,
944 )
945 .unwrap_err();
946
947 assert_eq!(
948 error,
949 ParseError::MissingAttribute {
950 element: "rule",
951 attribute: "context",
952 position: Position { row: 3, col: 21 },
953 }
954 );
955 }
956
957 #[test]
958 fn missing_test_is_an_error() {
959 let error = parse(
960 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
961 <pattern>
962 <rule context="/root">
963 <assert>message</assert>
964 </rule>
965 </pattern>
966 </schema>"#,
967 )
968 .unwrap_err();
969
970 assert_eq!(
971 error,
972 ParseError::MissingAttribute {
973 element: "assert",
974 attribute: "test",
975 position: Position { row: 4, col: 25 },
976 }
977 );
978 }
979
980 #[test]
981 fn no_pattern_is_an_error() {
982 let error = parse(r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron"/>"#).unwrap_err();
983
984 assert_eq!(error, ParseError::NoPatterns);
985 }
986
987 #[test]
988 fn wrong_root_namespace_is_an_error() {
989 let error = parse(r#"<schema xmlns="http://example.com/not-schematron"/>"#).unwrap_err();
990
991 assert_eq!(
992 error,
993 ParseError::WrongRootNamespace {
994 found: Some("http://example.com/not-schematron".to_owned()),
995 position: Position { row: 1, col: 1 },
996 }
997 );
998 }
999
1000 #[test]
1001 fn diagnostics_are_captured() {
1002 let schema = parse(
1003 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1004 <diagnostics>
1005 <diagnostic id="d1" role="warning">before <value-of select="."/> after</diagnostic>
1006 <diagnostic id="d2">second</diagnostic>
1007 </diagnostics>
1008 <pattern>
1009 <rule context="/root">
1010 <assert test="foo" diagnostics="d1 d2">message</assert>
1011 </rule>
1012 </pattern>
1013 </schema>"#,
1014 )
1015 .unwrap();
1016
1017 assert_eq!(schema.diagnostics.len(), 2);
1018 assert_eq!(schema.diagnostics[0].id, "d1");
1019 assert_eq!(schema.diagnostics[0].role.as_deref(), Some("warning"));
1020 assert_eq!(
1021 schema.diagnostics[0].message,
1022 vec![
1023 MessagePart::Text("before ".to_owned()),
1024 MessagePart::ValueOf(".".to_owned()),
1025 MessagePart::Text(" after".to_owned()),
1026 ]
1027 );
1028
1029 let check = &schema.patterns[0].rules[0].checks[0];
1030 assert_eq!(check.diagnostics, vec!["d1".to_owned(), "d2".to_owned()]);
1031 }
1032
1033 #[test]
1039 fn diagnostics_unknown_reference_is_an_error() {
1040 let error = parse(
1041 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1042 <diagnostics>
1043 <diagnostic id="d1">known</diagnostic>
1044 </diagnostics>
1045 <pattern>
1046 <rule context="/root">
1047 <assert test="foo" diagnostics="d1 missing">message</assert>
1048 </rule>
1049 </pattern>
1050 </schema>"#,
1051 )
1052 .unwrap_err();
1053
1054 assert!(matches!(
1055 error,
1056 ParseError::UnknownDiagnosticReference { id, .. } if id == "missing"
1057 ));
1058 }
1059
1060 #[test]
1061 fn duplicate_diagnostic_id_is_an_error() {
1062 let error = parse(
1063 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1064 <diagnostics>
1065 <diagnostic id="d1">first</diagnostic>
1066 <diagnostic id="d1">second</diagnostic>
1067 </diagnostics>
1068 <pattern>
1069 <rule context="/root">
1070 <assert test="foo" diagnostics="d1">message</assert>
1071 </rule>
1072 </pattern>
1073 </schema>"#,
1074 )
1075 .unwrap_err();
1076
1077 assert!(matches!(
1078 error,
1079 ParseError::DuplicateId { element: "diagnostic", id, .. } if id == "d1"
1080 ));
1081 }
1082
1083 #[test]
1084 fn check_without_diagnostics_attribute_has_an_empty_list() {
1085 let schema = parse(
1086 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1087 <pattern>
1088 <rule context="/root">
1089 <assert test="foo">message</assert>
1090 </rule>
1091 </pattern>
1092 </schema>"#,
1093 )
1094 .unwrap();
1095
1096 assert_eq!(
1097 schema.patterns[0].rules[0].checks[0].diagnostics,
1098 Vec::<String>::new()
1099 );
1100 assert_eq!(schema.diagnostics, vec![]);
1101 }
1102
1103 #[test]
1104 fn diagnostic_missing_id_is_an_error() {
1105 let error = parse(
1106 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1107 <diagnostics>
1108 <diagnostic>text</diagnostic>
1109 </diagnostics>
1110 <pattern>
1111 <rule context="/root">
1112 <assert test="foo">message</assert>
1113 </rule>
1114 </pattern>
1115 </schema>"#,
1116 )
1117 .unwrap_err();
1118
1119 assert_eq!(
1120 error,
1121 ParseError::MissingAttribute {
1122 element: "diagnostic",
1123 attribute: "id",
1124 position: Position { row: 3, col: 21 },
1125 }
1126 );
1127 }
1128
1129 #[test]
1130 fn let_bindings_are_captured_at_schema_pattern_and_rule_level() {
1131 let schema = parse(
1132 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1133 <let name="a" value="1"/>
1134 <pattern>
1135 <let name="b" value="2"/>
1136 <rule context="/root">
1137 <let name="c" value="3"/>
1138 <assert test="foo">message</assert>
1139 </rule>
1140 </pattern>
1141 </schema>"#,
1142 )
1143 .unwrap();
1144
1145 assert_eq!(
1146 schema.lets,
1147 vec![LetBinding {
1148 name: "a".to_owned(),
1149 value: "1".to_owned(),
1150 }]
1151 );
1152 assert_eq!(
1153 schema.patterns[0].lets,
1154 vec![LetBinding {
1155 name: "b".to_owned(),
1156 value: "2".to_owned(),
1157 }]
1158 );
1159 assert_eq!(
1160 schema.patterns[0].rules[0].lets,
1161 vec![LetBinding {
1162 name: "c".to_owned(),
1163 value: "3".to_owned(),
1164 }]
1165 );
1166 }
1167
1168 #[test]
1169 fn let_missing_value_is_an_error() {
1170 let error = parse(
1171 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1172 <let name="a"/>
1173 <pattern>
1174 <rule context="/root">
1175 <assert test="foo">message</assert>
1176 </rule>
1177 </pattern>
1178 </schema>"#,
1179 )
1180 .unwrap_err();
1181
1182 assert_eq!(
1183 error,
1184 ParseError::MissingAttribute {
1185 element: "let",
1186 attribute: "value",
1187 position: Position { row: 2, col: 17 },
1188 }
1189 );
1190 }
1191
1192 #[test]
1193 fn default_phase_and_phases_are_captured() {
1194 let schema = parse(
1195 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="p1">
1196 <phase id="p1">
1197 <let name="x" value="1"/>
1198 <active pattern="a"/>
1199 <active pattern="b"/>
1200 </phase>
1201 <phase id="p2">
1202 <active pattern="b"/>
1203 </phase>
1204 <pattern id="a">
1205 <rule context="/root">
1206 <assert test="foo">message</assert>
1207 </rule>
1208 </pattern>
1209 <pattern id="b">
1210 <rule context="/root">
1211 <assert test="foo">message</assert>
1212 </rule>
1213 </pattern>
1214 </schema>"#,
1215 )
1216 .unwrap();
1217
1218 assert_eq!(schema.default_phase.as_deref(), Some("p1"));
1219 assert_eq!(schema.phases.len(), 2);
1220 assert_eq!(schema.phases[0].id, "p1");
1221 assert_eq!(
1222 schema.phases[0].lets,
1223 vec![LetBinding {
1224 name: "x".to_owned(),
1225 value: "1".to_owned(),
1226 }]
1227 );
1228 assert_eq!(
1229 schema.phases[0].active,
1230 vec!["a".to_owned(), "b".to_owned()]
1231 );
1232 assert_eq!(schema.phases[1].id, "p2");
1233 assert_eq!(schema.phases[1].active, vec!["b".to_owned()]);
1234 }
1235
1236 #[test]
1237 fn schema_without_phase_has_no_default_phase_and_no_phases() {
1238 let schema = parse(
1239 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1240 <pattern>
1241 <rule context="/root">
1242 <assert test="foo">message</assert>
1243 </rule>
1244 </pattern>
1245 </schema>"#,
1246 )
1247 .unwrap();
1248
1249 assert_eq!(schema.default_phase, None);
1250 assert_eq!(schema.phases, vec![]);
1251 }
1252
1253 #[test]
1254 fn phase_missing_id_is_an_error() {
1255 let error = parse(
1256 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1257 <phase>
1258 <active pattern="a"/>
1259 </phase>
1260 <pattern id="a">
1261 <rule context="/root">
1262 <assert test="foo">message</assert>
1263 </rule>
1264 </pattern>
1265 </schema>"#,
1266 )
1267 .unwrap_err();
1268
1269 assert_eq!(
1270 error,
1271 ParseError::MissingAttribute {
1272 element: "phase",
1273 attribute: "id",
1274 position: Position { row: 2, col: 17 },
1275 }
1276 );
1277 }
1278
1279 #[test]
1280 fn active_missing_pattern_is_an_error() {
1281 let error = parse(
1282 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1283 <phase id="p1">
1284 <active/>
1285 </phase>
1286 <pattern id="a">
1287 <rule context="/root">
1288 <assert test="foo">message</assert>
1289 </rule>
1290 </pattern>
1291 </schema>"#,
1292 )
1293 .unwrap_err();
1294
1295 assert_eq!(
1296 error,
1297 ParseError::MissingAttribute {
1298 element: "active",
1299 attribute: "pattern",
1300 position: Position { row: 3, col: 21 },
1301 }
1302 );
1303 }
1304
1305 #[test]
1309 fn active_unknown_pattern_is_an_error() {
1310 let error = parse(
1311 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1312 <phase id="p1">
1313 <active pattern="missing"/>
1314 </phase>
1315 <pattern id="a">
1316 <rule context="/root">
1317 <assert test="foo">message</assert>
1318 </rule>
1319 </pattern>
1320 </schema>"#,
1321 )
1322 .unwrap_err();
1323
1324 assert!(matches!(
1325 error,
1326 ParseError::UnknownActivePattern { pattern, .. } if pattern == "missing"
1327 ));
1328 }
1329
1330 #[test]
1335 fn active_referencing_an_abstract_pattern_id_is_an_error() {
1336 let error = parse(
1337 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1338 <phase id="p1">
1339 <active pattern="tpl"/>
1340 </phase>
1341 <pattern abstract="true" id="tpl">
1342 <rule context="/root">
1343 <assert test="foo">message</assert>
1344 </rule>
1345 </pattern>
1346 <pattern id="a">
1347 <rule context="/root">
1348 <assert test="foo">message</assert>
1349 </rule>
1350 </pattern>
1351 </schema>"#,
1352 )
1353 .unwrap_err();
1354
1355 assert!(matches!(
1356 error,
1357 ParseError::UnknownActivePattern { pattern, .. } if pattern == "tpl"
1358 ));
1359 }
1360
1361 #[test]
1362 fn duplicate_pattern_id_is_an_error() {
1363 let error = parse(
1364 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1365 <pattern id="a">
1366 <rule context="/root">
1367 <assert test="foo">message</assert>
1368 </rule>
1369 </pattern>
1370 <pattern id="a">
1371 <rule context="/root">
1372 <assert test="foo">message</assert>
1373 </rule>
1374 </pattern>
1375 </schema>"#,
1376 )
1377 .unwrap_err();
1378
1379 assert!(matches!(
1380 error,
1381 ParseError::DuplicateId { element: "pattern", id, .. } if id == "a"
1382 ));
1383 }
1384
1385 #[test]
1386 fn unexpected_element_at_schema_level_is_an_error() {
1387 let error = parse(
1388 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1389 <include href="other.sch"/>
1390 <pattern>
1391 <rule context="/root">
1392 <assert test="foo">message</assert>
1393 </rule>
1394 </pattern>
1395 </schema>"#,
1396 )
1397 .unwrap_err();
1398
1399 assert!(matches!(
1400 error,
1401 ParseError::UnexpectedElement { element, .. } if element == "include"
1402 ));
1403 }
1404
1405 #[test]
1406 fn unexpected_element_inside_rule_is_an_error() {
1407 let error = parse(
1408 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1409 <pattern>
1410 <rule context="/root">
1411 <asert test="foo">typo'd element name</asert>
1412 </rule>
1413 </pattern>
1414 </schema>"#,
1415 )
1416 .unwrap_err();
1417
1418 assert!(matches!(
1419 error,
1420 ParseError::UnexpectedElement { element, .. } if element == "asert"
1421 ));
1422 }
1423
1424 #[test]
1428 fn title_and_p_are_accepted_but_ignored() {
1429 let schema = parse(
1430 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1431 <title>My Schema</title>
1432 <p>Some documentation.</p>
1433 <pattern>
1434 <rule context="/root">
1435 <assert test="foo">message</assert>
1436 </rule>
1437 </pattern>
1438 </schema>"#,
1439 )
1440 .unwrap();
1441
1442 assert_eq!(schema.patterns.len(), 1);
1443 }
1444
1445 #[test]
1446 fn extends_inlines_checks_from_another_rule_in_the_same_pattern() {
1447 let schema = parse(
1448 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1449 <pattern>
1450 <rule abstract="true" id="base">
1451 <assert test="false()">BASE-CHECK</assert>
1452 </rule>
1453 <rule context="/root">
1454 <extends rule="base"/>
1455 <assert test="false()">OWN-CHECK</assert>
1456 </rule>
1457 </pattern>
1458 </schema>"#,
1459 )
1460 .unwrap();
1461
1462 assert_eq!(schema.patterns[0].rules.len(), 1);
1464 let checks = &schema.patterns[0].rules[0].checks;
1465 assert_eq!(checks.len(), 2);
1466 assert_eq!(
1467 checks[0].message,
1468 vec![MessagePart::Text("BASE-CHECK".to_owned())]
1469 );
1470 assert_eq!(
1471 checks[1].message,
1472 vec![MessagePart::Text("OWN-CHECK".to_owned())]
1473 );
1474 }
1475
1476 #[test]
1477 fn extends_is_transitive() {
1478 let schema = parse(
1479 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1480 <pattern>
1481 <rule abstract="true" id="a">
1482 <assert test="false()">A</assert>
1483 </rule>
1484 <rule abstract="true" id="b">
1485 <extends rule="a"/>
1486 <assert test="false()">B</assert>
1487 </rule>
1488 <rule context="/root">
1489 <extends rule="b"/>
1490 </rule>
1491 </pattern>
1492 </schema>"#,
1493 )
1494 .unwrap();
1495
1496 let checks = &schema.patterns[0].rules[0].checks;
1497 assert_eq!(
1498 checks.iter().map(|c| c.message.clone()).collect::<Vec<_>>(),
1499 vec![
1500 vec![MessagePart::Text("A".to_owned())],
1501 vec![MessagePart::Text("B".to_owned())],
1502 ]
1503 );
1504 }
1505
1506 #[test]
1507 fn cyclic_extends_is_an_error() {
1508 let error = parse(
1509 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1510 <pattern>
1511 <rule abstract="true" id="a">
1512 <extends rule="b"/>
1513 </rule>
1514 <rule abstract="true" id="b">
1515 <extends rule="a"/>
1516 </rule>
1517 <rule context="/root">
1518 <extends rule="a"/>
1519 </rule>
1520 </pattern>
1521 </schema>"#,
1522 )
1523 .unwrap_err();
1524
1525 assert!(matches!(
1526 error,
1527 ParseError::CyclicRuleExtends { rule, .. } if rule == "a"
1528 ));
1529 }
1530
1531 #[test]
1532 fn extends_unknown_rule_is_an_error() {
1533 let error = parse(
1534 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1535 <pattern>
1536 <rule context="/root">
1537 <extends rule="missing"/>
1538 </rule>
1539 </pattern>
1540 </schema>"#,
1541 )
1542 .unwrap_err();
1543
1544 assert!(matches!(
1545 error,
1546 ParseError::UnknownRuleReference { rule, .. } if rule == "missing"
1547 ));
1548 }
1549
1550 #[test]
1551 fn is_a_instantiates_abstract_pattern_with_param_substitution() {
1552 let schema = parse(
1553 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1554 <pattern abstract="true" id="tpl">
1555 <rule context="$el">
1556 <assert test="$el = 1">msg</assert>
1557 </rule>
1558 </pattern>
1559 <pattern is-a="tpl" id="concrete">
1560 <param name="el" value="item"/>
1561 </pattern>
1562 </schema>"#,
1563 )
1564 .unwrap();
1565
1566 assert_eq!(schema.patterns.len(), 1);
1568 assert_eq!(schema.patterns[0].id.as_deref(), Some("concrete"));
1569 assert_eq!(schema.patterns[0].rules[0].context, "item");
1570 assert_eq!(schema.patterns[0].rules[0].checks[0].test, "item = 1");
1571 }
1572
1573 #[test]
1574 fn two_is_a_uses_of_the_same_abstract_pattern_are_independent() {
1575 let schema = parse(
1576 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1577 <pattern abstract="true" id="tpl">
1578 <rule context="$el">
1579 <assert test="true()">m</assert>
1580 </rule>
1581 </pattern>
1582 <pattern is-a="tpl" id="p1">
1583 <param name="el" value="a"/>
1584 </pattern>
1585 <pattern is-a="tpl" id="p2">
1586 <param name="el" value="b"/>
1587 </pattern>
1588 </schema>"#,
1589 )
1590 .unwrap();
1591
1592 assert_eq!(schema.patterns.len(), 2);
1593 assert_eq!(schema.patterns[0].id.as_deref(), Some("p1"));
1594 assert_eq!(schema.patterns[0].rules[0].context, "a");
1595 assert_eq!(schema.patterns[1].id.as_deref(), Some("p2"));
1596 assert_eq!(schema.patterns[1].rules[0].context, "b");
1597 }
1598
1599 #[test]
1600 fn abstract_pattern_never_appears_in_schema_patterns_even_if_unused() {
1601 let schema = parse(
1602 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1603 <pattern abstract="true" id="tpl">
1604 <rule context="$el">
1605 <assert test="true()">m</assert>
1606 </rule>
1607 </pattern>
1608 <pattern id="concrete">
1609 <rule context="/root">
1610 <assert test="foo">message</assert>
1611 </rule>
1612 </pattern>
1613 </schema>"#,
1614 )
1615 .unwrap();
1616
1617 assert_eq!(schema.patterns.len(), 1);
1618 assert_eq!(schema.patterns[0].id.as_deref(), Some("concrete"));
1619 }
1620
1621 #[test]
1626 fn let_value_is_not_substituted_in_an_instantiated_pattern() {
1627 let schema = parse(
1628 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1629 <pattern abstract="true" id="tpl">
1630 <rule context="$el">
1631 <let name="x" value="$el"/>
1632 <assert test="true()">m</assert>
1633 </rule>
1634 </pattern>
1635 <pattern is-a="tpl" id="concrete">
1636 <param name="el" value="item"/>
1637 </pattern>
1638 </schema>"#,
1639 )
1640 .unwrap();
1641
1642 let rule = &schema.patterns[0].rules[0];
1643 assert_eq!(rule.context, "item");
1644 assert_eq!(rule.lets[0].value, "$el");
1645 }
1646
1647 #[test]
1648 fn is_a_unknown_abstract_pattern_is_an_error() {
1649 let error = parse(
1650 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1651 <pattern is-a="missing" id="concrete">
1652 <param name="el" value="item"/>
1653 </pattern>
1654 </schema>"#,
1655 )
1656 .unwrap_err();
1657
1658 assert!(matches!(
1659 error,
1660 ParseError::UnknownAbstractPattern { pattern, .. } if pattern == "missing"
1661 ));
1662 }
1663}