1use std::collections::HashMap;
44
45use xpath_eval::{Document, EvaluationContext, Node, QName, Value};
46
47use crate::schema::{Check, CheckKind, Diagnostic, LetBinding, LetValue, MessagePart, Schema};
48
49#[derive(Clone, Debug)]
53pub struct Report<N> {
54 pub pattern_id: Option<String>,
55 pub check_id: Option<String>,
56 pub role: Option<String>,
57 pub kind: CheckKind,
58 pub message: String,
59 pub diagnostics: Vec<String>,
62 pub node: N,
63}
64
65#[derive(Debug, Clone, PartialEq)]
77pub enum SchematronEvalError {
78 Parse(xpath_eval::ParseError),
80 Eval(xpath_eval::EvalError),
82 ContextNotNodeSet { context: String },
87 UnknownPhase { phase: String },
91}
92
93impl std::fmt::Display for SchematronEvalError {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 SchematronEvalError::Parse(err) => write!(f, "XPath parse error: {err}"),
97 SchematronEvalError::Eval(err) => write!(f, "XPath evaluation error: {err}"),
98 SchematronEvalError::ContextNotNodeSet { context } => {
99 write!(f, "rule context {context:?} did not evaluate to a node-set")
100 }
101 SchematronEvalError::UnknownPhase { phase } => {
102 write!(f, "no <phase id={phase:?}> in this schema")
103 }
104 }
105 }
106}
107
108impl std::error::Error for SchematronEvalError {}
109
110fn split_top_level_union(context: &str) -> Vec<&str> {
116 let mut alternatives = Vec::new();
117 let mut depth: i32 = 0;
118 let mut quote: Option<char> = None;
119 let mut start = 0;
120
121 for (byte_offset, ch) in context.char_indices() {
122 match quote {
123 Some(q) => {
124 if ch == q {
125 quote = None;
126 }
127 }
128 None => match ch {
129 '\'' | '"' => quote = Some(ch),
130 '(' | '[' => depth += 1,
131 ')' | ']' => depth -= 1,
132 '|' if depth == 0 => {
133 alternatives.push(&context[start..byte_offset]);
134 start = byte_offset + ch.len_utf8();
135 }
136 _ => {}
137 },
138 }
139 }
140 alternatives.push(&context[start..]);
141 alternatives
142}
143
144pub fn evaluate<'a, D: Document>(
151 schema: &Schema,
152 document: &'a D,
153) -> Result<Vec<Report<D::N<'a>>>, SchematronEvalError> {
154 evaluate_with_phase(schema, document, schema.default_phase.as_deref())
155}
156
157pub fn evaluate_with_phase<'a, D: Document>(
163 schema: &Schema,
164 document: &'a D,
165 phase_id: Option<&str>,
166) -> Result<Vec<Report<D::N<'a>>>, SchematronEvalError> {
167 let ns_lookup = |prefix: &str| {
168 schema
169 .ns
170 .iter()
171 .find(|binding| binding.prefix == prefix)
172 .map(|binding| binding.uri.clone())
173 };
174
175 let phase = phase_id
176 .map(|id| {
177 schema
178 .phases
179 .iter()
180 .find(|phase| phase.id == id)
181 .ok_or_else(|| SchematronEvalError::UnknownPhase {
182 phase: id.to_owned(),
183 })
184 })
185 .transpose()?;
186
187 let mut reports = Vec::new();
188
189 let mut schema_vars: HashMap<String, Value<D::N<'a>>> = HashMap::new();
190 extend_lets(&mut schema_vars, &schema.lets, document.root(), &ns_lookup)?;
191
192 let mut phase_vars = schema_vars.clone();
193 if let Some(phase) = phase {
194 extend_lets(&mut phase_vars, &phase.lets, document.root(), &ns_lookup)?;
195 }
196
197 for pattern in &schema.patterns {
198 if let Some(phase) = phase {
199 let is_active = pattern
200 .id
201 .as_deref()
202 .is_some_and(|id| phase.active.iter().any(|active| active == id));
203 if !is_active {
204 continue;
205 }
206 }
207
208 let mut pattern_vars = phase_vars.clone();
209 extend_lets(
210 &mut pattern_vars,
211 &pattern.lets,
212 document.root(),
213 &ns_lookup,
214 )?;
215
216 let mut claimed: Vec<D::N<'a>> = Vec::new();
217
218 for rule in &pattern.rules {
219 let path = split_top_level_union(&rule.context)
220 .into_iter()
221 .map(|alternative| {
222 let alternative = alternative.trim();
223 if alternative == "/" {
233 "/".to_owned()
234 } else {
235 format!("descendant-or-self::node()/{alternative}")
236 }
237 })
238 .collect::<Vec<_>>()
239 .join(" | ");
240 let expr = xpath_eval::parse(&path).map_err(SchematronEvalError::Parse)?;
241
242 let mut ctx = EvaluationContext::new(document.root());
243 ctx.namespaces = Some(&ns_lookup);
244 let value = xpath_eval::evaluate(&expr, &ctx).map_err(SchematronEvalError::Eval)?;
245
246 let mut matches = match value {
247 Value::NodeSet(nodes) => nodes,
248 _ => {
249 return Err(SchematronEvalError::ContextNotNodeSet {
250 context: rule.context.clone(),
251 });
252 }
253 };
254 matches.sort_by(|a, b| a.document_order(*b));
255
256 for node in matches {
257 if claimed.contains(&node) {
258 continue;
259 }
260 claimed.push(node);
261
262 let mut node_vars = pattern_vars.clone();
263 extend_lets(&mut node_vars, &rule.lets, node, &ns_lookup)?;
264
265 for check in &rule.checks {
266 if let Some(report) = evaluate_check(
267 check,
268 node,
269 pattern.id.clone(),
270 &ns_lookup,
271 &node_vars,
272 &schema.diagnostics,
273 )? {
274 reports.push(report);
275 }
276 }
277 }
278 }
279 }
280
281 Ok(reports)
282}
283
284fn evaluate_xpath<'n, N: Node<'n>>(
291 raw: &str,
292 node: N,
293 ns_lookup: &dyn Fn(&str) -> Option<String>,
294 vars: &HashMap<String, Value<N>>,
295) -> Result<Value<N>, SchematronEvalError> {
296 let expr = xpath_eval::parse(raw).map_err(SchematronEvalError::Parse)?;
297 let lookup = |name: &QName| {
298 if name.prefix.is_some() {
299 None
300 } else {
301 vars.get(&name.local).cloned()
302 }
303 };
304 let mut ctx = EvaluationContext::new(node);
305 ctx.namespaces = Some(ns_lookup);
306 ctx.variables = Some(&lookup);
307 xpath_eval::evaluate(&expr, &ctx).map_err(SchematronEvalError::Eval)
308}
309
310fn extend_lets<'n, N: Node<'n>>(
318 vars: &mut HashMap<String, Value<N>>,
319 lets: &[LetBinding],
320 context_node: N,
321 ns_lookup: &dyn Fn(&str) -> Option<String>,
322) -> Result<(), SchematronEvalError> {
323 for binding in lets {
324 let value = match &binding.value {
325 LetValue::Expr(expr) => evaluate_xpath(expr, context_node, ns_lookup, vars)?,
326 LetValue::Literal(text) => Value::String(text.clone()),
329 };
330 vars.insert(binding.name.clone(), value);
331 }
332 Ok(())
333}
334
335fn evaluate_check<'n, N: Node<'n>>(
339 check: &Check,
340 node: N,
341 pattern_id: Option<String>,
342 ns_lookup: &dyn Fn(&str) -> Option<String>,
343 vars: &HashMap<String, Value<N>>,
344 diagnostics: &[Diagnostic],
345) -> Result<Option<Report<N>>, SchematronEvalError> {
346 let value = evaluate_xpath(&check.test, node, ns_lookup, vars)?;
347 let boolean = value.to_boolean();
348
349 let fires = match check.kind {
350 CheckKind::Assert => !boolean,
351 CheckKind::Report => boolean,
352 };
353
354 if !fires {
355 return Ok(None);
356 }
357
358 let message = render_message(&check.message, node, ns_lookup, vars)?;
359
360 let rendered_diagnostics = check
361 .diagnostics
362 .iter()
363 .filter_map(|id| diagnostics.iter().find(|d| &d.id == id))
364 .map(|d| render_message(&d.message, node, ns_lookup, vars))
365 .collect::<Result<Vec<_>, _>>()?;
366
367 Ok(Some(Report {
368 pattern_id,
369 check_id: check.id.clone(),
370 role: check.role.clone(),
371 kind: check.kind,
372 message,
373 diagnostics: rendered_diagnostics,
374 node,
375 }))
376}
377
378fn render_message<'n, N: Node<'n>>(
392 parts: &[MessagePart],
393 node: N,
394 ns_lookup: &dyn Fn(&str) -> Option<String>,
395 vars: &HashMap<String, Value<N>>,
396) -> Result<String, SchematronEvalError> {
397 let mut message = String::new();
398 for part in parts {
399 match part {
400 MessagePart::Text(text) => message.push_str(text),
401 MessagePart::ValueOf(select) => {
402 let value = evaluate_xpath(select, node, ns_lookup, vars)?;
403 message.push_str(&value.to_xpath_string());
404 }
405 MessagePart::Name(path) => {
406 let select = match path {
407 Some(path) => format!("name({path})"),
408 None => "name()".to_owned(),
409 };
410 let value = evaluate_xpath(&select, node, ns_lookup, vars)?;
411 message.push_str(&value.to_xpath_string());
412 }
413 }
414 }
415 Ok(message.trim().to_owned())
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use std::cmp::Ordering;
422 use xpath_eval::{ExpandedName, NodeKind};
423
424 #[derive(Debug)]
430 struct ElementData {
431 namespace_uri: Option<String>,
432 local_name: String,
433 parent: Option<usize>,
434 children: Vec<usize>,
435 }
436
437 #[derive(Debug)]
438 struct Arena {
439 elements: Vec<ElementData>,
440 }
441
442 struct TestDoc {
443 arena: Arena,
444 }
445
446 impl TestDoc {
447 fn node(&self, idx: usize) -> TestNode<'_> {
448 TestNode {
449 arena: &self.arena,
450 idx,
451 }
452 }
453 }
454
455 impl xpath_eval::Document for TestDoc {
456 type N<'a> = TestNode<'a>;
457
458 fn root(&self) -> Self::N<'_> {
459 self.node(0)
460 }
461 }
462
463 #[derive(Clone, Copy, Debug)]
464 struct TestNode<'a> {
465 arena: &'a Arena,
466 idx: usize,
467 }
468
469 impl<'a> PartialEq for TestNode<'a> {
470 fn eq(&self, other: &Self) -> bool {
471 std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
472 }
473 }
474
475 impl<'a> Eq for TestNode<'a> {}
476
477 impl<'a> Node<'a> for TestNode<'a> {
478 fn kind(self) -> NodeKind {
479 if self.idx == 0 {
480 NodeKind::Root
481 } else {
482 NodeKind::Element
483 }
484 }
485
486 fn parent(self) -> Option<Self> {
487 self.arena.elements[self.idx].parent.map(|p| TestNode {
488 arena: self.arena,
489 idx: p,
490 })
491 }
492
493 fn children(self) -> impl Iterator<Item = Self> + 'a {
494 let arena = self.arena;
495 self.arena.elements[self.idx]
496 .children
497 .clone()
498 .into_iter()
499 .map(move |i| TestNode { arena, idx: i })
500 }
501
502 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
503 std::iter::empty()
504 }
505
506 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
507 std::iter::empty()
508 }
509
510 fn expanded_name(self) -> Option<ExpandedName> {
511 if self.idx == 0 {
512 None
513 } else {
514 let element = &self.arena.elements[self.idx];
515 Some(ExpandedName {
516 namespace_uri: element.namespace_uri.clone(),
517 local_name: element.local_name.clone(),
518 })
519 }
520 }
521
522 fn string_value(self) -> String {
523 String::new()
524 }
525
526 fn document_order(self, other: Self) -> Ordering {
527 self.idx.cmp(&other.idx)
528 }
529 }
530
531 fn doc_with_root_children(children: &[(Option<&str>, &str)]) -> TestDoc {
534 let mut elements = vec![ElementData {
535 namespace_uri: None,
536 local_name: String::new(),
537 parent: None,
538 children: Vec::new(),
539 }];
540 for (namespace_uri, local_name) in children {
541 let idx = elements.len();
542 elements.push(ElementData {
543 namespace_uri: namespace_uri.map(str::to_owned),
544 local_name: (*local_name).to_owned(),
545 parent: Some(0),
546 children: Vec::new(),
547 });
548 elements[0].children.push(idx);
549 }
550 TestDoc {
551 arena: Arena { elements },
552 }
553 }
554
555 fn schema(sch_xml: &str) -> Schema {
556 crate::parse(sch_xml).expect("test schema must parse")
557 }
558
559 fn doc_with_nested_item() -> TestDoc {
568 let elements = vec![
569 ElementData {
570 namespace_uri: None,
571 local_name: String::new(),
572 parent: None,
573 children: vec![1],
574 },
575 ElementData {
576 namespace_uri: None,
577 local_name: "wrapper".to_owned(),
578 parent: Some(0),
579 children: vec![2],
580 },
581 ElementData {
582 namespace_uri: None,
583 local_name: "item".to_owned(),
584 parent: Some(1),
585 children: Vec::new(),
586 },
587 ];
588 TestDoc {
589 arena: Arena { elements },
590 }
591 }
592
593 #[test]
594 fn assert_true_produces_no_report() {
595 let doc = doc_with_root_children(&[(None, "item")]);
596 let schema = schema(
597 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
598 <pattern>
599 <rule context="item">
600 <assert test="true()">should not fire</assert>
601 </rule>
602 </pattern>
603 </schema>"#,
604 );
605
606 let reports = evaluate(&schema, &doc).unwrap();
607 assert_eq!(reports.len(), 0);
608 }
609
610 #[test]
611 fn assert_false_produces_one_report() {
612 let doc = doc_with_root_children(&[(None, "item")]);
613 let schema = schema(
614 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
615 <pattern id="p1">
616 <rule context="item">
617 <assert id="a1" role="error" test="false()">item is invalid</assert>
618 </rule>
619 </pattern>
620 </schema>"#,
621 );
622
623 let reports = evaluate(&schema, &doc).unwrap();
624 assert_eq!(reports.len(), 1);
625 let report = &reports[0];
626 assert_eq!(report.pattern_id.as_deref(), Some("p1"));
627 assert_eq!(report.check_id.as_deref(), Some("a1"));
628 assert_eq!(report.role.as_deref(), Some("error"));
629 assert_eq!(report.kind, CheckKind::Assert);
630 assert_eq!(report.message, "item is invalid");
631 assert_eq!(report.node, doc.node(1));
632 }
633
634 #[test]
650 fn context_union_matches_through_every_alternative() {
651 let doc = doc_with_nested_item();
652
653 let first_alternative = schema(
654 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
655 <pattern>
656 <rule context="item | does-not-exist">
657 <report test="true()">observed item</report>
658 </rule>
659 </pattern>
660 </schema>"#,
661 );
662 let reports = evaluate(&first_alternative, &doc).unwrap();
663 assert_eq!(reports.len(), 1, "item as the first union alternative");
664
665 let second_alternative = schema(
666 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
667 <pattern>
668 <rule context="does-not-exist | item">
669 <report test="true()">observed item</report>
670 </rule>
671 </pattern>
672 </schema>"#,
673 );
674 let reports = evaluate(&second_alternative, &doc).unwrap();
675 assert_eq!(reports.len(), 1, "item as the second union alternative");
676 }
677
678 #[test]
679 fn split_top_level_union_ignores_nested_pipes() {
680 assert_eq!(split_top_level_union("item"), vec!["item"]);
681 assert_eq!(split_top_level_union("a | b | c"), vec!["a ", " b ", " c"]);
682 assert_eq!(
685 split_top_level_union("a[@x = 'p|q']"),
686 vec!["a[@x = 'p|q']"]
687 );
688 assert_eq!(split_top_level_union("a[b | c]"), vec!["a[b | c]"]);
689 assert_eq!(split_top_level_union("(a | b)/c"), vec!["(a | b)/c"]);
690 assert_eq!(
691 split_top_level_union("a[@x = 'p|q'] | b"),
692 vec!["a[@x = 'p|q'] ", " b"]
693 );
694 }
695
696 #[test]
697 fn report_true_fires() {
698 let doc = doc_with_root_children(&[(None, "item")]);
699 let schema = schema(
700 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
701 <pattern>
702 <rule context="item">
703 <report test="true()">observed item</report>
704 </rule>
705 </pattern>
706 </schema>"#,
707 );
708
709 let reports = evaluate(&schema, &doc).unwrap();
710 assert_eq!(reports.len(), 1);
711 assert_eq!(reports[0].kind, CheckKind::Report);
712 assert_eq!(reports[0].message, "observed item");
713 }
714
715 #[test]
721 fn same_pattern_first_matching_rule_wins() {
722 let doc = doc_with_root_children(&[(None, "item")]);
723 let schema = schema(
724 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
725 <pattern>
726 <rule context="item">
727 <assert test="false()">RULE-ONE-FIRED</assert>
728 </rule>
729 <rule context="item">
730 <assert test="false()">RULE-TWO-FIRED</assert>
731 </rule>
732 </pattern>
733 </schema>"#,
734 );
735
736 let reports = evaluate(&schema, &doc).unwrap();
737 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
738 assert_eq!(messages, vec!["RULE-ONE-FIRED"]);
739 }
740
741 #[test]
745 fn independent_patterns_both_fire() {
746 let doc = doc_with_root_children(&[(None, "item")]);
747 let schema = schema(
748 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
749 <pattern>
750 <rule context="item">
751 <assert test="false()">PATTERN-ONE-FIRED</assert>
752 </rule>
753 </pattern>
754 <pattern>
755 <rule context="item">
756 <assert test="false()">PATTERN-TWO-FIRED</assert>
757 </rule>
758 </pattern>
759 </schema>"#,
760 );
761
762 let reports = evaluate(&schema, &doc).unwrap();
763 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
764 assert_eq!(messages, vec!["PATTERN-ONE-FIRED", "PATTERN-TWO-FIRED"]);
765 }
766
767 #[test]
774 fn namespace_binding_resolves_prefixed_context() {
775 let doc = doc_with_root_children(&[(Some("urn:test"), "item")]);
776 let schema = schema(
777 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
778 <ns prefix="t" uri="urn:test"/>
779 <pattern>
780 <rule context="t:item">
781 <assert test="false()">NS-MATCH</assert>
782 </rule>
783 </pattern>
784 </schema>"#,
785 );
786
787 let reports = evaluate(&schema, &doc).unwrap();
788 assert_eq!(reports.len(), 1);
789 assert_eq!(reports[0].message, "NS-MATCH");
790 }
791
792 #[test]
793 fn invalid_test_expression_is_a_parse_error_not_a_panic() {
794 let doc = doc_with_root_children(&[(None, "item")]);
795 let schema = schema(
796 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
797 <pattern>
798 <rule context="item">
799 <assert test="((">broken</assert>
800 </rule>
801 </pattern>
802 </schema>"#,
803 );
804
805 let result = evaluate(&schema, &doc);
806 assert!(matches!(result, Err(SchematronEvalError::Parse(_))));
807 }
808
809 #[test]
810 fn schema_level_let_is_visible_to_checks() {
811 let doc = doc_with_root_children(&[(None, "item")]);
812 let schema = schema(
813 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
814 <let name="x" value="'schema'"/>
815 <pattern>
816 <rule context="item">
817 <report test="$x = 'schema'">SCHEMA-LET</report>
818 </rule>
819 </pattern>
820 </schema>"#,
821 );
822
823 let reports = evaluate(&schema, &doc).unwrap();
824 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
825 assert_eq!(messages, vec!["SCHEMA-LET"]);
826 }
827
828 #[test]
833 fn pattern_level_let_shadows_schema_level_let() {
834 let doc = doc_with_root_children(&[(None, "item")]);
835 let schema = schema(
836 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
837 <let name="x" value="'schema'"/>
838 <pattern>
839 <let name="x" value="'pattern'"/>
840 <rule context="item">
841 <report test="$x = 'pattern'">PATTERN-WINS</report>
842 </rule>
843 </pattern>
844 </schema>"#,
845 );
846
847 let reports = evaluate(&schema, &doc).unwrap();
848 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
849 assert_eq!(messages, vec!["PATTERN-WINS"]);
850 }
851
852 #[test]
856 fn rule_level_let_is_reevaluated_per_context_node() {
857 let doc = doc_with_root_children(&[(None, "alpha"), (None, "beta")]);
858 let schema = schema(
859 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
860 <pattern>
861 <rule context="*">
862 <let name="n" value="local-name()"/>
863 <assert test="$n = 'alpha'">NOT-ALPHA</assert>
864 </rule>
865 </pattern>
866 </schema>"#,
867 );
868
869 let reports = evaluate(&schema, &doc).unwrap();
870 assert_eq!(reports.len(), 1);
871 assert_eq!(reports[0].message, "NOT-ALPHA");
872 assert_eq!(reports[0].node, doc.node(2));
873 }
874
875 #[test]
878 fn let_can_reference_earlier_let_in_same_scope() {
879 let doc = doc_with_root_children(&[(None, "item")]);
880 let schema = schema(
881 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
882 <let name="a" value="'A'"/>
883 <let name="b" value="concat($a, 'B')"/>
884 <pattern>
885 <rule context="item">
886 <report test="$b = 'AB'">SEQUENTIAL</report>
887 </rule>
888 </pattern>
889 </schema>"#,
890 );
891
892 let reports = evaluate(&schema, &doc).unwrap();
893 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
894 assert_eq!(messages, vec!["SEQUENTIAL"]);
895 }
896
897 #[test]
901 fn literal_xml_let_is_usable_as_a_string_in_a_test_expression() {
902 let doc = doc_with_root_children(&[(None, "item")]);
903 let schema = schema(
904 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
905 <let name="x"><foo>literal</foo></let>
906 <pattern>
907 <rule context="item">
908 <report test="$x = 'literal'">LITERAL-LET</report>
909 </rule>
910 </pattern>
911 </schema>"#,
912 );
913
914 let reports = evaluate(&schema, &doc).unwrap();
915 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
916 assert_eq!(messages, vec!["LITERAL-LET"]);
917 }
918
919 #[test]
920 fn unbound_variable_is_an_eval_error_not_a_panic() {
921 let doc = doc_with_root_children(&[(None, "item")]);
922 let schema = schema(
923 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
924 <pattern>
925 <rule context="item">
926 <assert test="$missing">broken</assert>
927 </rule>
928 </pattern>
929 </schema>"#,
930 );
931
932 let result = evaluate(&schema, &doc);
933 assert!(matches!(result, Err(SchematronEvalError::Eval(_))));
934 }
935
936 #[test]
939 fn evaluate_with_phase_restricts_to_activated_patterns() {
940 let doc = doc_with_root_children(&[(None, "item")]);
941 let schema = schema(
942 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
943 <phase id="p1">
944 <active pattern="pa"/>
945 </phase>
946 <phase id="p2">
947 <active pattern="pb"/>
948 </phase>
949 <pattern id="pa">
950 <rule context="item">
951 <assert test="false()">PATTERN-A</assert>
952 </rule>
953 </pattern>
954 <pattern id="pb">
955 <rule context="item">
956 <assert test="false()">PATTERN-B</assert>
957 </rule>
958 </pattern>
959 </schema>"#,
960 );
961
962 let reports_p1 = evaluate_with_phase(&schema, &doc, Some("p1")).unwrap();
963 let messages_p1: Vec<&str> = reports_p1.iter().map(|r| r.message.as_str()).collect();
964 assert_eq!(messages_p1, vec!["PATTERN-A"]);
965
966 let reports_p2 = evaluate_with_phase(&schema, &doc, Some("p2")).unwrap();
967 let messages_p2: Vec<&str> = reports_p2.iter().map(|r| r.message.as_str()).collect();
968 assert_eq!(messages_p2, vec!["PATTERN-B"]);
969
970 let reports_all = evaluate_with_phase(&schema, &doc, None).unwrap();
971 let messages_all: Vec<&str> = reports_all.iter().map(|r| r.message.as_str()).collect();
972 assert_eq!(messages_all, vec!["PATTERN-A", "PATTERN-B"]);
973 }
974
975 #[test]
981 fn evaluate_honors_default_phase() {
982 let doc = doc_with_root_children(&[(None, "item")]);
983 let schema = schema(
984 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="p1">
985 <phase id="p1">
986 <active pattern="pa"/>
987 </phase>
988 <pattern id="pa">
989 <rule context="item">
990 <assert test="false()">PATTERN-A</assert>
991 </rule>
992 </pattern>
993 <pattern id="pb">
994 <rule context="item">
995 <assert test="false()">PATTERN-B</assert>
996 </rule>
997 </pattern>
998 </schema>"#,
999 );
1000
1001 let reports = evaluate(&schema, &doc).unwrap();
1002 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
1003 assert_eq!(messages, vec!["PATTERN-A"]);
1004 }
1005
1006 #[test]
1007 fn evaluate_with_phase_unknown_id_is_an_error() {
1008 let doc = doc_with_root_children(&[(None, "item")]);
1009 let schema = schema(
1010 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1011 <pattern id="pa">
1012 <rule context="item">
1013 <assert test="false()">PATTERN-A</assert>
1014 </rule>
1015 </pattern>
1016 </schema>"#,
1017 );
1018
1019 let error = evaluate_with_phase(&schema, &doc, Some("does-not-exist")).unwrap_err();
1020 assert_eq!(
1021 error,
1022 SchematronEvalError::UnknownPhase {
1023 phase: "does-not-exist".to_owned(),
1024 }
1025 );
1026 }
1027
1028 #[test]
1029 fn phase_level_let_is_visible_within_that_phase() {
1030 let doc = doc_with_root_children(&[(None, "item")]);
1031 let schema = schema(
1032 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1033 <phase id="p1">
1034 <let name="x" value="'phase'"/>
1035 <active pattern="pa"/>
1036 </phase>
1037 <pattern id="pa">
1038 <rule context="item">
1039 <report test="$x = 'phase'">PHASE-LET</report>
1040 </rule>
1041 </pattern>
1042 </schema>"#,
1043 );
1044
1045 let reports = evaluate_with_phase(&schema, &doc, Some("p1")).unwrap();
1046 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
1047 assert_eq!(messages, vec!["PHASE-LET"]);
1048 }
1049
1050 #[test]
1054 fn plain_text_message_is_trimmed_at_render_time() {
1055 let doc = doc_with_root_children(&[(None, "item")]);
1056 let schema = schema(
1057 "<schema xmlns=\"http://purl.oclc.org/dsdl/schematron\">\
1058 <pattern>\
1059 <rule context=\"item\">\
1060 <assert test=\"false()\">\n first second \n</assert>\
1061 </rule>\
1062 </pattern>\
1063 </schema>",
1064 );
1065
1066 let reports = evaluate(&schema, &doc).unwrap();
1067 assert_eq!(reports[0].message, "first second");
1068 }
1069
1070 #[test]
1071 fn value_of_interpolates_the_context_node() {
1072 let doc = doc_with_root_children(&[(None, "item")]);
1073 let schema = schema(
1074 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1075 <pattern>
1076 <rule context="item">
1077 <assert test="false()">Element: <value-of select="local-name()"/>.</assert>
1078 </rule>
1079 </pattern>
1080 </schema>"#,
1081 );
1082
1083 let reports = evaluate(&schema, &doc).unwrap();
1084 assert_eq!(reports[0].message, "Element: item.");
1085 }
1086
1087 #[test]
1090 fn name_resolves_to_the_context_node_name() {
1091 let doc = doc_with_root_children(&[(None, "item")]);
1092 let schema = schema(
1093 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1094 <pattern>
1095 <rule context="item">
1096 <assert test="false()">Element <name/> is invalid.</assert>
1097 </rule>
1098 </pattern>
1099 </schema>"#,
1100 );
1101
1102 let reports = evaluate(&schema, &doc).unwrap();
1103 assert_eq!(reports[0].message, "Element item is invalid.");
1104 }
1105
1106 #[test]
1109 fn name_with_path_resolves_to_a_different_node() {
1110 let doc = doc_with_root_children(&[(None, "item")]);
1111 let schema = schema(
1112 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1113 <pattern>
1114 <rule context="item">
1115 <assert test="false()">Parent is <name path=".."/>.</assert>
1116 </rule>
1117 </pattern>
1118 </schema>"#,
1119 );
1120
1121 let reports = evaluate(&schema, &doc).unwrap();
1122 assert_eq!(reports[0].message, "Parent is .");
1127 }
1128
1129 #[test]
1130 fn value_of_can_reference_a_let_variable() {
1131 let doc = doc_with_root_children(&[(None, "item")]);
1132 let schema = schema(
1133 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1134 <pattern>
1135 <rule context="item">
1136 <let name="x" value="'bound-value'"/>
1137 <assert test="false()">x is <value-of select="$x"/></assert>
1138 </rule>
1139 </pattern>
1140 </schema>"#,
1141 );
1142
1143 let reports = evaluate(&schema, &doc).unwrap();
1144 assert_eq!(reports[0].message, "x is bound-value");
1145 }
1146
1147 #[test]
1150 fn broken_value_of_in_a_non_firing_check_is_not_an_error() {
1151 let doc = doc_with_root_children(&[(None, "item")]);
1152 let schema = schema(
1153 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1154 <pattern>
1155 <rule context="item">
1156 <assert test="true()">broken: <value-of select="(("/></assert>
1157 </rule>
1158 </pattern>
1159 </schema>"#,
1160 );
1161
1162 let reports = evaluate(&schema, &doc).unwrap();
1163 assert_eq!(reports.len(), 0);
1164 }
1165
1166 #[test]
1169 fn broken_value_of_in_a_firing_check_is_a_parse_error() {
1170 let doc = doc_with_root_children(&[(None, "item")]);
1171 let schema = schema(
1172 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1173 <pattern>
1174 <rule context="item">
1175 <assert test="false()">broken: <value-of select="(("/></assert>
1176 </rule>
1177 </pattern>
1178 </schema>"#,
1179 );
1180
1181 let result = evaluate(&schema, &doc);
1182 assert!(matches!(result, Err(SchematronEvalError::Parse(_))));
1183 }
1184
1185 #[test]
1190 fn extends_inlined_check_fires_during_evaluation() {
1191 let doc = doc_with_root_children(&[(None, "item")]);
1192 let schema = schema(
1193 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1194 <pattern>
1195 <rule abstract="true" id="base">
1196 <assert test="false()">FROM-BASE</assert>
1197 </rule>
1198 <rule context="item">
1199 <extends rule="base"/>
1200 </rule>
1201 </pattern>
1202 </schema>"#,
1203 );
1204
1205 let reports = evaluate(&schema, &doc).unwrap();
1206 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
1207 assert_eq!(messages, vec!["FROM-BASE"]);
1208 }
1209
1210 #[test]
1214 fn extends_inlined_let_is_visible_to_own_checks() {
1215 let doc = doc_with_root_children(&[(None, "item")]);
1216 let schema = schema(
1217 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1218 <pattern>
1219 <rule abstract="true" id="base">
1220 <let name="x" value="'from-base'"/>
1221 </rule>
1222 <rule context="item">
1223 <extends rule="base"/>
1224 <report test="$x = 'from-base'">INHERITED-LET</report>
1225 </rule>
1226 </pattern>
1227 </schema>"#,
1228 );
1229
1230 let reports = evaluate(&schema, &doc).unwrap();
1231 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
1232 assert_eq!(messages, vec!["INHERITED-LET"]);
1233 }
1234
1235 #[test]
1238 fn is_a_instantiated_pattern_evaluates_with_substituted_context() {
1239 let doc = doc_with_root_children(&[(None, "alpha"), (None, "beta")]);
1240 let schema = schema(
1241 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1242 <pattern abstract="true" id="tpl">
1243 <rule context="$el">
1244 <assert test="false()">MATCHED-$el</assert>
1245 </rule>
1246 </pattern>
1247 <pattern is-a="tpl" id="concrete">
1248 <param name="el" value="alpha"/>
1249 </pattern>
1250 </schema>"#,
1251 );
1252
1253 let reports = evaluate(&schema, &doc).unwrap();
1254 assert_eq!(reports.len(), 1);
1255 assert_eq!(reports[0].message, "MATCHED-$el");
1258 assert_eq!(reports[0].node, doc.node(1));
1259 }
1260
1261 #[test]
1262 fn fired_check_carries_its_rendered_diagnostics() {
1263 let doc = doc_with_root_children(&[(None, "item")]);
1264 let schema = schema(
1265 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1266 <diagnostics>
1267 <diagnostic id="d1">first</diagnostic>
1268 <diagnostic id="d2">element is <value-of select="local-name()"/></diagnostic>
1269 </diagnostics>
1270 <pattern>
1271 <rule context="item">
1272 <assert test="false()" diagnostics="d1 d2">broken</assert>
1273 </rule>
1274 </pattern>
1275 </schema>"#,
1276 );
1277
1278 let reports = evaluate(&schema, &doc).unwrap();
1279 assert_eq!(reports[0].diagnostics, vec!["first", "element is item"]);
1280 }
1281
1282 #[test]
1283 fn unknown_diagnostic_id_is_silently_skipped() {
1284 let schema_xml = r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1292 <diagnostics>
1293 <diagnostic id="d1">known</diagnostic>
1294 </diagnostics>
1295 <pattern>
1296 <rule context="item">
1297 <assert test="false()" diagnostics="d1 missing">broken</assert>
1298 </rule>
1299 </pattern>
1300 </schema>"#;
1301
1302 assert!(crate::parse(schema_xml).is_err());
1303 }
1304
1305 #[test]
1306 fn check_without_diagnostics_attribute_yields_an_empty_list() {
1307 let doc = doc_with_root_children(&[(None, "item")]);
1308 let schema = schema(
1309 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1310 <pattern>
1311 <rule context="item">
1312 <assert test="false()">broken</assert>
1313 </rule>
1314 </pattern>
1315 </schema>"#,
1316 );
1317
1318 let reports = evaluate(&schema, &doc).unwrap();
1319 assert_eq!(reports[0].diagnostics, Vec::<String>::new());
1320 }
1321}