1use std::collections::{HashMap, HashSet};
6use std::fmt;
7use std::ops::Range;
8
9use roxmltree::Node;
10
11use crate::resolver::SchemaResolver;
12use crate::schema::{
13 Check, CheckKind, Diagnostic, LetBinding, LetValue, MessagePart, NamespaceBinding, Pattern,
14 Phase, Property, Rule, Schema,
15};
16
17const SCHEMATRON_NAMESPACE: &str = "http://purl.oclc.org/dsdl/schematron";
18
19const XPATH1_QUERY_BINDINGS: &[&str] = &["xslt", "xpath"];
29
30const ID_TYPED_ELEMENTS: &[&str] = &[
39 "pattern",
40 "rule",
41 "assert",
42 "report",
43 "phase",
44 "diagnostic",
45 "property",
46];
47
48fn check_document_wide_id_uniqueness(root: Node<'_, '_>) -> Result<(), ParseError> {
64 let mut seen: HashSet<String> = HashSet::new();
65 for node in root.descendants() {
66 if !node.is_element() || node.tag_name().namespace() != Some(SCHEMATRON_NAMESPACE) {
67 continue;
68 }
69 let Some(&element) = ID_TYPED_ELEMENTS
70 .iter()
71 .find(|&&tag| node.has_tag_name((SCHEMATRON_NAMESPACE, tag)))
72 else {
73 continue;
74 };
75 if let Some(id) = node.attribute("id")
76 && !seen.insert(id.to_owned())
77 {
78 return Err(ParseError::DuplicateId {
79 element,
80 id: id.to_owned(),
81 position: position_of(node),
82 });
83 }
84 }
85 Ok(())
86}
87
88const SCHEMA_ALLOWED_CHILDREN: &[&str] = &[
97 "title",
98 "p",
99 "ns",
100 "let",
101 "phase",
102 "pattern",
103 "diagnostics",
104 "properties",
105];
106const PROPERTIES_ALLOWED_CHILDREN: &[&str] = &["property"];
107const PATTERN_ALLOWED_CHILDREN: &[&str] = &["title", "p", "let", "rule"];
108const IS_A_USE_ALLOWED_CHILDREN: &[&str] = &["p", "param"];
111const RULE_ALLOWED_CHILDREN: &[&str] = &["let", "assert", "report", "extends", "p"];
112const PHASE_ALLOWED_CHILDREN: &[&str] = &["title", "p", "let", "active"];
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub struct Position {
117 pub row: u32,
118 pub col: u32,
119}
120
121impl fmt::Display for Position {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 write!(formatter, "{}:{}", self.row, self.col)
124 }
125}
126
127#[derive(Clone, Debug, Eq, PartialEq)]
129pub enum ParseError {
130 InvalidXml(String),
132 WrongRootNamespace {
134 found: Option<String>,
135 position: Position,
136 },
137 NoPatterns,
139 MissingAttribute {
141 element: &'static str,
142 attribute: &'static str,
143 position: Position,
144 },
145 UnknownAbstractPattern { pattern: String, position: Position },
148 UnknownRuleReference { rule: String, position: Position },
153 CyclicRuleExtends { rule: String, position: Position },
156 UnexpectedElement { element: String, position: Position },
164 DuplicateId {
169 element: &'static str,
170 id: String,
171 position: Position,
172 },
173 UnknownActivePattern { pattern: String, position: Position },
179 UnknownDiagnosticReference { id: String, position: Position },
182 UnsupportedQueryBinding { binding: String, position: Position },
189 CyclicInclude { href: String, position: Position },
194 UnresolvableInclude {
199 href: String,
200 message: String,
201 position: Position,
202 },
203 InvalidInclude {
210 href: String,
211 reason: &'static str,
212 position: Position,
213 },
214}
215
216impl fmt::Display for ParseError {
217 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218 match self {
219 ParseError::InvalidXml(message) => {
220 write!(formatter, "invalid XML: {message}")
221 }
222 ParseError::WrongRootNamespace { found, position } => write!(
223 formatter,
224 "schema root must be in the Schematron namespace `{SCHEMATRON_NAMESPACE}`, found `{}` at {position}",
225 found.as_deref().unwrap_or("(none)")
226 ),
227 ParseError::NoPatterns => {
228 write!(formatter, "schema must contain at least one <pattern>")
229 }
230 ParseError::MissingAttribute {
231 element,
232 attribute,
233 position,
234 } => write!(
235 formatter,
236 "<{element}> is missing required attribute `{attribute}` at {position}"
237 ),
238 ParseError::UnknownAbstractPattern { pattern, position } => write!(
239 formatter,
240 "is-a references unknown abstract pattern `{pattern}` at {position}"
241 ),
242 ParseError::UnknownRuleReference { rule, position } => write!(
243 formatter,
244 "extends references unknown rule `{rule}` (not found in the same pattern) at {position}"
245 ),
246 ParseError::CyclicRuleExtends { rule, position } => write!(
247 formatter,
248 "cyclic extends involving rule `{rule}` at {position}"
249 ),
250 ParseError::UnexpectedElement { element, position } => write!(
251 formatter,
252 "unexpected <{element}> at {position} (unknown, or not supported here — see README.md)"
253 ),
254 ParseError::DuplicateId {
255 element,
256 id,
257 position,
258 } => write!(
259 formatter,
260 "duplicate {element} id `{id}` at {position} (already declared earlier in the schema)"
261 ),
262 ParseError::UnknownActivePattern { pattern, position } => write!(
263 formatter,
264 "active references unknown pattern `{pattern}` at {position}"
265 ),
266 ParseError::UnknownDiagnosticReference { id, position } => write!(
267 formatter,
268 "diagnostics references unknown diagnostic `{id}` at {position}"
269 ),
270 ParseError::UnsupportedQueryBinding { binding, position } => write!(
271 formatter,
272 "schema/@queryBinding `{binding}` at {position} is not equivalent to XPath 1.0 (only `xslt`/`xpath`, or an absent queryBinding, are accepted — see README.md)"
273 ),
274 ParseError::CyclicInclude { href, position } => {
275 write!(formatter, "cyclic include involving `{href}` at {position}")
276 }
277 ParseError::UnresolvableInclude {
278 href,
279 message,
280 position,
281 } => write!(
282 formatter,
283 "could not resolve <include href=\"{href}\"> at {position}: {message}"
284 ),
285 ParseError::InvalidInclude {
286 href,
287 reason,
288 position,
289 } => write!(
290 formatter,
291 "<include href=\"{href}\"> at {position} is invalid: {reason}"
292 ),
293 }
294 }
295}
296
297impl std::error::Error for ParseError {}
298
299fn parse_xml_document(xml: &str) -> Result<roxmltree::Document<'_>, ParseError> {
304 roxmltree::Document::parse(xml).map_err(|error| ParseError::InvalidXml(error.to_string()))
305}
306
307fn is_schematron_element(node: Node<'_, '_>) -> bool {
312 node.tag_name().namespace() == Some(SCHEMATRON_NAMESPACE)
313}
314
315pub fn parse(xml: &str) -> Result<Schema, ParseError> {
317 let document = parse_xml_document(xml)?;
318 let root = document.root_element();
319
320 if !is_schematron_element(root) {
321 return Err(ParseError::WrongRootNamespace {
322 found: root.tag_name().namespace().map(str::to_owned),
323 position: position_of(root),
324 });
325 }
326
327 validate_known_children(root, SCHEMA_ALLOWED_CHILDREN)?;
328
329 check_document_wide_id_uniqueness(root)?;
330
331 let ns = collect_children(root, "ns", parse_ns)?;
332
333 let lets = collect_lets(root)?;
334
335 let default_phase = root.attribute("defaultPhase").map(str::to_owned);
336
337 let query_binding = root.attribute("queryBinding").map(str::to_owned);
338 if let Some(binding) = &query_binding
339 && !XPATH1_QUERY_BINDINGS
340 .iter()
341 .any(|known| known.eq_ignore_ascii_case(binding))
342 {
343 return Err(ParseError::UnsupportedQueryBinding {
344 binding: binding.clone(),
345 position: position_of(root),
346 });
347 }
348
349 let diagnostics = root
353 .children()
354 .find(|node| node.has_tag_name((SCHEMATRON_NAMESPACE, "diagnostics")))
355 .map(parse_diagnostics_element)
356 .transpose()?
357 .unwrap_or_default();
358 let diagnostic_ids: HashSet<String> = diagnostics.iter().map(|d| d.id.clone()).collect();
359
360 let properties = root
361 .children()
362 .find(|node| node.has_tag_name((SCHEMATRON_NAMESPACE, "properties")))
363 .map(parse_properties_element)
364 .transpose()?
365 .unwrap_or_default();
366
367 let abstract_patterns: HashMap<String, Node<'_, '_>> = root
371 .children()
372 .filter(|node| {
373 node.has_tag_name((SCHEMATRON_NAMESPACE, "pattern"))
374 && node.attribute("abstract") == Some("true")
375 })
376 .map(|node| {
377 node.attribute("id")
378 .map(|id| (id.to_owned(), node))
379 .ok_or_else(|| ParseError::MissingAttribute {
380 element: "pattern",
381 attribute: "id",
382 position: position_of(node),
383 })
384 })
385 .collect::<Result<_, _>>()?;
386
387 let mut pattern_ids: HashSet<String> = HashSet::new();
393
394 let patterns: Vec<Pattern> = root
395 .children()
396 .filter(|node| {
397 node.has_tag_name((SCHEMATRON_NAMESPACE, "pattern"))
398 && node.attribute("abstract") != Some("true")
399 })
400 .map(|node| {
401 let id = node.attribute("id").map(str::to_owned);
402 if let Some(id) = &id {
403 pattern_ids.insert(id.clone());
404 }
405
406 if let Some(is_a) = node.attribute("is-a") {
407 validate_known_children(node, IS_A_USE_ALLOWED_CHILDREN)?;
408 let abstract_node = abstract_patterns.get(is_a).copied().ok_or_else(|| {
409 ParseError::UnknownAbstractPattern {
410 pattern: is_a.to_owned(),
411 position: position_of(node),
412 }
413 })?;
414 let params = collect_children(node, "param", parse_param)?;
415 parse_pattern(abstract_node, id, ¶ms, &diagnostic_ids)
416 } else {
417 parse_pattern(node, id, &[], &diagnostic_ids)
418 }
419 })
420 .collect::<Result<_, _>>()?;
421
422 if patterns.is_empty() {
423 return Err(ParseError::NoPatterns);
424 }
425
426 let phases = collect_children(root, "phase", |node| parse_phase(node, &pattern_ids))?;
427
428 Ok(Schema {
429 ns,
430 lets,
431 default_phase,
432 phases,
433 patterns,
434 diagnostics,
435 properties,
436 query_binding,
437 })
438}
439
440pub fn parse_with_resolver(
474 xml: &str,
475 base_uri: &str,
476 resolver: &impl SchemaResolver,
477) -> Result<Schema, ParseError> {
478 let mut loading = HashSet::new();
479 let expanded = resolve_includes(xml, base_uri, resolver, &mut loading)?;
480 parse(&expanded)
481}
482
483fn resolve_includes(
494 xml: &str,
495 base_uri: &str,
496 resolver: &impl SchemaResolver,
497 loading: &mut HashSet<String>,
498) -> Result<String, ParseError> {
499 let document = parse_xml_document(xml)?;
500
501 let includes: Vec<(Range<usize>, String, Position)> = document
502 .root_element()
503 .descendants()
504 .filter(|node| node.has_tag_name((SCHEMATRON_NAMESPACE, "include")))
505 .map(|node| {
506 let href = required_attribute(node, "include", "href")?.to_owned();
507 Ok((node.range(), href, position_of(node)))
508 })
509 .collect::<Result<_, ParseError>>()?;
510
511 if includes.is_empty() {
512 return Ok(xml.to_owned());
513 }
514
515 let mut result = String::with_capacity(xml.len());
516 let mut cursor = 0usize;
517 for (range, href, position) in includes {
518 result.push_str(&xml[cursor..range.start]);
519
520 let key = format!("{base_uri}::{href}");
521 if !loading.insert(key.clone()) {
522 return Err(ParseError::CyclicInclude { href, position });
523 }
524 let resolved_text = resolver
525 .resolve(&href, base_uri)
526 .map_err(|error| ParseError::UnresolvableInclude {
527 href: href.clone(),
528 message: error.to_string(),
529 position,
530 })
531 .and_then(|source| {
532 resolve_includes(source.text(), source.base_uri(), resolver, loading)
533 });
534 loading.remove(&key);
535 let resolved_text = resolved_text?;
536
537 let included_document = parse_xml_document(&resolved_text)?;
538 let included_root = included_document.root_element();
539 if !is_schematron_element(included_root) {
540 return Err(ParseError::InvalidInclude {
541 href,
542 reason: "included root element is not in the Schematron namespace",
543 position,
544 });
545 }
546 if included_root.has_tag_name((SCHEMATRON_NAMESPACE, "schema")) {
547 return Err(ParseError::InvalidInclude {
548 href,
549 reason: "included content must be a fragment, not a whole <schema> (ISO reference: \"use include to include fragments, not a whole schema\")",
550 position,
551 });
552 }
553 result.push_str(&resolved_text[included_root.range()]);
554
555 cursor = range.end;
556 }
557 result.push_str(&xml[cursor..]);
558 Ok(result)
559}
560
561fn parse_diagnostics_element(node: Node<'_, '_>) -> Result<Vec<Diagnostic>, ParseError> {
566 validate_known_children(node, &["diagnostic"])?;
567 collect_children(node, "diagnostic", parse_diagnostic)
568}
569
570fn parse_diagnostic(node: Node<'_, '_>) -> Result<Diagnostic, ParseError> {
575 let id = required_attribute(node, "diagnostic", "id")?.to_owned();
576 let (role, message) = parse_role_and_message(node, &[])?;
577 Ok(Diagnostic { id, role, message })
578}
579
580fn parse_properties_element(node: Node<'_, '_>) -> Result<Vec<Property>, ParseError> {
586 validate_known_children(node, PROPERTIES_ALLOWED_CHILDREN)?;
587 collect_children(node, "property", parse_property)
588}
589
590fn parse_property(node: Node<'_, '_>) -> Result<Property, ParseError> {
594 let id = required_attribute(node, "property", "id")?.to_owned();
595 let scheme = node.attribute("scheme").map(str::to_owned);
596 let (role, message) = parse_role_and_message(node, &[])?;
597 Ok(Property {
598 id,
599 role,
600 scheme,
601 message,
602 })
603}
604
605fn parse_role_and_message(
610 node: Node<'_, '_>,
611 params: &[(String, String)],
612) -> Result<(Option<String>, Vec<MessagePart>), ParseError> {
613 let role = node.attribute("role").map(str::to_owned);
614 let mut message = Vec::new();
615 collect_message_parts(node, &mut message, params)?;
616 Ok((role, message))
617}
618
619fn parse_ns(node: Node<'_, '_>) -> Result<NamespaceBinding, ParseError> {
624 let prefix = required_attribute(node, "ns", "prefix")?.to_owned();
625 let uri = required_attribute(node, "ns", "uri")?.to_owned();
626 Ok(NamespaceBinding { prefix, uri })
627}
628
629fn parse_let(node: Node<'_, '_>) -> Result<LetBinding, ParseError> {
643 let name = required_attribute(node, "let", "name")?.to_owned();
644 let value = if let Some(value) = node.attribute("value") {
645 LetValue::Expr(value.to_owned())
646 } else if node.children().any(|child| child.is_element()) {
647 LetValue::Literal(collect_literal_text(node))
648 } else {
649 return Err(ParseError::MissingAttribute {
650 element: "let",
651 attribute: "value",
652 position: position_of(node),
653 });
654 };
655 Ok(LetBinding { name, value })
656}
657
658fn collect_literal_text(node: Node<'_, '_>) -> String {
665 let mut text = String::new();
666 for child in node.children() {
667 if child.is_text() {
668 if let Some(t) = child.text() {
669 text.push_str(t);
670 }
671 } else if child.is_element() {
672 text.push_str(&collect_literal_text(child));
673 }
674 }
675 text
676}
677
678fn parse_phase(node: Node<'_, '_>, pattern_ids: &HashSet<String>) -> Result<Phase, ParseError> {
685 validate_known_children(node, PHASE_ALLOWED_CHILDREN)?;
686 let id = required_attribute(node, "phase", "id")?.to_owned();
687 let lets = collect_lets(node)?;
688 let active = collect_children(node, "active", |child| parse_active(child, pattern_ids))?;
689 Ok(Phase { id, lets, active })
690}
691
692fn parse_active(node: Node<'_, '_>, pattern_ids: &HashSet<String>) -> Result<String, ParseError> {
693 let pattern = required_attribute(node, "active", "pattern")?;
694 if !pattern_ids.contains(pattern) {
695 return Err(ParseError::UnknownActivePattern {
696 pattern: pattern.to_owned(),
697 position: position_of(node),
698 });
699 }
700 Ok(pattern.to_owned())
701}
702
703fn parse_param(node: Node<'_, '_>) -> Result<(String, String), ParseError> {
706 let name = required_attribute(node, "param", "name")?.to_owned();
707 let value = required_attribute(node, "param", "value")?.to_owned();
708 Ok((name, value))
709}
710
711fn substitute(text: &str, params: &[(String, String)]) -> String {
718 params.iter().fold(text.to_owned(), |acc, (name, value)| {
719 acc.replace(&format!("${name}"), value)
720 })
721}
722
723fn parse_pattern(
731 node: Node<'_, '_>,
732 id: Option<String>,
733 params: &[(String, String)],
734 diagnostic_ids: &HashSet<String>,
735) -> Result<Pattern, ParseError> {
736 validate_known_children(node, PATTERN_ALLOWED_CHILDREN)?;
737
738 let lets = collect_lets(node)?;
739
740 let rule_index: HashMap<String, Node<'_, '_>> = node
741 .children()
742 .filter(|child| child.has_tag_name((SCHEMATRON_NAMESPACE, "rule")))
743 .filter_map(|child| child.attribute("id").map(|id| (id.to_owned(), child)))
744 .collect();
745
746 let rules = node
747 .children()
748 .filter(|child| {
749 child.has_tag_name((SCHEMATRON_NAMESPACE, "rule"))
750 && child.attribute("abstract") != Some("true")
751 })
752 .map(|child| parse_rule(child, &rule_index, params, diagnostic_ids))
753 .collect::<Result<_, _>>()?;
754
755 Ok(Pattern { id, lets, rules })
756}
757
758fn parse_rule(
762 node: Node<'_, '_>,
763 rule_index: &HashMap<String, Node<'_, '_>>,
764 params: &[(String, String)],
765 diagnostic_ids: &HashSet<String>,
766) -> Result<Rule, ParseError> {
767 validate_known_children(node, RULE_ALLOWED_CHILDREN)?;
768
769 let context = substitute(required_attribute(node, "rule", "context")?, params);
770
771 let mut visiting: Vec<String> = node
774 .attribute("id")
775 .map(str::to_owned)
776 .into_iter()
777 .collect();
778
779 let mut checks = Vec::new();
780 let mut lets = Vec::new();
781 collect_checks(
782 node,
783 rule_index,
784 params,
785 &mut visiting,
786 &mut checks,
787 &mut lets,
788 diagnostic_ids,
789 )?;
790
791 Ok(Rule {
792 context,
793 lets,
794 checks,
795 })
796}
797
798fn collect_checks(
817 node: Node<'_, '_>,
818 rule_index: &HashMap<String, Node<'_, '_>>,
819 params: &[(String, String)],
820 visiting: &mut Vec<String>,
821 checks: &mut Vec<Check>,
822 lets: &mut Vec<LetBinding>,
823 diagnostic_ids: &HashSet<String>,
824) -> Result<(), ParseError> {
825 for child in node.children() {
826 if let Some(kind) = check_kind(child) {
827 checks.push(parse_check(kind, child, params, diagnostic_ids)?);
828 } else if child.has_tag_name((SCHEMATRON_NAMESPACE, "let")) {
829 lets.push(parse_let(child)?);
830 } else if child.has_tag_name((SCHEMATRON_NAMESPACE, "extends")) {
831 let target_id =
832 child
833 .attribute("rule")
834 .ok_or_else(|| ParseError::MissingAttribute {
835 element: "extends",
836 attribute: "rule",
837 position: position_of(child),
838 })?;
839 let target = rule_index.get(target_id).copied().ok_or_else(|| {
840 ParseError::UnknownRuleReference {
841 rule: target_id.to_owned(),
842 position: position_of(child),
843 }
844 })?;
845 if visiting.iter().any(|id| id == target_id) {
846 return Err(ParseError::CyclicRuleExtends {
847 rule: target_id.to_owned(),
848 position: position_of(child),
849 });
850 }
851 visiting.push(target_id.to_owned());
852 collect_checks(
853 target,
854 rule_index,
855 params,
856 visiting,
857 checks,
858 lets,
859 diagnostic_ids,
860 )?;
861 visiting.pop();
862 }
863 }
864 Ok(())
865}
866
867fn check_kind(node: Node<'_, '_>) -> Option<CheckKind> {
868 if node.has_tag_name((SCHEMATRON_NAMESPACE, "assert")) {
869 Some(CheckKind::Assert)
870 } else if node.has_tag_name((SCHEMATRON_NAMESPACE, "report")) {
871 Some(CheckKind::Report)
872 } else {
873 None
874 }
875}
876
877fn parse_check(
878 kind: CheckKind,
879 node: Node<'_, '_>,
880 params: &[(String, String)],
881 diagnostic_ids: &HashSet<String>,
882) -> Result<Check, ParseError> {
883 let element = match kind {
884 CheckKind::Assert => "assert",
885 CheckKind::Report => "report",
886 };
887 let test = substitute(required_attribute(node, element, "test")?, params);
888 let id = node.attribute("id").map(str::to_owned);
889 let (role, message) = parse_role_and_message(node, params)?;
890 let diagnostics: Vec<String> = node
891 .attribute("diagnostics")
892 .map(|ids| ids.split_whitespace().map(str::to_owned).collect())
893 .unwrap_or_default();
894 for diagnostic_id in &diagnostics {
895 if !diagnostic_ids.contains(diagnostic_id) {
896 return Err(ParseError::UnknownDiagnosticReference {
897 id: diagnostic_id.clone(),
898 position: position_of(node),
899 });
900 }
901 }
902
903 Ok(Check {
904 kind,
905 test,
906 id,
907 role,
908 message,
909 diagnostics,
910 })
911}
912
913fn collect_message_parts(
926 node: Node<'_, '_>,
927 parts: &mut Vec<MessagePart>,
928 params: &[(String, String)],
929) -> Result<(), ParseError> {
930 for child in node.children() {
931 if child.is_text() {
932 if let Some(text) = child.text() {
933 parts.push(MessagePart::Text(text.to_owned()));
934 }
935 } else if child.has_tag_name((SCHEMATRON_NAMESPACE, "value-of")) {
936 let select = required_attribute(child, "value-of", "select")?;
937 parts.push(MessagePart::ValueOf(substitute(select, params)));
938 } else if child.has_tag_name((SCHEMATRON_NAMESPACE, "name")) {
939 let path = child.attribute("path").map(|path| substitute(path, params));
940 parts.push(MessagePart::Name(path));
941 } else if child.is_element() {
942 collect_message_parts(child, parts, params)?;
943 }
944 }
945 Ok(())
946}
947
948fn required_attribute<'a>(
952 node: Node<'a, '_>,
953 element: &'static str,
954 attribute: &'static str,
955) -> Result<&'a str, ParseError> {
956 node.attribute(attribute)
957 .ok_or_else(|| ParseError::MissingAttribute {
958 element,
959 attribute,
960 position: position_of(node),
961 })
962}
963
964fn collect_lets(node: Node<'_, '_>) -> Result<Vec<LetBinding>, ParseError> {
968 collect_children(node, "let", parse_let)
969}
970
971fn collect_children<T>(
975 node: Node<'_, '_>,
976 tag: &str,
977 f: impl FnMut(Node<'_, '_>) -> Result<T, ParseError>,
978) -> Result<Vec<T>, ParseError> {
979 node.children()
980 .filter(|child| child.has_tag_name((SCHEMATRON_NAMESPACE, tag)))
981 .map(f)
982 .collect()
983}
984
985fn validate_known_children(node: Node<'_, '_>, allowed: &[&str]) -> Result<(), ParseError> {
992 for child in node.children() {
993 if child.is_element()
994 && child.tag_name().namespace() == Some(SCHEMATRON_NAMESPACE)
995 && !allowed.contains(&child.tag_name().name())
996 {
997 return Err(ParseError::UnexpectedElement {
998 element: child.tag_name().name().to_owned(),
999 position: position_of(child),
1000 });
1001 }
1002 }
1003 Ok(())
1004}
1005
1006fn position_of(node: Node<'_, '_>) -> Position {
1007 let text_pos = node.document().text_pos_at(node.range().start);
1008 Position {
1009 row: text_pos.row,
1010 col: text_pos.col,
1011 }
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017
1018 #[test]
1019 fn minimal_valid_schema() {
1020 let schema = parse(
1021 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1022 <pattern>
1023 <rule context="/root">
1024 <assert test="foo">message</assert>
1025 </rule>
1026 </pattern>
1027 </schema>"#,
1028 )
1029 .unwrap();
1030
1031 assert_eq!(schema.ns, vec![]);
1032 assert_eq!(schema.patterns.len(), 1);
1033 let pattern = &schema.patterns[0];
1034 assert_eq!(pattern.id, None);
1035 assert_eq!(pattern.rules.len(), 1);
1036 let rule = &pattern.rules[0];
1037 assert_eq!(rule.context, "/root");
1038 assert_eq!(rule.checks.len(), 1);
1039 let check = &rule.checks[0];
1040 assert_eq!(check.kind, CheckKind::Assert);
1041 assert_eq!(check.test, "foo");
1042 assert_eq!(check.message, vec![MessagePart::Text("message".to_owned())]);
1043 }
1044
1045 #[test]
1046 fn multiple_patterns_rules_and_checks() {
1047 let schema = parse(
1048 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1049 <pattern id="p1">
1050 <rule context="/a">
1051 <assert test="t1">m1</assert>
1052 <assert test="t2">m2</assert>
1053 </rule>
1054 <rule context="/b">
1055 <assert test="t3">m3</assert>
1056 </rule>
1057 </pattern>
1058 <pattern id="p2">
1059 <rule context="/c">
1060 <assert test="t4">m4</assert>
1061 </rule>
1062 </pattern>
1063 </schema>"#,
1064 )
1065 .unwrap();
1066
1067 assert_eq!(schema.patterns.len(), 2);
1068 assert_eq!(schema.patterns[0].id.as_deref(), Some("p1"));
1069 assert_eq!(schema.patterns[0].rules.len(), 2);
1070 assert_eq!(schema.patterns[0].rules[0].checks.len(), 2);
1071 assert_eq!(schema.patterns[0].rules[1].checks.len(), 1);
1072 assert_eq!(schema.patterns[1].id.as_deref(), Some("p2"));
1073 assert_eq!(schema.patterns[1].rules.len(), 1);
1074 }
1075
1076 #[test]
1077 fn report_alongside_assert() {
1078 let schema = parse(
1079 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1080 <pattern>
1081 <rule context="/root">
1082 <assert test="a-test">assert message</assert>
1083 <report test="r-test">report message</report>
1084 </rule>
1085 </pattern>
1086 </schema>"#,
1087 )
1088 .unwrap();
1089
1090 let checks = &schema.patterns[0].rules[0].checks;
1091 assert_eq!(checks.len(), 2);
1092 assert_eq!(checks[0].kind, CheckKind::Assert);
1093 assert_eq!(checks[0].test, "a-test");
1094 assert_eq!(checks[1].kind, CheckKind::Report);
1095 assert_eq!(checks[1].test, "r-test");
1096 }
1097
1098 #[test]
1099 fn ns_bindings_are_captured() {
1100 let schema = parse(
1101 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1102 <ns prefix="ex" uri="http://example.com/ns"/>
1103 <ns prefix="other" uri="http://example.com/other"/>
1104 <pattern>
1105 <rule context="/root">
1106 <assert test="foo">message</assert>
1107 </rule>
1108 </pattern>
1109 </schema>"#,
1110 )
1111 .unwrap();
1112
1113 assert_eq!(
1114 schema.ns,
1115 vec![
1116 NamespaceBinding {
1117 prefix: "ex".to_owned(),
1118 uri: "http://example.com/ns".to_owned(),
1119 },
1120 NamespaceBinding {
1121 prefix: "other".to_owned(),
1122 uri: "http://example.com/other".to_owned(),
1123 },
1124 ]
1125 );
1126 }
1127
1128 #[test]
1129 fn ns_missing_prefix_is_an_error() {
1130 let error = parse(
1131 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1132 <ns uri="http://example.com/ns"/>
1133 <pattern>
1134 <rule context="/root">
1135 <assert test="foo">message</assert>
1136 </rule>
1137 </pattern>
1138 </schema>"#,
1139 )
1140 .unwrap_err();
1141
1142 assert_eq!(
1143 error,
1144 ParseError::MissingAttribute {
1145 element: "ns",
1146 attribute: "prefix",
1147 position: Position { row: 2, col: 17 },
1148 }
1149 );
1150 }
1151
1152 #[test]
1153 fn ns_missing_uri_is_an_error() {
1154 let error = parse(
1155 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1156 <ns prefix="ex"/>
1157 <pattern>
1158 <rule context="/root">
1159 <assert test="foo">message</assert>
1160 </rule>
1161 </pattern>
1162 </schema>"#,
1163 )
1164 .unwrap_err();
1165
1166 assert_eq!(
1167 error,
1168 ParseError::MissingAttribute {
1169 element: "ns",
1170 attribute: "uri",
1171 position: Position { row: 2, col: 17 },
1172 }
1173 );
1174 }
1175
1176 #[test]
1177 fn id_and_role_present() {
1178 let schema = parse(
1179 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1180 <pattern>
1181 <rule context="/root">
1182 <assert test="foo" id="my-id" role="warning">message</assert>
1183 </rule>
1184 </pattern>
1185 </schema>"#,
1186 )
1187 .unwrap();
1188
1189 let check = &schema.patterns[0].rules[0].checks[0];
1190 assert_eq!(check.id.as_deref(), Some("my-id"));
1191 assert_eq!(check.role.as_deref(), Some("warning"));
1192 }
1193
1194 #[test]
1195 fn id_and_role_absent() {
1196 let schema = parse(
1197 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1198 <pattern>
1199 <rule context="/root">
1200 <assert test="foo">message</assert>
1201 </rule>
1202 </pattern>
1203 </schema>"#,
1204 )
1205 .unwrap();
1206
1207 let check = &schema.patterns[0].rules[0].checks[0];
1208 assert_eq!(check.id, None);
1209 assert_eq!(check.role, None);
1210 }
1211
1212 #[test]
1216 fn message_text_is_captured_untrimmed() {
1217 let schema = parse(
1218 "<schema xmlns=\"http://purl.oclc.org/dsdl/schematron\">\
1219 <pattern>\
1220 <rule context=\"/root\">\
1221 <assert test=\"foo\">\n first second \n</assert>\
1222 </rule>\
1223 </pattern>\
1224 </schema>",
1225 )
1226 .unwrap();
1227
1228 let check = &schema.patterns[0].rules[0].checks[0];
1229 assert_eq!(
1230 check.message,
1231 vec![MessagePart::Text("\n first second \n".to_owned())]
1232 );
1233 }
1234
1235 #[test]
1236 fn message_text_without_surrounding_whitespace_is_unchanged() {
1237 let schema = parse(
1238 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1239 <pattern>
1240 <rule context="/root">
1241 <assert test="foo">exact</assert>
1242 </rule>
1243 </pattern>
1244 </schema>"#,
1245 )
1246 .unwrap();
1247
1248 let check = &schema.patterns[0].rules[0].checks[0];
1249 assert_eq!(check.message, vec![MessagePart::Text("exact".to_owned())]);
1250 }
1251
1252 #[test]
1253 fn value_of_and_wrapper_element_text_are_captured() {
1254 let schema = parse(
1255 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1256 <pattern>
1257 <rule context="/root">
1258 <assert test="foo">before <value-of select="@x"/> after <emph>wrapped</emph></assert>
1259 </rule>
1260 </pattern>
1261 </schema>"#,
1262 )
1263 .unwrap();
1264
1265 let check = &schema.patterns[0].rules[0].checks[0];
1266 assert_eq!(
1267 check.message,
1268 vec![
1269 MessagePart::Text("before ".to_owned()),
1270 MessagePart::ValueOf("@x".to_owned()),
1271 MessagePart::Text(" after ".to_owned()),
1272 MessagePart::Text("wrapped".to_owned()),
1273 ]
1274 );
1275 }
1276
1277 #[test]
1281 fn name_element_is_captured_as_a_message_part() {
1282 let schema = parse(
1283 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1284 <pattern>
1285 <rule context="/root">
1286 <assert test="foo">Element <name/> or <name path="../other"/>.</assert>
1287 </rule>
1288 </pattern>
1289 </schema>"#,
1290 )
1291 .unwrap();
1292
1293 let check = &schema.patterns[0].rules[0].checks[0];
1294 assert_eq!(
1295 check.message,
1296 vec![
1297 MessagePart::Text("Element ".to_owned()),
1298 MessagePart::Name(None),
1299 MessagePart::Text(" or ".to_owned()),
1300 MessagePart::Name(Some("../other".to_owned())),
1301 MessagePart::Text(".".to_owned()),
1302 ]
1303 );
1304 }
1305
1306 #[test]
1307 fn value_of_missing_select_is_an_error() {
1308 let error = parse(
1309 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1310 <pattern>
1311 <rule context="/root">
1312 <assert test="foo"><value-of/></assert>
1313 </rule>
1314 </pattern>
1315 </schema>"#,
1316 )
1317 .unwrap_err();
1318
1319 assert_eq!(
1320 error,
1321 ParseError::MissingAttribute {
1322 element: "value-of",
1323 attribute: "select",
1324 position: Position { row: 4, col: 44 },
1325 }
1326 );
1327 }
1328
1329 #[test]
1330 fn missing_context_is_an_error() {
1331 let error = parse(
1332 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1333 <pattern>
1334 <rule>
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: "rule",
1346 attribute: "context",
1347 position: Position { row: 3, col: 21 },
1348 }
1349 );
1350 }
1351
1352 #[test]
1353 fn missing_test_is_an_error() {
1354 let error = parse(
1355 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1356 <pattern>
1357 <rule context="/root">
1358 <assert>message</assert>
1359 </rule>
1360 </pattern>
1361 </schema>"#,
1362 )
1363 .unwrap_err();
1364
1365 assert_eq!(
1366 error,
1367 ParseError::MissingAttribute {
1368 element: "assert",
1369 attribute: "test",
1370 position: Position { row: 4, col: 25 },
1371 }
1372 );
1373 }
1374
1375 #[test]
1376 fn no_pattern_is_an_error() {
1377 let error = parse(r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron"/>"#).unwrap_err();
1378
1379 assert_eq!(error, ParseError::NoPatterns);
1380 }
1381
1382 #[test]
1383 fn wrong_root_namespace_is_an_error() {
1384 let error = parse(r#"<schema xmlns="http://example.com/not-schematron"/>"#).unwrap_err();
1385
1386 assert_eq!(
1387 error,
1388 ParseError::WrongRootNamespace {
1389 found: Some("http://example.com/not-schematron".to_owned()),
1390 position: Position { row: 1, col: 1 },
1391 }
1392 );
1393 }
1394
1395 #[test]
1396 fn diagnostics_are_captured() {
1397 let schema = parse(
1398 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1399 <diagnostics>
1400 <diagnostic id="d1" role="warning">before <value-of select="."/> after</diagnostic>
1401 <diagnostic id="d2">second</diagnostic>
1402 </diagnostics>
1403 <pattern>
1404 <rule context="/root">
1405 <assert test="foo" diagnostics="d1 d2">message</assert>
1406 </rule>
1407 </pattern>
1408 </schema>"#,
1409 )
1410 .unwrap();
1411
1412 assert_eq!(schema.diagnostics.len(), 2);
1413 assert_eq!(schema.diagnostics[0].id, "d1");
1414 assert_eq!(schema.diagnostics[0].role.as_deref(), Some("warning"));
1415 assert_eq!(
1416 schema.diagnostics[0].message,
1417 vec![
1418 MessagePart::Text("before ".to_owned()),
1419 MessagePart::ValueOf(".".to_owned()),
1420 MessagePart::Text(" after".to_owned()),
1421 ]
1422 );
1423
1424 let check = &schema.patterns[0].rules[0].checks[0];
1425 assert_eq!(check.diagnostics, vec!["d1".to_owned(), "d2".to_owned()]);
1426 }
1427
1428 #[test]
1434 fn diagnostics_unknown_reference_is_an_error() {
1435 let error = parse(
1436 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1437 <diagnostics>
1438 <diagnostic id="d1">known</diagnostic>
1439 </diagnostics>
1440 <pattern>
1441 <rule context="/root">
1442 <assert test="foo" diagnostics="d1 missing">message</assert>
1443 </rule>
1444 </pattern>
1445 </schema>"#,
1446 )
1447 .unwrap_err();
1448
1449 assert!(matches!(
1450 error,
1451 ParseError::UnknownDiagnosticReference { id, .. } if id == "missing"
1452 ));
1453 }
1454
1455 #[test]
1456 fn duplicate_diagnostic_id_is_an_error() {
1457 let error = parse(
1458 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1459 <diagnostics>
1460 <diagnostic id="d1">first</diagnostic>
1461 <diagnostic id="d1">second</diagnostic>
1462 </diagnostics>
1463 <pattern>
1464 <rule context="/root">
1465 <assert test="foo" diagnostics="d1">message</assert>
1466 </rule>
1467 </pattern>
1468 </schema>"#,
1469 )
1470 .unwrap_err();
1471
1472 assert!(matches!(
1473 error,
1474 ParseError::DuplicateId { element: "diagnostic", id, .. } if id == "d1"
1475 ));
1476 }
1477
1478 #[test]
1479 fn check_without_diagnostics_attribute_has_an_empty_list() {
1480 let schema = parse(
1481 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1482 <pattern>
1483 <rule context="/root">
1484 <assert test="foo">message</assert>
1485 </rule>
1486 </pattern>
1487 </schema>"#,
1488 )
1489 .unwrap();
1490
1491 assert_eq!(
1492 schema.patterns[0].rules[0].checks[0].diagnostics,
1493 Vec::<String>::new()
1494 );
1495 assert_eq!(schema.diagnostics, vec![]);
1496 }
1497
1498 #[test]
1499 fn diagnostic_missing_id_is_an_error() {
1500 let error = parse(
1501 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1502 <diagnostics>
1503 <diagnostic>text</diagnostic>
1504 </diagnostics>
1505 <pattern>
1506 <rule context="/root">
1507 <assert test="foo">message</assert>
1508 </rule>
1509 </pattern>
1510 </schema>"#,
1511 )
1512 .unwrap_err();
1513
1514 assert_eq!(
1515 error,
1516 ParseError::MissingAttribute {
1517 element: "diagnostic",
1518 attribute: "id",
1519 position: Position { row: 3, col: 21 },
1520 }
1521 );
1522 }
1523
1524 #[test]
1527 fn properties_are_captured() {
1528 let schema = parse(
1529 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1530 <properties>
1531 <property id="p1" role="severity" scheme="acme">before <value-of select="."/> after</property>
1532 <property id="p2">second</property>
1533 </properties>
1534 <pattern>
1535 <rule context="/root">
1536 <assert test="foo">message</assert>
1537 </rule>
1538 </pattern>
1539 </schema>"#,
1540 )
1541 .unwrap();
1542
1543 assert_eq!(schema.properties.len(), 2);
1544 assert_eq!(schema.properties[0].id, "p1");
1545 assert_eq!(schema.properties[0].role.as_deref(), Some("severity"));
1546 assert_eq!(schema.properties[0].scheme.as_deref(), Some("acme"));
1547 assert_eq!(
1548 schema.properties[0].message,
1549 vec![
1550 MessagePart::Text("before ".to_owned()),
1551 MessagePart::ValueOf(".".to_owned()),
1552 MessagePart::Text(" after".to_owned()),
1553 ]
1554 );
1555 assert_eq!(schema.properties[1].id, "p2");
1556 assert_eq!(schema.properties[1].role, None);
1557 assert_eq!(schema.properties[1].scheme, None);
1558 }
1559
1560 #[test]
1561 fn schema_without_properties_has_an_empty_properties_list() {
1562 let schema = parse(
1563 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1564 <pattern>
1565 <rule context="/root">
1566 <assert test="foo">message</assert>
1567 </rule>
1568 </pattern>
1569 </schema>"#,
1570 )
1571 .unwrap();
1572
1573 assert_eq!(schema.properties, vec![]);
1574 }
1575
1576 #[test]
1577 fn duplicate_property_id_is_an_error() {
1578 let error = parse(
1579 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1580 <properties>
1581 <property id="p1">first</property>
1582 <property id="p1">second</property>
1583 </properties>
1584 <pattern>
1585 <rule context="/root">
1586 <assert test="foo">message</assert>
1587 </rule>
1588 </pattern>
1589 </schema>"#,
1590 )
1591 .unwrap_err();
1592
1593 assert!(matches!(
1594 error,
1595 ParseError::DuplicateId { element: "property", id, .. } if id == "p1"
1596 ));
1597 }
1598
1599 #[test]
1600 fn property_missing_id_is_an_error() {
1601 let error = parse(
1602 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1603 <properties>
1604 <property>text</property>
1605 </properties>
1606 <pattern>
1607 <rule context="/root">
1608 <assert test="foo">message</assert>
1609 </rule>
1610 </pattern>
1611 </schema>"#,
1612 )
1613 .unwrap_err();
1614
1615 assert_eq!(
1616 error,
1617 ParseError::MissingAttribute {
1618 element: "property",
1619 attribute: "id",
1620 position: Position { row: 3, col: 21 },
1621 }
1622 );
1623 }
1624
1625 #[test]
1626 fn unexpected_element_inside_properties_is_an_error() {
1627 let error = parse(
1628 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1629 <properties>
1630 <bogus/>
1631 </properties>
1632 <pattern>
1633 <rule context="/root">
1634 <assert test="foo">message</assert>
1635 </rule>
1636 </pattern>
1637 </schema>"#,
1638 )
1639 .unwrap_err();
1640
1641 assert!(matches!(
1642 error,
1643 ParseError::UnexpectedElement { element, .. } if element == "bogus"
1644 ));
1645 }
1646
1647 #[test]
1650 fn query_binding_xslt_and_xpath_are_accepted_case_insensitively() {
1651 for binding in ["xslt", "XSLT", "xpath", "XPath"] {
1652 let schema = parse(&format!(
1653 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="{binding}">
1654 <pattern>
1655 <rule context="/root">
1656 <assert test="foo">message</assert>
1657 </rule>
1658 </pattern>
1659 </schema>"#,
1660 ))
1661 .unwrap();
1662
1663 assert_eq!(schema.query_binding.as_deref(), Some(binding));
1664 }
1665 }
1666
1667 #[test]
1668 fn schema_without_query_binding_has_none() {
1669 let schema = parse(
1670 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1671 <pattern>
1672 <rule context="/root">
1673 <assert test="foo">message</assert>
1674 </rule>
1675 </pattern>
1676 </schema>"#,
1677 )
1678 .unwrap();
1679
1680 assert_eq!(schema.query_binding, None);
1681 }
1682
1683 #[test]
1684 fn unsupported_query_binding_is_an_error() {
1685 let error = parse(
1686 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
1687 <pattern>
1688 <rule context="/root">
1689 <assert test="foo">message</assert>
1690 </rule>
1691 </pattern>
1692 </schema>"#,
1693 )
1694 .unwrap_err();
1695
1696 assert!(matches!(
1697 error,
1698 ParseError::UnsupportedQueryBinding { binding, .. } if binding == "xslt2"
1699 ));
1700 }
1701
1702 #[test]
1703 fn let_bindings_are_captured_at_schema_pattern_and_rule_level() {
1704 let schema = parse(
1705 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1706 <let name="a" value="1"/>
1707 <pattern>
1708 <let name="b" value="2"/>
1709 <rule context="/root">
1710 <let name="c" value="3"/>
1711 <assert test="foo">message</assert>
1712 </rule>
1713 </pattern>
1714 </schema>"#,
1715 )
1716 .unwrap();
1717
1718 assert_eq!(
1719 schema.lets,
1720 vec![LetBinding {
1721 name: "a".to_owned(),
1722 value: LetValue::Expr("1".to_owned()),
1723 }]
1724 );
1725 assert_eq!(
1726 schema.patterns[0].lets,
1727 vec![LetBinding {
1728 name: "b".to_owned(),
1729 value: LetValue::Expr("2".to_owned()),
1730 }]
1731 );
1732 assert_eq!(
1733 schema.patterns[0].rules[0].lets,
1734 vec![LetBinding {
1735 name: "c".to_owned(),
1736 value: LetValue::Expr("3".to_owned()),
1737 }]
1738 );
1739 }
1740
1741 #[test]
1742 fn let_missing_value_is_an_error() {
1743 let error = parse(
1744 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1745 <let name="a"/>
1746 <pattern>
1747 <rule context="/root">
1748 <assert test="foo">message</assert>
1749 </rule>
1750 </pattern>
1751 </schema>"#,
1752 )
1753 .unwrap_err();
1754
1755 assert_eq!(
1756 error,
1757 ParseError::MissingAttribute {
1758 element: "let",
1759 attribute: "value",
1760 position: Position { row: 2, col: 17 },
1761 }
1762 );
1763 }
1764
1765 #[test]
1770 fn let_with_literal_xml_content_is_captured_as_its_string_value() {
1771 let schema = parse(
1772 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1773 <let name="x"><foo>hello <bar>world</bar></foo></let>
1774 <pattern>
1775 <rule context="/root">
1776 <assert test="foo">message</assert>
1777 </rule>
1778 </pattern>
1779 </schema>"#,
1780 )
1781 .unwrap();
1782
1783 assert_eq!(
1784 schema.lets,
1785 vec![LetBinding {
1786 name: "x".to_owned(),
1787 value: LetValue::Literal("hello world".to_owned()),
1788 }]
1789 );
1790 }
1791
1792 #[test]
1798 fn let_with_only_whitespace_text_is_still_a_missing_value_error() {
1799 let error = parse(
1800 "<schema xmlns=\"http://purl.oclc.org/dsdl/schematron\">\
1801 <let name=\"a\"> </let>\
1802 <pattern>\
1803 <rule context=\"/root\">\
1804 <assert test=\"foo\">message</assert>\
1805 </rule>\
1806 </pattern>\
1807 </schema>",
1808 )
1809 .unwrap_err();
1810
1811 assert!(matches!(
1812 error,
1813 ParseError::MissingAttribute {
1814 element: "let",
1815 attribute: "value",
1816 ..
1817 }
1818 ));
1819 }
1820
1821 #[test]
1822 fn default_phase_and_phases_are_captured() {
1823 let schema = parse(
1824 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="p1">
1825 <phase id="p1">
1826 <let name="x" value="1"/>
1827 <active pattern="a"/>
1828 <active pattern="b"/>
1829 </phase>
1830 <phase id="p2">
1831 <active pattern="b"/>
1832 </phase>
1833 <pattern id="a">
1834 <rule context="/root">
1835 <assert test="foo">message</assert>
1836 </rule>
1837 </pattern>
1838 <pattern id="b">
1839 <rule context="/root">
1840 <assert test="foo">message</assert>
1841 </rule>
1842 </pattern>
1843 </schema>"#,
1844 )
1845 .unwrap();
1846
1847 assert_eq!(schema.default_phase.as_deref(), Some("p1"));
1848 assert_eq!(schema.phases.len(), 2);
1849 assert_eq!(schema.phases[0].id, "p1");
1850 assert_eq!(
1851 schema.phases[0].lets,
1852 vec![LetBinding {
1853 name: "x".to_owned(),
1854 value: LetValue::Expr("1".to_owned()),
1855 }]
1856 );
1857 assert_eq!(
1858 schema.phases[0].active,
1859 vec!["a".to_owned(), "b".to_owned()]
1860 );
1861 assert_eq!(schema.phases[1].id, "p2");
1862 assert_eq!(schema.phases[1].active, vec!["b".to_owned()]);
1863 }
1864
1865 #[test]
1866 fn schema_without_phase_has_no_default_phase_and_no_phases() {
1867 let schema = parse(
1868 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1869 <pattern>
1870 <rule context="/root">
1871 <assert test="foo">message</assert>
1872 </rule>
1873 </pattern>
1874 </schema>"#,
1875 )
1876 .unwrap();
1877
1878 assert_eq!(schema.default_phase, None);
1879 assert_eq!(schema.phases, vec![]);
1880 }
1881
1882 #[test]
1883 fn phase_missing_id_is_an_error() {
1884 let error = parse(
1885 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1886 <phase>
1887 <active pattern="a"/>
1888 </phase>
1889 <pattern id="a">
1890 <rule context="/root">
1891 <assert test="foo">message</assert>
1892 </rule>
1893 </pattern>
1894 </schema>"#,
1895 )
1896 .unwrap_err();
1897
1898 assert_eq!(
1899 error,
1900 ParseError::MissingAttribute {
1901 element: "phase",
1902 attribute: "id",
1903 position: Position { row: 2, col: 17 },
1904 }
1905 );
1906 }
1907
1908 #[test]
1909 fn active_missing_pattern_is_an_error() {
1910 let error = parse(
1911 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1912 <phase id="p1">
1913 <active/>
1914 </phase>
1915 <pattern id="a">
1916 <rule context="/root">
1917 <assert test="foo">message</assert>
1918 </rule>
1919 </pattern>
1920 </schema>"#,
1921 )
1922 .unwrap_err();
1923
1924 assert_eq!(
1925 error,
1926 ParseError::MissingAttribute {
1927 element: "active",
1928 attribute: "pattern",
1929 position: Position { row: 3, col: 21 },
1930 }
1931 );
1932 }
1933
1934 #[test]
1938 fn active_unknown_pattern_is_an_error() {
1939 let error = parse(
1940 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1941 <phase id="p1">
1942 <active pattern="missing"/>
1943 </phase>
1944 <pattern id="a">
1945 <rule context="/root">
1946 <assert test="foo">message</assert>
1947 </rule>
1948 </pattern>
1949 </schema>"#,
1950 )
1951 .unwrap_err();
1952
1953 assert!(matches!(
1954 error,
1955 ParseError::UnknownActivePattern { pattern, .. } if pattern == "missing"
1956 ));
1957 }
1958
1959 #[test]
1964 fn active_referencing_an_abstract_pattern_id_is_an_error() {
1965 let error = parse(
1966 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1967 <phase id="p1">
1968 <active pattern="tpl"/>
1969 </phase>
1970 <pattern abstract="true" id="tpl">
1971 <rule context="/root">
1972 <assert test="foo">message</assert>
1973 </rule>
1974 </pattern>
1975 <pattern id="a">
1976 <rule context="/root">
1977 <assert test="foo">message</assert>
1978 </rule>
1979 </pattern>
1980 </schema>"#,
1981 )
1982 .unwrap_err();
1983
1984 assert!(matches!(
1985 error,
1986 ParseError::UnknownActivePattern { pattern, .. } if pattern == "tpl"
1987 ));
1988 }
1989
1990 #[test]
1991 fn duplicate_pattern_id_is_an_error() {
1992 let error = parse(
1993 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1994 <pattern id="a">
1995 <rule context="/root">
1996 <assert test="foo">message</assert>
1997 </rule>
1998 </pattern>
1999 <pattern id="a">
2000 <rule context="/root">
2001 <assert test="foo">message</assert>
2002 </rule>
2003 </pattern>
2004 </schema>"#,
2005 )
2006 .unwrap_err();
2007
2008 assert!(matches!(
2009 error,
2010 ParseError::DuplicateId { element: "pattern", id, .. } if id == "a"
2011 ));
2012 }
2013
2014 #[test]
2020 fn cross_element_type_id_collision_is_an_error() {
2021 let error = parse(
2022 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2023 <phase id="shared">
2024 <active pattern="a"/>
2025 </phase>
2026 <pattern id="a">
2027 <rule id="shared" context="/root">
2028 <assert test="foo">message</assert>
2029 </rule>
2030 </pattern>
2031 </schema>"#,
2032 )
2033 .unwrap_err();
2034
2035 assert!(matches!(
2036 error,
2037 ParseError::DuplicateId { element: "rule", id, .. } if id == "shared"
2038 ));
2039 }
2040
2041 #[test]
2046 fn duplicate_abstract_pattern_id_is_an_error() {
2047 let error = parse(
2048 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2049 <pattern abstract="true" id="tpl">
2050 <rule context="/root">
2051 <assert test="foo">message</assert>
2052 </rule>
2053 </pattern>
2054 <pattern abstract="true" id="tpl">
2055 <rule context="/root">
2056 <assert test="foo">message</assert>
2057 </rule>
2058 </pattern>
2059 <pattern is-a="tpl" id="concrete">
2060 <param name="x" value="1"/>
2061 </pattern>
2062 </schema>"#,
2063 )
2064 .unwrap_err();
2065
2066 assert!(matches!(
2067 error,
2068 ParseError::DuplicateId { element: "pattern", id, .. } if id == "tpl"
2069 ));
2070 }
2071
2072 #[test]
2073 fn unexpected_element_at_schema_level_is_an_error() {
2074 let error = parse(
2075 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2076 <include href="other.sch"/>
2077 <pattern>
2078 <rule context="/root">
2079 <assert test="foo">message</assert>
2080 </rule>
2081 </pattern>
2082 </schema>"#,
2083 )
2084 .unwrap_err();
2085
2086 assert!(matches!(
2087 error,
2088 ParseError::UnexpectedElement { element, .. } if element == "include"
2089 ));
2090 }
2091
2092 #[test]
2093 fn unexpected_element_inside_rule_is_an_error() {
2094 let error = parse(
2095 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2096 <pattern>
2097 <rule context="/root">
2098 <asert test="foo">typo'd element name</asert>
2099 </rule>
2100 </pattern>
2101 </schema>"#,
2102 )
2103 .unwrap_err();
2104
2105 assert!(matches!(
2106 error,
2107 ParseError::UnexpectedElement { element, .. } if element == "asert"
2108 ));
2109 }
2110
2111 #[test]
2115 fn title_and_p_are_accepted_but_ignored() {
2116 let schema = parse(
2117 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2118 <title>My Schema</title>
2119 <p>Some documentation.</p>
2120 <pattern>
2121 <rule context="/root">
2122 <assert test="foo">message</assert>
2123 </rule>
2124 </pattern>
2125 </schema>"#,
2126 )
2127 .unwrap();
2128
2129 assert_eq!(schema.patterns.len(), 1);
2130 }
2131
2132 #[test]
2133 fn extends_inlines_checks_from_another_rule_in_the_same_pattern() {
2134 let schema = parse(
2135 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2136 <pattern>
2137 <rule abstract="true" id="base">
2138 <assert test="false()">BASE-CHECK</assert>
2139 </rule>
2140 <rule context="/root">
2141 <extends rule="base"/>
2142 <assert test="false()">OWN-CHECK</assert>
2143 </rule>
2144 </pattern>
2145 </schema>"#,
2146 )
2147 .unwrap();
2148
2149 assert_eq!(schema.patterns[0].rules.len(), 1);
2151 let checks = &schema.patterns[0].rules[0].checks;
2152 assert_eq!(checks.len(), 2);
2153 assert_eq!(
2154 checks[0].message,
2155 vec![MessagePart::Text("BASE-CHECK".to_owned())]
2156 );
2157 assert_eq!(
2158 checks[1].message,
2159 vec![MessagePart::Text("OWN-CHECK".to_owned())]
2160 );
2161 }
2162
2163 #[test]
2164 fn extends_is_transitive() {
2165 let schema = parse(
2166 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2167 <pattern>
2168 <rule abstract="true" id="a">
2169 <assert test="false()">A</assert>
2170 </rule>
2171 <rule abstract="true" id="b">
2172 <extends rule="a"/>
2173 <assert test="false()">B</assert>
2174 </rule>
2175 <rule context="/root">
2176 <extends rule="b"/>
2177 </rule>
2178 </pattern>
2179 </schema>"#,
2180 )
2181 .unwrap();
2182
2183 let checks = &schema.patterns[0].rules[0].checks;
2184 assert_eq!(
2185 checks.iter().map(|c| c.message.clone()).collect::<Vec<_>>(),
2186 vec![
2187 vec![MessagePart::Text("A".to_owned())],
2188 vec![MessagePart::Text("B".to_owned())],
2189 ]
2190 );
2191 }
2192
2193 #[test]
2197 fn extends_inlines_lets_from_another_rule_in_the_same_pattern() {
2198 let schema = parse(
2199 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2200 <pattern>
2201 <rule abstract="true" id="base">
2202 <let name="x" value="'from-base'"/>
2203 </rule>
2204 <rule context="/root">
2205 <extends rule="base"/>
2206 <let name="y" value="'own'"/>
2207 <assert test="false()">CHECK</assert>
2208 </rule>
2209 </pattern>
2210 </schema>"#,
2211 )
2212 .unwrap();
2213
2214 let lets = &schema.patterns[0].rules[0].lets;
2215 assert_eq!(
2216 lets,
2217 &vec![
2218 LetBinding {
2219 name: "x".to_owned(),
2220 value: LetValue::Expr("'from-base'".to_owned()),
2221 },
2222 LetBinding {
2223 name: "y".to_owned(),
2224 value: LetValue::Expr("'own'".to_owned()),
2225 },
2226 ]
2227 );
2228 }
2229
2230 #[test]
2234 fn extends_inlines_lets_transitively() {
2235 let schema = parse(
2236 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2237 <pattern>
2238 <rule abstract="true" id="a">
2239 <let name="x" value="'A'"/>
2240 </rule>
2241 <rule abstract="true" id="b">
2242 <extends rule="a"/>
2243 <let name="y" value="'B'"/>
2244 </rule>
2245 <rule context="/root">
2246 <extends rule="b"/>
2247 </rule>
2248 </pattern>
2249 </schema>"#,
2250 )
2251 .unwrap();
2252
2253 let lets = &schema.patterns[0].rules[0].lets;
2254 assert_eq!(
2255 lets.iter().map(|l| l.name.as_str()).collect::<Vec<_>>(),
2256 vec!["x", "y"]
2257 );
2258 }
2259
2260 #[test]
2265 fn own_let_shadows_an_earlier_inherited_let_of_the_same_name() {
2266 let schema = parse(
2267 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2268 <pattern>
2269 <rule abstract="true" id="base">
2270 <let name="x" value="'from-base'"/>
2271 </rule>
2272 <rule context="/root">
2273 <extends rule="base"/>
2274 <let name="x" value="'own'"/>
2275 <assert test="false()">CHECK</assert>
2276 </rule>
2277 </pattern>
2278 </schema>"#,
2279 )
2280 .unwrap();
2281
2282 let lets = &schema.patterns[0].rules[0].lets;
2283 assert_eq!(
2284 lets,
2285 &vec![
2286 LetBinding {
2287 name: "x".to_owned(),
2288 value: LetValue::Expr("'from-base'".to_owned()),
2289 },
2290 LetBinding {
2291 name: "x".to_owned(),
2292 value: LetValue::Expr("'own'".to_owned()),
2293 },
2294 ]
2295 );
2296 }
2297
2298 #[test]
2299 fn cyclic_extends_is_an_error() {
2300 let error = parse(
2301 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2302 <pattern>
2303 <rule abstract="true" id="a">
2304 <extends rule="b"/>
2305 </rule>
2306 <rule abstract="true" id="b">
2307 <extends rule="a"/>
2308 </rule>
2309 <rule context="/root">
2310 <extends rule="a"/>
2311 </rule>
2312 </pattern>
2313 </schema>"#,
2314 )
2315 .unwrap_err();
2316
2317 assert!(matches!(
2318 error,
2319 ParseError::CyclicRuleExtends { rule, .. } if rule == "a"
2320 ));
2321 }
2322
2323 #[test]
2324 fn extends_unknown_rule_is_an_error() {
2325 let error = parse(
2326 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2327 <pattern>
2328 <rule context="/root">
2329 <extends rule="missing"/>
2330 </rule>
2331 </pattern>
2332 </schema>"#,
2333 )
2334 .unwrap_err();
2335
2336 assert!(matches!(
2337 error,
2338 ParseError::UnknownRuleReference { rule, .. } if rule == "missing"
2339 ));
2340 }
2341
2342 #[test]
2343 fn is_a_instantiates_abstract_pattern_with_param_substitution() {
2344 let schema = parse(
2345 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2346 <pattern abstract="true" id="tpl">
2347 <rule context="$el">
2348 <assert test="$el = 1">msg</assert>
2349 </rule>
2350 </pattern>
2351 <pattern is-a="tpl" id="concrete">
2352 <param name="el" value="item"/>
2353 </pattern>
2354 </schema>"#,
2355 )
2356 .unwrap();
2357
2358 assert_eq!(schema.patterns.len(), 1);
2360 assert_eq!(schema.patterns[0].id.as_deref(), Some("concrete"));
2361 assert_eq!(schema.patterns[0].rules[0].context, "item");
2362 assert_eq!(schema.patterns[0].rules[0].checks[0].test, "item = 1");
2363 }
2364
2365 #[test]
2366 fn two_is_a_uses_of_the_same_abstract_pattern_are_independent() {
2367 let schema = parse(
2368 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2369 <pattern abstract="true" id="tpl">
2370 <rule context="$el">
2371 <assert test="true()">m</assert>
2372 </rule>
2373 </pattern>
2374 <pattern is-a="tpl" id="p1">
2375 <param name="el" value="a"/>
2376 </pattern>
2377 <pattern is-a="tpl" id="p2">
2378 <param name="el" value="b"/>
2379 </pattern>
2380 </schema>"#,
2381 )
2382 .unwrap();
2383
2384 assert_eq!(schema.patterns.len(), 2);
2385 assert_eq!(schema.patterns[0].id.as_deref(), Some("p1"));
2386 assert_eq!(schema.patterns[0].rules[0].context, "a");
2387 assert_eq!(schema.patterns[1].id.as_deref(), Some("p2"));
2388 assert_eq!(schema.patterns[1].rules[0].context, "b");
2389 }
2390
2391 #[test]
2392 fn abstract_pattern_never_appears_in_schema_patterns_even_if_unused() {
2393 let schema = parse(
2394 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2395 <pattern abstract="true" id="tpl">
2396 <rule context="$el">
2397 <assert test="true()">m</assert>
2398 </rule>
2399 </pattern>
2400 <pattern id="concrete">
2401 <rule context="/root">
2402 <assert test="foo">message</assert>
2403 </rule>
2404 </pattern>
2405 </schema>"#,
2406 )
2407 .unwrap();
2408
2409 assert_eq!(schema.patterns.len(), 1);
2410 assert_eq!(schema.patterns[0].id.as_deref(), Some("concrete"));
2411 }
2412
2413 #[test]
2418 fn let_value_is_not_substituted_in_an_instantiated_pattern() {
2419 let schema = parse(
2420 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2421 <pattern abstract="true" id="tpl">
2422 <rule context="$el">
2423 <let name="x" value="$el"/>
2424 <assert test="true()">m</assert>
2425 </rule>
2426 </pattern>
2427 <pattern is-a="tpl" id="concrete">
2428 <param name="el" value="item"/>
2429 </pattern>
2430 </schema>"#,
2431 )
2432 .unwrap();
2433
2434 let rule = &schema.patterns[0].rules[0];
2435 assert_eq!(rule.context, "item");
2436 assert_eq!(rule.lets[0].value, LetValue::Expr("$el".to_owned()));
2437 }
2438
2439 #[test]
2440 fn is_a_unknown_abstract_pattern_is_an_error() {
2441 let error = parse(
2442 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2443 <pattern is-a="missing" id="concrete">
2444 <param name="el" value="item"/>
2445 </pattern>
2446 </schema>"#,
2447 )
2448 .unwrap_err();
2449
2450 assert!(matches!(
2451 error,
2452 ParseError::UnknownAbstractPattern { pattern, .. } if pattern == "missing"
2453 ));
2454 }
2455
2456 use crate::resolver::{ResolveError, SchemaSource};
2459 use std::collections::BTreeMap;
2460
2461 struct MapResolver(BTreeMap<&'static str, &'static str>);
2464
2465 impl SchemaResolver for MapResolver {
2466 fn resolve(&self, href: &str, _base_uri: &str) -> Result<SchemaSource, ResolveError> {
2467 self.0
2468 .get(href)
2469 .map(|text| SchemaSource::new(*text, href))
2470 .ok_or_else(|| ResolveError::new(format!("no such resource: {href}")))
2471 }
2472 }
2473
2474 #[test]
2475 fn include_at_schema_level_splices_a_pattern_fragment() {
2476 let resolver = MapResolver(BTreeMap::from([(
2477 "pattern.sch",
2478 r#"<pattern xmlns="http://purl.oclc.org/dsdl/schematron" id="included">
2479 <rule context="/root">
2480 <assert test="foo">from included pattern</assert>
2481 </rule>
2482 </pattern>"#,
2483 )]));
2484
2485 let schema = parse_with_resolver(
2486 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2487 <include href="pattern.sch"/>
2488 </schema>"#,
2489 "root.sch",
2490 &resolver,
2491 )
2492 .unwrap();
2493
2494 assert_eq!(schema.patterns.len(), 1);
2495 assert_eq!(schema.patterns[0].id.as_deref(), Some("included"));
2496 assert_eq!(schema.patterns[0].rules[0].context, "/root");
2497 }
2498
2499 #[test]
2504 fn include_at_rule_level_splices_an_assert_fragment() {
2505 let resolver = MapResolver(BTreeMap::from([(
2506 "assert.sch",
2507 r#"<assert xmlns="http://purl.oclc.org/dsdl/schematron" test="foo">from included assert</assert>"#,
2508 )]));
2509
2510 let schema = parse_with_resolver(
2511 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2512 <pattern>
2513 <rule context="/root">
2514 <include href="assert.sch"/>
2515 </rule>
2516 </pattern>
2517 </schema>"#,
2518 "root.sch",
2519 &resolver,
2520 )
2521 .unwrap();
2522
2523 assert_eq!(
2524 schema.patterns[0].rules[0].checks[0].message,
2525 vec![MessagePart::Text("from included assert".to_owned())]
2526 );
2527 }
2528
2529 #[test]
2532 fn include_resolves_transitively() {
2533 let resolver = MapResolver(BTreeMap::from([
2534 (
2535 "outer.sch",
2536 r#"<pattern xmlns="http://purl.oclc.org/dsdl/schematron" id="outer">
2537 <include href="b.sch"/>
2538 </pattern>"#,
2539 ),
2540 (
2541 "b.sch",
2542 r#"<rule xmlns="http://purl.oclc.org/dsdl/schematron" context="/root">
2543 <assert test="foo">transitively included</assert>
2544 </rule>"#,
2545 ),
2546 ]));
2547
2548 let schema = parse_with_resolver(
2549 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2550 <include href="outer.sch"/>
2551 </schema>"#,
2552 "root.sch",
2553 &resolver,
2554 )
2555 .unwrap();
2556
2557 assert_eq!(schema.patterns[0].id.as_deref(), Some("outer"));
2558 assert_eq!(
2559 schema.patterns[0].rules[0].checks[0].message,
2560 vec![MessagePart::Text("transitively included".to_owned())]
2561 );
2562 }
2563
2564 #[test]
2565 fn cyclic_include_is_an_error() {
2566 let resolver = MapResolver(BTreeMap::from([(
2574 "a.sch",
2575 r#"<pattern xmlns="http://purl.oclc.org/dsdl/schematron" id="a">
2576 <include href="a.sch"/>
2577 </pattern>"#,
2578 )]));
2579
2580 let error = parse_with_resolver(
2581 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2582 <include href="a.sch"/>
2583 </schema>"#,
2584 "root.sch",
2585 &resolver,
2586 )
2587 .unwrap_err();
2588
2589 assert!(matches!(error, ParseError::CyclicInclude { href, .. } if href == "a.sch"));
2590 }
2591
2592 #[test]
2593 fn unresolvable_include_surfaces_the_resolver_error() {
2594 let resolver = MapResolver(BTreeMap::new());
2595
2596 let error = parse_with_resolver(
2597 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2598 <include href="missing.sch"/>
2599 </schema>"#,
2600 "root.sch",
2601 &resolver,
2602 )
2603 .unwrap_err();
2604
2605 assert!(matches!(
2606 error,
2607 ParseError::UnresolvableInclude { href, .. } if href == "missing.sch"
2608 ));
2609 }
2610
2611 #[test]
2612 fn including_a_whole_schema_is_an_error() {
2613 let resolver = MapResolver(BTreeMap::from([(
2614 "whole.sch",
2615 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2616 <pattern>
2617 <rule context="/root">
2618 <assert test="foo">m</assert>
2619 </rule>
2620 </pattern>
2621 </schema>"#,
2622 )]));
2623
2624 let error = parse_with_resolver(
2625 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2626 <include href="whole.sch"/>
2627 </schema>"#,
2628 "root.sch",
2629 &resolver,
2630 )
2631 .unwrap_err();
2632
2633 assert!(matches!(
2634 error,
2635 ParseError::InvalidInclude { href, .. } if href == "whole.sch"
2636 ));
2637 }
2638
2639 #[test]
2640 fn including_a_non_schematron_root_is_an_error() {
2641 let resolver = MapResolver(BTreeMap::from([(
2642 "other.xml",
2643 r#"<foo xmlns="http://example.com/other"/>"#,
2644 )]));
2645
2646 let error = parse_with_resolver(
2647 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2648 <include href="other.xml"/>
2649 </schema>"#,
2650 "root.sch",
2651 &resolver,
2652 )
2653 .unwrap_err();
2654
2655 assert!(matches!(
2656 error,
2657 ParseError::InvalidInclude { href, .. } if href == "other.xml"
2658 ));
2659 }
2660
2661 #[test]
2667 fn parse_with_resolver_missing_href_is_a_missing_attribute_error() {
2668 let resolver = MapResolver(BTreeMap::new());
2669
2670 let error = parse_with_resolver(
2671 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
2672 <include/>
2673 <pattern>
2674 <rule context="/root">
2675 <assert test="foo">m</assert>
2676 </rule>
2677 </pattern>
2678 </schema>"#,
2679 "root.sch",
2680 &resolver,
2681 )
2682 .unwrap_err();
2683
2684 assert_eq!(
2685 error,
2686 ParseError::MissingAttribute {
2687 element: "include",
2688 attribute: "href",
2689 position: Position { row: 2, col: 17 },
2690 }
2691 );
2692 }
2693}