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