1use std::collections::HashMap;
44
45use xpath_eval::{Document, EvaluationContext, Node, QName, Value};
46
47use crate::schema::{Check, CheckKind, Diagnostic, LetBinding, 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| format!("descendant-or-self::node()/{}", alternative.trim()))
222 .collect::<Vec<_>>()
223 .join(" | ");
224 let expr = xpath_eval::parse(&path).map_err(SchematronEvalError::Parse)?;
225
226 let mut ctx = EvaluationContext::new(document.root());
227 ctx.namespaces = Some(&ns_lookup);
228 let value = xpath_eval::evaluate(&expr, &ctx).map_err(SchematronEvalError::Eval)?;
229
230 let mut matches = match value {
231 Value::NodeSet(nodes) => nodes,
232 _ => {
233 return Err(SchematronEvalError::ContextNotNodeSet {
234 context: rule.context.clone(),
235 });
236 }
237 };
238 matches.sort_by(|a, b| a.document_order(*b));
239
240 for node in matches {
241 if claimed.contains(&node) {
242 continue;
243 }
244 claimed.push(node);
245
246 let mut node_vars = pattern_vars.clone();
247 extend_lets(&mut node_vars, &rule.lets, node, &ns_lookup)?;
248
249 for check in &rule.checks {
250 if let Some(report) = evaluate_check(
251 check,
252 node,
253 pattern.id.clone(),
254 &ns_lookup,
255 &node_vars,
256 &schema.diagnostics,
257 )? {
258 reports.push(report);
259 }
260 }
261 }
262 }
263 }
264
265 Ok(reports)
266}
267
268fn evaluate_xpath<'n, N: Node<'n>>(
275 raw: &str,
276 node: N,
277 ns_lookup: &dyn Fn(&str) -> Option<String>,
278 vars: &HashMap<String, Value<N>>,
279) -> Result<Value<N>, SchematronEvalError> {
280 let expr = xpath_eval::parse(raw).map_err(SchematronEvalError::Parse)?;
281 let lookup = |name: &QName| {
282 if name.prefix.is_some() {
283 None
284 } else {
285 vars.get(&name.local).cloned()
286 }
287 };
288 let mut ctx = EvaluationContext::new(node);
289 ctx.namespaces = Some(ns_lookup);
290 ctx.variables = Some(&lookup);
291 xpath_eval::evaluate(&expr, &ctx).map_err(SchematronEvalError::Eval)
292}
293
294fn extend_lets<'n, N: Node<'n>>(
302 vars: &mut HashMap<String, Value<N>>,
303 lets: &[LetBinding],
304 context_node: N,
305 ns_lookup: &dyn Fn(&str) -> Option<String>,
306) -> Result<(), SchematronEvalError> {
307 for binding in lets {
308 let value = evaluate_xpath(&binding.value, context_node, ns_lookup, vars)?;
309 vars.insert(binding.name.clone(), value);
310 }
311 Ok(())
312}
313
314fn evaluate_check<'n, N: Node<'n>>(
318 check: &Check,
319 node: N,
320 pattern_id: Option<String>,
321 ns_lookup: &dyn Fn(&str) -> Option<String>,
322 vars: &HashMap<String, Value<N>>,
323 diagnostics: &[Diagnostic],
324) -> Result<Option<Report<N>>, SchematronEvalError> {
325 let value = evaluate_xpath(&check.test, node, ns_lookup, vars)?;
326 let boolean = value.to_boolean();
327
328 let fires = match check.kind {
329 CheckKind::Assert => !boolean,
330 CheckKind::Report => boolean,
331 };
332
333 if !fires {
334 return Ok(None);
335 }
336
337 let message = render_message(&check.message, node, ns_lookup, vars)?;
338
339 let rendered_diagnostics = check
340 .diagnostics
341 .iter()
342 .filter_map(|id| diagnostics.iter().find(|d| &d.id == id))
343 .map(|d| render_message(&d.message, node, ns_lookup, vars))
344 .collect::<Result<Vec<_>, _>>()?;
345
346 Ok(Some(Report {
347 pattern_id,
348 check_id: check.id.clone(),
349 role: check.role.clone(),
350 kind: check.kind,
351 message,
352 diagnostics: rendered_diagnostics,
353 node,
354 }))
355}
356
357fn render_message<'n, N: Node<'n>>(
367 parts: &[MessagePart],
368 node: N,
369 ns_lookup: &dyn Fn(&str) -> Option<String>,
370 vars: &HashMap<String, Value<N>>,
371) -> Result<String, SchematronEvalError> {
372 let mut message = String::new();
373 for part in parts {
374 match part {
375 MessagePart::Text(text) => message.push_str(text),
376 MessagePart::ValueOf(select) => {
377 let value = evaluate_xpath(select, node, ns_lookup, vars)?;
378 message.push_str(&value.to_xpath_string());
379 }
380 }
381 }
382 Ok(message.trim().to_owned())
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use std::cmp::Ordering;
389 use xpath_eval::{ExpandedName, NodeKind};
390
391 #[derive(Debug)]
397 struct ElementData {
398 namespace_uri: Option<String>,
399 local_name: String,
400 parent: Option<usize>,
401 children: Vec<usize>,
402 }
403
404 #[derive(Debug)]
405 struct Arena {
406 elements: Vec<ElementData>,
407 }
408
409 struct TestDoc {
410 arena: Arena,
411 }
412
413 impl TestDoc {
414 fn node(&self, idx: usize) -> TestNode<'_> {
415 TestNode {
416 arena: &self.arena,
417 idx,
418 }
419 }
420 }
421
422 impl xpath_eval::Document for TestDoc {
423 type N<'a> = TestNode<'a>;
424
425 fn root(&self) -> Self::N<'_> {
426 self.node(0)
427 }
428 }
429
430 #[derive(Clone, Copy, Debug)]
431 struct TestNode<'a> {
432 arena: &'a Arena,
433 idx: usize,
434 }
435
436 impl<'a> PartialEq for TestNode<'a> {
437 fn eq(&self, other: &Self) -> bool {
438 std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
439 }
440 }
441
442 impl<'a> Eq for TestNode<'a> {}
443
444 impl<'a> Node<'a> for TestNode<'a> {
445 fn kind(self) -> NodeKind {
446 if self.idx == 0 {
447 NodeKind::Root
448 } else {
449 NodeKind::Element
450 }
451 }
452
453 fn parent(self) -> Option<Self> {
454 self.arena.elements[self.idx].parent.map(|p| TestNode {
455 arena: self.arena,
456 idx: p,
457 })
458 }
459
460 fn children(self) -> impl Iterator<Item = Self> + 'a {
461 let arena = self.arena;
462 self.arena.elements[self.idx]
463 .children
464 .clone()
465 .into_iter()
466 .map(move |i| TestNode { arena, idx: i })
467 }
468
469 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
470 std::iter::empty()
471 }
472
473 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
474 std::iter::empty()
475 }
476
477 fn expanded_name(self) -> Option<ExpandedName> {
478 if self.idx == 0 {
479 None
480 } else {
481 let element = &self.arena.elements[self.idx];
482 Some(ExpandedName {
483 namespace_uri: element.namespace_uri.clone(),
484 local_name: element.local_name.clone(),
485 })
486 }
487 }
488
489 fn string_value(self) -> String {
490 String::new()
491 }
492
493 fn document_order(self, other: Self) -> Ordering {
494 self.idx.cmp(&other.idx)
495 }
496 }
497
498 fn doc_with_root_children(children: &[(Option<&str>, &str)]) -> TestDoc {
501 let mut elements = vec![ElementData {
502 namespace_uri: None,
503 local_name: String::new(),
504 parent: None,
505 children: Vec::new(),
506 }];
507 for (namespace_uri, local_name) in children {
508 let idx = elements.len();
509 elements.push(ElementData {
510 namespace_uri: namespace_uri.map(str::to_owned),
511 local_name: (*local_name).to_owned(),
512 parent: Some(0),
513 children: Vec::new(),
514 });
515 elements[0].children.push(idx);
516 }
517 TestDoc {
518 arena: Arena { elements },
519 }
520 }
521
522 fn schema(sch_xml: &str) -> Schema {
523 crate::parse(sch_xml).expect("test schema must parse")
524 }
525
526 fn doc_with_nested_item() -> TestDoc {
535 let elements = vec![
536 ElementData {
537 namespace_uri: None,
538 local_name: String::new(),
539 parent: None,
540 children: vec![1],
541 },
542 ElementData {
543 namespace_uri: None,
544 local_name: "wrapper".to_owned(),
545 parent: Some(0),
546 children: vec![2],
547 },
548 ElementData {
549 namespace_uri: None,
550 local_name: "item".to_owned(),
551 parent: Some(1),
552 children: Vec::new(),
553 },
554 ];
555 TestDoc {
556 arena: Arena { elements },
557 }
558 }
559
560 #[test]
561 fn assert_true_produces_no_report() {
562 let doc = doc_with_root_children(&[(None, "item")]);
563 let schema = schema(
564 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
565 <pattern>
566 <rule context="item">
567 <assert test="true()">should not fire</assert>
568 </rule>
569 </pattern>
570 </schema>"#,
571 );
572
573 let reports = evaluate(&schema, &doc).unwrap();
574 assert_eq!(reports.len(), 0);
575 }
576
577 #[test]
578 fn assert_false_produces_one_report() {
579 let doc = doc_with_root_children(&[(None, "item")]);
580 let schema = schema(
581 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
582 <pattern id="p1">
583 <rule context="item">
584 <assert id="a1" role="error" test="false()">item is invalid</assert>
585 </rule>
586 </pattern>
587 </schema>"#,
588 );
589
590 let reports = evaluate(&schema, &doc).unwrap();
591 assert_eq!(reports.len(), 1);
592 let report = &reports[0];
593 assert_eq!(report.pattern_id.as_deref(), Some("p1"));
594 assert_eq!(report.check_id.as_deref(), Some("a1"));
595 assert_eq!(report.role.as_deref(), Some("error"));
596 assert_eq!(report.kind, CheckKind::Assert);
597 assert_eq!(report.message, "item is invalid");
598 assert_eq!(report.node, doc.node(1));
599 }
600
601 #[test]
617 fn context_union_matches_through_every_alternative() {
618 let doc = doc_with_nested_item();
619
620 let first_alternative = schema(
621 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
622 <pattern>
623 <rule context="item | does-not-exist">
624 <report test="true()">observed item</report>
625 </rule>
626 </pattern>
627 </schema>"#,
628 );
629 let reports = evaluate(&first_alternative, &doc).unwrap();
630 assert_eq!(reports.len(), 1, "item as the first union alternative");
631
632 let second_alternative = schema(
633 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
634 <pattern>
635 <rule context="does-not-exist | item">
636 <report test="true()">observed item</report>
637 </rule>
638 </pattern>
639 </schema>"#,
640 );
641 let reports = evaluate(&second_alternative, &doc).unwrap();
642 assert_eq!(reports.len(), 1, "item as the second union alternative");
643 }
644
645 #[test]
646 fn split_top_level_union_ignores_nested_pipes() {
647 assert_eq!(split_top_level_union("item"), vec!["item"]);
648 assert_eq!(split_top_level_union("a | b | c"), vec!["a ", " b ", " c"]);
649 assert_eq!(
652 split_top_level_union("a[@x = 'p|q']"),
653 vec!["a[@x = 'p|q']"]
654 );
655 assert_eq!(split_top_level_union("a[b | c]"), vec!["a[b | c]"]);
656 assert_eq!(split_top_level_union("(a | b)/c"), vec!["(a | b)/c"]);
657 assert_eq!(
658 split_top_level_union("a[@x = 'p|q'] | b"),
659 vec!["a[@x = 'p|q'] ", " b"]
660 );
661 }
662
663 #[test]
664 fn report_true_fires() {
665 let doc = doc_with_root_children(&[(None, "item")]);
666 let schema = schema(
667 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
668 <pattern>
669 <rule context="item">
670 <report test="true()">observed item</report>
671 </rule>
672 </pattern>
673 </schema>"#,
674 );
675
676 let reports = evaluate(&schema, &doc).unwrap();
677 assert_eq!(reports.len(), 1);
678 assert_eq!(reports[0].kind, CheckKind::Report);
679 assert_eq!(reports[0].message, "observed item");
680 }
681
682 #[test]
688 fn same_pattern_first_matching_rule_wins() {
689 let doc = doc_with_root_children(&[(None, "item")]);
690 let schema = schema(
691 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
692 <pattern>
693 <rule context="item">
694 <assert test="false()">RULE-ONE-FIRED</assert>
695 </rule>
696 <rule context="item">
697 <assert test="false()">RULE-TWO-FIRED</assert>
698 </rule>
699 </pattern>
700 </schema>"#,
701 );
702
703 let reports = evaluate(&schema, &doc).unwrap();
704 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
705 assert_eq!(messages, vec!["RULE-ONE-FIRED"]);
706 }
707
708 #[test]
712 fn independent_patterns_both_fire() {
713 let doc = doc_with_root_children(&[(None, "item")]);
714 let schema = schema(
715 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
716 <pattern>
717 <rule context="item">
718 <assert test="false()">PATTERN-ONE-FIRED</assert>
719 </rule>
720 </pattern>
721 <pattern>
722 <rule context="item">
723 <assert test="false()">PATTERN-TWO-FIRED</assert>
724 </rule>
725 </pattern>
726 </schema>"#,
727 );
728
729 let reports = evaluate(&schema, &doc).unwrap();
730 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
731 assert_eq!(messages, vec!["PATTERN-ONE-FIRED", "PATTERN-TWO-FIRED"]);
732 }
733
734 #[test]
741 fn namespace_binding_resolves_prefixed_context() {
742 let doc = doc_with_root_children(&[(Some("urn:test"), "item")]);
743 let schema = schema(
744 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
745 <ns prefix="t" uri="urn:test"/>
746 <pattern>
747 <rule context="t:item">
748 <assert test="false()">NS-MATCH</assert>
749 </rule>
750 </pattern>
751 </schema>"#,
752 );
753
754 let reports = evaluate(&schema, &doc).unwrap();
755 assert_eq!(reports.len(), 1);
756 assert_eq!(reports[0].message, "NS-MATCH");
757 }
758
759 #[test]
760 fn invalid_test_expression_is_a_parse_error_not_a_panic() {
761 let doc = doc_with_root_children(&[(None, "item")]);
762 let schema = schema(
763 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
764 <pattern>
765 <rule context="item">
766 <assert test="((">broken</assert>
767 </rule>
768 </pattern>
769 </schema>"#,
770 );
771
772 let result = evaluate(&schema, &doc);
773 assert!(matches!(result, Err(SchematronEvalError::Parse(_))));
774 }
775
776 #[test]
777 fn schema_level_let_is_visible_to_checks() {
778 let doc = doc_with_root_children(&[(None, "item")]);
779 let schema = schema(
780 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
781 <let name="x" value="'schema'"/>
782 <pattern>
783 <rule context="item">
784 <report test="$x = 'schema'">SCHEMA-LET</report>
785 </rule>
786 </pattern>
787 </schema>"#,
788 );
789
790 let reports = evaluate(&schema, &doc).unwrap();
791 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
792 assert_eq!(messages, vec!["SCHEMA-LET"]);
793 }
794
795 #[test]
800 fn pattern_level_let_shadows_schema_level_let() {
801 let doc = doc_with_root_children(&[(None, "item")]);
802 let schema = schema(
803 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
804 <let name="x" value="'schema'"/>
805 <pattern>
806 <let name="x" value="'pattern'"/>
807 <rule context="item">
808 <report test="$x = 'pattern'">PATTERN-WINS</report>
809 </rule>
810 </pattern>
811 </schema>"#,
812 );
813
814 let reports = evaluate(&schema, &doc).unwrap();
815 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
816 assert_eq!(messages, vec!["PATTERN-WINS"]);
817 }
818
819 #[test]
823 fn rule_level_let_is_reevaluated_per_context_node() {
824 let doc = doc_with_root_children(&[(None, "alpha"), (None, "beta")]);
825 let schema = schema(
826 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
827 <pattern>
828 <rule context="*">
829 <let name="n" value="local-name()"/>
830 <assert test="$n = 'alpha'">NOT-ALPHA</assert>
831 </rule>
832 </pattern>
833 </schema>"#,
834 );
835
836 let reports = evaluate(&schema, &doc).unwrap();
837 assert_eq!(reports.len(), 1);
838 assert_eq!(reports[0].message, "NOT-ALPHA");
839 assert_eq!(reports[0].node, doc.node(2));
840 }
841
842 #[test]
845 fn let_can_reference_earlier_let_in_same_scope() {
846 let doc = doc_with_root_children(&[(None, "item")]);
847 let schema = schema(
848 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
849 <let name="a" value="'A'"/>
850 <let name="b" value="concat($a, 'B')"/>
851 <pattern>
852 <rule context="item">
853 <report test="$b = 'AB'">SEQUENTIAL</report>
854 </rule>
855 </pattern>
856 </schema>"#,
857 );
858
859 let reports = evaluate(&schema, &doc).unwrap();
860 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
861 assert_eq!(messages, vec!["SEQUENTIAL"]);
862 }
863
864 #[test]
865 fn unbound_variable_is_an_eval_error_not_a_panic() {
866 let doc = doc_with_root_children(&[(None, "item")]);
867 let schema = schema(
868 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
869 <pattern>
870 <rule context="item">
871 <assert test="$missing">broken</assert>
872 </rule>
873 </pattern>
874 </schema>"#,
875 );
876
877 let result = evaluate(&schema, &doc);
878 assert!(matches!(result, Err(SchematronEvalError::Eval(_))));
879 }
880
881 #[test]
884 fn evaluate_with_phase_restricts_to_activated_patterns() {
885 let doc = doc_with_root_children(&[(None, "item")]);
886 let schema = schema(
887 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
888 <phase id="p1">
889 <active pattern="pa"/>
890 </phase>
891 <phase id="p2">
892 <active pattern="pb"/>
893 </phase>
894 <pattern id="pa">
895 <rule context="item">
896 <assert test="false()">PATTERN-A</assert>
897 </rule>
898 </pattern>
899 <pattern id="pb">
900 <rule context="item">
901 <assert test="false()">PATTERN-B</assert>
902 </rule>
903 </pattern>
904 </schema>"#,
905 );
906
907 let reports_p1 = evaluate_with_phase(&schema, &doc, Some("p1")).unwrap();
908 let messages_p1: Vec<&str> = reports_p1.iter().map(|r| r.message.as_str()).collect();
909 assert_eq!(messages_p1, vec!["PATTERN-A"]);
910
911 let reports_p2 = evaluate_with_phase(&schema, &doc, Some("p2")).unwrap();
912 let messages_p2: Vec<&str> = reports_p2.iter().map(|r| r.message.as_str()).collect();
913 assert_eq!(messages_p2, vec!["PATTERN-B"]);
914
915 let reports_all = evaluate_with_phase(&schema, &doc, None).unwrap();
916 let messages_all: Vec<&str> = reports_all.iter().map(|r| r.message.as_str()).collect();
917 assert_eq!(messages_all, vec!["PATTERN-A", "PATTERN-B"]);
918 }
919
920 #[test]
926 fn evaluate_honors_default_phase() {
927 let doc = doc_with_root_children(&[(None, "item")]);
928 let schema = schema(
929 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="p1">
930 <phase id="p1">
931 <active pattern="pa"/>
932 </phase>
933 <pattern id="pa">
934 <rule context="item">
935 <assert test="false()">PATTERN-A</assert>
936 </rule>
937 </pattern>
938 <pattern id="pb">
939 <rule context="item">
940 <assert test="false()">PATTERN-B</assert>
941 </rule>
942 </pattern>
943 </schema>"#,
944 );
945
946 let reports = evaluate(&schema, &doc).unwrap();
947 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
948 assert_eq!(messages, vec!["PATTERN-A"]);
949 }
950
951 #[test]
952 fn evaluate_with_phase_unknown_id_is_an_error() {
953 let doc = doc_with_root_children(&[(None, "item")]);
954 let schema = schema(
955 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
956 <pattern id="pa">
957 <rule context="item">
958 <assert test="false()">PATTERN-A</assert>
959 </rule>
960 </pattern>
961 </schema>"#,
962 );
963
964 let error = evaluate_with_phase(&schema, &doc, Some("does-not-exist")).unwrap_err();
965 assert_eq!(
966 error,
967 SchematronEvalError::UnknownPhase {
968 phase: "does-not-exist".to_owned(),
969 }
970 );
971 }
972
973 #[test]
974 fn phase_level_let_is_visible_within_that_phase() {
975 let doc = doc_with_root_children(&[(None, "item")]);
976 let schema = schema(
977 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
978 <phase id="p1">
979 <let name="x" value="'phase'"/>
980 <active pattern="pa"/>
981 </phase>
982 <pattern id="pa">
983 <rule context="item">
984 <report test="$x = 'phase'">PHASE-LET</report>
985 </rule>
986 </pattern>
987 </schema>"#,
988 );
989
990 let reports = evaluate_with_phase(&schema, &doc, Some("p1")).unwrap();
991 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
992 assert_eq!(messages, vec!["PHASE-LET"]);
993 }
994
995 #[test]
999 fn plain_text_message_is_trimmed_at_render_time() {
1000 let doc = doc_with_root_children(&[(None, "item")]);
1001 let schema = schema(
1002 "<schema xmlns=\"http://purl.oclc.org/dsdl/schematron\">\
1003 <pattern>\
1004 <rule context=\"item\">\
1005 <assert test=\"false()\">\n first second \n</assert>\
1006 </rule>\
1007 </pattern>\
1008 </schema>",
1009 );
1010
1011 let reports = evaluate(&schema, &doc).unwrap();
1012 assert_eq!(reports[0].message, "first second");
1013 }
1014
1015 #[test]
1016 fn value_of_interpolates_the_context_node() {
1017 let doc = doc_with_root_children(&[(None, "item")]);
1018 let schema = schema(
1019 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1020 <pattern>
1021 <rule context="item">
1022 <assert test="false()">Element: <value-of select="local-name()"/>.</assert>
1023 </rule>
1024 </pattern>
1025 </schema>"#,
1026 );
1027
1028 let reports = evaluate(&schema, &doc).unwrap();
1029 assert_eq!(reports[0].message, "Element: item.");
1030 }
1031
1032 #[test]
1033 fn value_of_can_reference_a_let_variable() {
1034 let doc = doc_with_root_children(&[(None, "item")]);
1035 let schema = schema(
1036 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1037 <pattern>
1038 <rule context="item">
1039 <let name="x" value="'bound-value'"/>
1040 <assert test="false()">x is <value-of select="$x"/></assert>
1041 </rule>
1042 </pattern>
1043 </schema>"#,
1044 );
1045
1046 let reports = evaluate(&schema, &doc).unwrap();
1047 assert_eq!(reports[0].message, "x is bound-value");
1048 }
1049
1050 #[test]
1053 fn broken_value_of_in_a_non_firing_check_is_not_an_error() {
1054 let doc = doc_with_root_children(&[(None, "item")]);
1055 let schema = schema(
1056 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1057 <pattern>
1058 <rule context="item">
1059 <assert test="true()">broken: <value-of select="(("/></assert>
1060 </rule>
1061 </pattern>
1062 </schema>"#,
1063 );
1064
1065 let reports = evaluate(&schema, &doc).unwrap();
1066 assert_eq!(reports.len(), 0);
1067 }
1068
1069 #[test]
1072 fn broken_value_of_in_a_firing_check_is_a_parse_error() {
1073 let doc = doc_with_root_children(&[(None, "item")]);
1074 let schema = schema(
1075 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1076 <pattern>
1077 <rule context="item">
1078 <assert test="false()">broken: <value-of select="(("/></assert>
1079 </rule>
1080 </pattern>
1081 </schema>"#,
1082 );
1083
1084 let result = evaluate(&schema, &doc);
1085 assert!(matches!(result, Err(SchematronEvalError::Parse(_))));
1086 }
1087
1088 #[test]
1093 fn extends_inlined_check_fires_during_evaluation() {
1094 let doc = doc_with_root_children(&[(None, "item")]);
1095 let schema = schema(
1096 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1097 <pattern>
1098 <rule abstract="true" id="base">
1099 <assert test="false()">FROM-BASE</assert>
1100 </rule>
1101 <rule context="item">
1102 <extends rule="base"/>
1103 </rule>
1104 </pattern>
1105 </schema>"#,
1106 );
1107
1108 let reports = evaluate(&schema, &doc).unwrap();
1109 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
1110 assert_eq!(messages, vec!["FROM-BASE"]);
1111 }
1112
1113 #[test]
1116 fn is_a_instantiated_pattern_evaluates_with_substituted_context() {
1117 let doc = doc_with_root_children(&[(None, "alpha"), (None, "beta")]);
1118 let schema = schema(
1119 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1120 <pattern abstract="true" id="tpl">
1121 <rule context="$el">
1122 <assert test="false()">MATCHED-$el</assert>
1123 </rule>
1124 </pattern>
1125 <pattern is-a="tpl" id="concrete">
1126 <param name="el" value="alpha"/>
1127 </pattern>
1128 </schema>"#,
1129 );
1130
1131 let reports = evaluate(&schema, &doc).unwrap();
1132 assert_eq!(reports.len(), 1);
1133 assert_eq!(reports[0].message, "MATCHED-$el");
1136 assert_eq!(reports[0].node, doc.node(1));
1137 }
1138
1139 #[test]
1140 fn fired_check_carries_its_rendered_diagnostics() {
1141 let doc = doc_with_root_children(&[(None, "item")]);
1142 let schema = schema(
1143 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1144 <diagnostics>
1145 <diagnostic id="d1">first</diagnostic>
1146 <diagnostic id="d2">element is <value-of select="local-name()"/></diagnostic>
1147 </diagnostics>
1148 <pattern>
1149 <rule context="item">
1150 <assert test="false()" diagnostics="d1 d2">broken</assert>
1151 </rule>
1152 </pattern>
1153 </schema>"#,
1154 );
1155
1156 let reports = evaluate(&schema, &doc).unwrap();
1157 assert_eq!(reports[0].diagnostics, vec!["first", "element is item"]);
1158 }
1159
1160 #[test]
1161 fn unknown_diagnostic_id_is_silently_skipped() {
1162 let schema_xml = r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1170 <diagnostics>
1171 <diagnostic id="d1">known</diagnostic>
1172 </diagnostics>
1173 <pattern>
1174 <rule context="item">
1175 <assert test="false()" diagnostics="d1 missing">broken</assert>
1176 </rule>
1177 </pattern>
1178 </schema>"#;
1179
1180 assert!(crate::parse(schema_xml).is_err());
1181 }
1182
1183 #[test]
1184 fn check_without_diagnostics_attribute_yields_an_empty_list() {
1185 let doc = doc_with_root_children(&[(None, "item")]);
1186 let schema = schema(
1187 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1188 <pattern>
1189 <rule context="item">
1190 <assert test="false()">broken</assert>
1191 </rule>
1192 </pattern>
1193 </schema>"#,
1194 );
1195
1196 let reports = evaluate(&schema, &doc).unwrap();
1197 assert_eq!(reports[0].diagnostics, Vec::<String>::new());
1198 }
1199}