1use std::collections::HashMap;
37
38use xpath_eval::{Document, EvaluationContext, Node, QName, Value};
39
40use crate::schema::{Check, CheckKind, Diagnostic, LetBinding, MessagePart, Schema};
41
42#[derive(Clone, Debug)]
46pub struct Report<N> {
47 pub pattern_id: Option<String>,
48 pub check_id: Option<String>,
49 pub role: Option<String>,
50 pub kind: CheckKind,
51 pub message: String,
52 pub diagnostics: Vec<String>,
55 pub node: N,
56}
57
58#[derive(Debug, Clone, PartialEq)]
70pub enum SchematronEvalError {
71 Parse(xpath_eval::ParseError),
73 Eval(xpath_eval::EvalError),
75 ContextNotNodeSet { context: String },
80 UnknownPhase { phase: String },
84}
85
86impl std::fmt::Display for SchematronEvalError {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 SchematronEvalError::Parse(err) => write!(f, "XPath parse error: {err}"),
90 SchematronEvalError::Eval(err) => write!(f, "XPath evaluation error: {err}"),
91 SchematronEvalError::ContextNotNodeSet { context } => {
92 write!(f, "rule context {context:?} did not evaluate to a node-set")
93 }
94 SchematronEvalError::UnknownPhase { phase } => {
95 write!(f, "no <phase id={phase:?}> in this schema")
96 }
97 }
98 }
99}
100
101impl std::error::Error for SchematronEvalError {}
102
103pub fn evaluate<'a, D: Document>(
110 schema: &Schema,
111 document: &'a D,
112) -> Result<Vec<Report<D::N<'a>>>, SchematronEvalError> {
113 evaluate_with_phase(schema, document, schema.default_phase.as_deref())
114}
115
116pub fn evaluate_with_phase<'a, D: Document>(
122 schema: &Schema,
123 document: &'a D,
124 phase_id: Option<&str>,
125) -> Result<Vec<Report<D::N<'a>>>, SchematronEvalError> {
126 let ns_lookup = |prefix: &str| {
127 schema
128 .ns
129 .iter()
130 .find(|binding| binding.prefix == prefix)
131 .map(|binding| binding.uri.clone())
132 };
133
134 let phase = phase_id
135 .map(|id| {
136 schema
137 .phases
138 .iter()
139 .find(|phase| phase.id == id)
140 .ok_or_else(|| SchematronEvalError::UnknownPhase {
141 phase: id.to_owned(),
142 })
143 })
144 .transpose()?;
145
146 let mut reports = Vec::new();
147
148 let mut schema_vars: HashMap<String, Value<D::N<'a>>> = HashMap::new();
149 extend_lets(&mut schema_vars, &schema.lets, document.root(), &ns_lookup)?;
150
151 let mut phase_vars = schema_vars.clone();
152 if let Some(phase) = phase {
153 extend_lets(&mut phase_vars, &phase.lets, document.root(), &ns_lookup)?;
154 }
155
156 for pattern in &schema.patterns {
157 if let Some(phase) = phase {
158 let is_active = pattern
159 .id
160 .as_deref()
161 .is_some_and(|id| phase.active.iter().any(|active| active == id));
162 if !is_active {
163 continue;
164 }
165 }
166
167 let mut pattern_vars = phase_vars.clone();
168 extend_lets(
169 &mut pattern_vars,
170 &pattern.lets,
171 document.root(),
172 &ns_lookup,
173 )?;
174
175 let mut claimed: Vec<D::N<'a>> = Vec::new();
176
177 for rule in &pattern.rules {
178 let path = format!("descendant-or-self::node()/{}", rule.context);
179 let expr = xpath_eval::parse(&path).map_err(SchematronEvalError::Parse)?;
180
181 let mut ctx = EvaluationContext::new(document.root());
182 ctx.namespaces = Some(&ns_lookup);
183 let value = xpath_eval::evaluate(&expr, &ctx).map_err(SchematronEvalError::Eval)?;
184
185 let mut matches = match value {
186 Value::NodeSet(nodes) => nodes,
187 _ => {
188 return Err(SchematronEvalError::ContextNotNodeSet {
189 context: rule.context.clone(),
190 });
191 }
192 };
193 matches.sort_by(|a, b| a.document_order(*b));
194
195 for node in matches {
196 if claimed.contains(&node) {
197 continue;
198 }
199 claimed.push(node);
200
201 let mut node_vars = pattern_vars.clone();
202 extend_lets(&mut node_vars, &rule.lets, node, &ns_lookup)?;
203
204 for check in &rule.checks {
205 if let Some(report) = evaluate_check(
206 check,
207 node,
208 pattern.id.clone(),
209 &ns_lookup,
210 &node_vars,
211 &schema.diagnostics,
212 )? {
213 reports.push(report);
214 }
215 }
216 }
217 }
218 }
219
220 Ok(reports)
221}
222
223fn evaluate_xpath<'n, N: Node<'n>>(
230 raw: &str,
231 node: N,
232 ns_lookup: &dyn Fn(&str) -> Option<String>,
233 vars: &HashMap<String, Value<N>>,
234) -> Result<Value<N>, SchematronEvalError> {
235 let expr = xpath_eval::parse(raw).map_err(SchematronEvalError::Parse)?;
236 let lookup = |name: &QName| {
237 if name.prefix.is_some() {
238 None
239 } else {
240 vars.get(&name.local).cloned()
241 }
242 };
243 let mut ctx = EvaluationContext::new(node);
244 ctx.namespaces = Some(ns_lookup);
245 ctx.variables = Some(&lookup);
246 xpath_eval::evaluate(&expr, &ctx).map_err(SchematronEvalError::Eval)
247}
248
249fn extend_lets<'n, N: Node<'n>>(
257 vars: &mut HashMap<String, Value<N>>,
258 lets: &[LetBinding],
259 context_node: N,
260 ns_lookup: &dyn Fn(&str) -> Option<String>,
261) -> Result<(), SchematronEvalError> {
262 for binding in lets {
263 let value = evaluate_xpath(&binding.value, context_node, ns_lookup, vars)?;
264 vars.insert(binding.name.clone(), value);
265 }
266 Ok(())
267}
268
269fn evaluate_check<'n, N: Node<'n>>(
273 check: &Check,
274 node: N,
275 pattern_id: Option<String>,
276 ns_lookup: &dyn Fn(&str) -> Option<String>,
277 vars: &HashMap<String, Value<N>>,
278 diagnostics: &[Diagnostic],
279) -> Result<Option<Report<N>>, SchematronEvalError> {
280 let value = evaluate_xpath(&check.test, node, ns_lookup, vars)?;
281 let boolean = value.to_boolean();
282
283 let fires = match check.kind {
284 CheckKind::Assert => !boolean,
285 CheckKind::Report => boolean,
286 };
287
288 if !fires {
289 return Ok(None);
290 }
291
292 let message = render_message(&check.message, node, ns_lookup, vars)?;
293
294 let rendered_diagnostics = check
295 .diagnostics
296 .iter()
297 .filter_map(|id| diagnostics.iter().find(|d| &d.id == id))
298 .map(|d| render_message(&d.message, node, ns_lookup, vars))
299 .collect::<Result<Vec<_>, _>>()?;
300
301 Ok(Some(Report {
302 pattern_id,
303 check_id: check.id.clone(),
304 role: check.role.clone(),
305 kind: check.kind,
306 message,
307 diagnostics: rendered_diagnostics,
308 node,
309 }))
310}
311
312fn render_message<'n, N: Node<'n>>(
322 parts: &[MessagePart],
323 node: N,
324 ns_lookup: &dyn Fn(&str) -> Option<String>,
325 vars: &HashMap<String, Value<N>>,
326) -> Result<String, SchematronEvalError> {
327 let mut message = String::new();
328 for part in parts {
329 match part {
330 MessagePart::Text(text) => message.push_str(text),
331 MessagePart::ValueOf(select) => {
332 let value = evaluate_xpath(select, node, ns_lookup, vars)?;
333 message.push_str(&value.to_xpath_string());
334 }
335 }
336 }
337 Ok(message.trim().to_owned())
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use std::cmp::Ordering;
344 use xpath_eval::{ExpandedName, NodeKind};
345
346 #[derive(Debug)]
352 struct ElementData {
353 namespace_uri: Option<String>,
354 local_name: String,
355 parent: Option<usize>,
356 children: Vec<usize>,
357 }
358
359 #[derive(Debug)]
360 struct Arena {
361 elements: Vec<ElementData>,
362 }
363
364 struct TestDoc {
365 arena: Arena,
366 }
367
368 impl TestDoc {
369 fn node(&self, idx: usize) -> TestNode<'_> {
370 TestNode {
371 arena: &self.arena,
372 idx,
373 }
374 }
375 }
376
377 impl xpath_eval::Document for TestDoc {
378 type N<'a> = TestNode<'a>;
379
380 fn root(&self) -> Self::N<'_> {
381 self.node(0)
382 }
383 }
384
385 #[derive(Clone, Copy, Debug)]
386 struct TestNode<'a> {
387 arena: &'a Arena,
388 idx: usize,
389 }
390
391 impl<'a> PartialEq for TestNode<'a> {
392 fn eq(&self, other: &Self) -> bool {
393 std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
394 }
395 }
396
397 impl<'a> Eq for TestNode<'a> {}
398
399 impl<'a> Node<'a> for TestNode<'a> {
400 fn kind(self) -> NodeKind {
401 if self.idx == 0 {
402 NodeKind::Root
403 } else {
404 NodeKind::Element
405 }
406 }
407
408 fn parent(self) -> Option<Self> {
409 self.arena.elements[self.idx].parent.map(|p| TestNode {
410 arena: self.arena,
411 idx: p,
412 })
413 }
414
415 fn children(self) -> impl Iterator<Item = Self> + 'a {
416 let arena = self.arena;
417 self.arena.elements[self.idx]
418 .children
419 .clone()
420 .into_iter()
421 .map(move |i| TestNode { arena, idx: i })
422 }
423
424 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
425 std::iter::empty()
426 }
427
428 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
429 std::iter::empty()
430 }
431
432 fn expanded_name(self) -> Option<ExpandedName> {
433 if self.idx == 0 {
434 None
435 } else {
436 let element = &self.arena.elements[self.idx];
437 Some(ExpandedName {
438 namespace_uri: element.namespace_uri.clone(),
439 local_name: element.local_name.clone(),
440 })
441 }
442 }
443
444 fn string_value(self) -> String {
445 String::new()
446 }
447
448 fn document_order(self, other: Self) -> Ordering {
449 self.idx.cmp(&other.idx)
450 }
451 }
452
453 fn doc_with_root_children(children: &[(Option<&str>, &str)]) -> TestDoc {
456 let mut elements = vec![ElementData {
457 namespace_uri: None,
458 local_name: String::new(),
459 parent: None,
460 children: Vec::new(),
461 }];
462 for (namespace_uri, local_name) in children {
463 let idx = elements.len();
464 elements.push(ElementData {
465 namespace_uri: namespace_uri.map(str::to_owned),
466 local_name: (*local_name).to_owned(),
467 parent: Some(0),
468 children: Vec::new(),
469 });
470 elements[0].children.push(idx);
471 }
472 TestDoc {
473 arena: Arena { elements },
474 }
475 }
476
477 fn schema(sch_xml: &str) -> Schema {
478 crate::parse(sch_xml).expect("test schema must parse")
479 }
480
481 #[test]
482 fn assert_true_produces_no_report() {
483 let doc = doc_with_root_children(&[(None, "item")]);
484 let schema = schema(
485 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
486 <pattern>
487 <rule context="item">
488 <assert test="true()">should not fire</assert>
489 </rule>
490 </pattern>
491 </schema>"#,
492 );
493
494 let reports = evaluate(&schema, &doc).unwrap();
495 assert_eq!(reports.len(), 0);
496 }
497
498 #[test]
499 fn assert_false_produces_one_report() {
500 let doc = doc_with_root_children(&[(None, "item")]);
501 let schema = schema(
502 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
503 <pattern id="p1">
504 <rule context="item">
505 <assert id="a1" role="error" test="false()">item is invalid</assert>
506 </rule>
507 </pattern>
508 </schema>"#,
509 );
510
511 let reports = evaluate(&schema, &doc).unwrap();
512 assert_eq!(reports.len(), 1);
513 let report = &reports[0];
514 assert_eq!(report.pattern_id.as_deref(), Some("p1"));
515 assert_eq!(report.check_id.as_deref(), Some("a1"));
516 assert_eq!(report.role.as_deref(), Some("error"));
517 assert_eq!(report.kind, CheckKind::Assert);
518 assert_eq!(report.message, "item is invalid");
519 assert_eq!(report.node, doc.node(1));
520 }
521
522 #[test]
523 fn report_true_fires() {
524 let doc = doc_with_root_children(&[(None, "item")]);
525 let schema = schema(
526 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
527 <pattern>
528 <rule context="item">
529 <report test="true()">observed item</report>
530 </rule>
531 </pattern>
532 </schema>"#,
533 );
534
535 let reports = evaluate(&schema, &doc).unwrap();
536 assert_eq!(reports.len(), 1);
537 assert_eq!(reports[0].kind, CheckKind::Report);
538 assert_eq!(reports[0].message, "observed item");
539 }
540
541 #[test]
547 fn same_pattern_first_matching_rule_wins() {
548 let doc = doc_with_root_children(&[(None, "item")]);
549 let schema = schema(
550 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
551 <pattern>
552 <rule context="item">
553 <assert test="false()">RULE-ONE-FIRED</assert>
554 </rule>
555 <rule context="item">
556 <assert test="false()">RULE-TWO-FIRED</assert>
557 </rule>
558 </pattern>
559 </schema>"#,
560 );
561
562 let reports = evaluate(&schema, &doc).unwrap();
563 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
564 assert_eq!(messages, vec!["RULE-ONE-FIRED"]);
565 }
566
567 #[test]
571 fn independent_patterns_both_fire() {
572 let doc = doc_with_root_children(&[(None, "item")]);
573 let schema = schema(
574 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
575 <pattern>
576 <rule context="item">
577 <assert test="false()">PATTERN-ONE-FIRED</assert>
578 </rule>
579 </pattern>
580 <pattern>
581 <rule context="item">
582 <assert test="false()">PATTERN-TWO-FIRED</assert>
583 </rule>
584 </pattern>
585 </schema>"#,
586 );
587
588 let reports = evaluate(&schema, &doc).unwrap();
589 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
590 assert_eq!(messages, vec!["PATTERN-ONE-FIRED", "PATTERN-TWO-FIRED"]);
591 }
592
593 #[test]
600 fn namespace_binding_resolves_prefixed_context() {
601 let doc = doc_with_root_children(&[(Some("urn:test"), "item")]);
602 let schema = schema(
603 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
604 <ns prefix="t" uri="urn:test"/>
605 <pattern>
606 <rule context="t:item">
607 <assert test="false()">NS-MATCH</assert>
608 </rule>
609 </pattern>
610 </schema>"#,
611 );
612
613 let reports = evaluate(&schema, &doc).unwrap();
614 assert_eq!(reports.len(), 1);
615 assert_eq!(reports[0].message, "NS-MATCH");
616 }
617
618 #[test]
619 fn invalid_test_expression_is_a_parse_error_not_a_panic() {
620 let doc = doc_with_root_children(&[(None, "item")]);
621 let schema = schema(
622 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
623 <pattern>
624 <rule context="item">
625 <assert test="((">broken</assert>
626 </rule>
627 </pattern>
628 </schema>"#,
629 );
630
631 let result = evaluate(&schema, &doc);
632 assert!(matches!(result, Err(SchematronEvalError::Parse(_))));
633 }
634
635 #[test]
636 fn schema_level_let_is_visible_to_checks() {
637 let doc = doc_with_root_children(&[(None, "item")]);
638 let schema = schema(
639 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
640 <let name="x" value="'schema'"/>
641 <pattern>
642 <rule context="item">
643 <report test="$x = 'schema'">SCHEMA-LET</report>
644 </rule>
645 </pattern>
646 </schema>"#,
647 );
648
649 let reports = evaluate(&schema, &doc).unwrap();
650 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
651 assert_eq!(messages, vec!["SCHEMA-LET"]);
652 }
653
654 #[test]
659 fn pattern_level_let_shadows_schema_level_let() {
660 let doc = doc_with_root_children(&[(None, "item")]);
661 let schema = schema(
662 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
663 <let name="x" value="'schema'"/>
664 <pattern>
665 <let name="x" value="'pattern'"/>
666 <rule context="item">
667 <report test="$x = 'pattern'">PATTERN-WINS</report>
668 </rule>
669 </pattern>
670 </schema>"#,
671 );
672
673 let reports = evaluate(&schema, &doc).unwrap();
674 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
675 assert_eq!(messages, vec!["PATTERN-WINS"]);
676 }
677
678 #[test]
682 fn rule_level_let_is_reevaluated_per_context_node() {
683 let doc = doc_with_root_children(&[(None, "alpha"), (None, "beta")]);
684 let schema = schema(
685 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
686 <pattern>
687 <rule context="*">
688 <let name="n" value="local-name()"/>
689 <assert test="$n = 'alpha'">NOT-ALPHA</assert>
690 </rule>
691 </pattern>
692 </schema>"#,
693 );
694
695 let reports = evaluate(&schema, &doc).unwrap();
696 assert_eq!(reports.len(), 1);
697 assert_eq!(reports[0].message, "NOT-ALPHA");
698 assert_eq!(reports[0].node, doc.node(2));
699 }
700
701 #[test]
704 fn let_can_reference_earlier_let_in_same_scope() {
705 let doc = doc_with_root_children(&[(None, "item")]);
706 let schema = schema(
707 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
708 <let name="a" value="'A'"/>
709 <let name="b" value="concat($a, 'B')"/>
710 <pattern>
711 <rule context="item">
712 <report test="$b = 'AB'">SEQUENTIAL</report>
713 </rule>
714 </pattern>
715 </schema>"#,
716 );
717
718 let reports = evaluate(&schema, &doc).unwrap();
719 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
720 assert_eq!(messages, vec!["SEQUENTIAL"]);
721 }
722
723 #[test]
724 fn unbound_variable_is_an_eval_error_not_a_panic() {
725 let doc = doc_with_root_children(&[(None, "item")]);
726 let schema = schema(
727 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
728 <pattern>
729 <rule context="item">
730 <assert test="$missing">broken</assert>
731 </rule>
732 </pattern>
733 </schema>"#,
734 );
735
736 let result = evaluate(&schema, &doc);
737 assert!(matches!(result, Err(SchematronEvalError::Eval(_))));
738 }
739
740 #[test]
743 fn evaluate_with_phase_restricts_to_activated_patterns() {
744 let doc = doc_with_root_children(&[(None, "item")]);
745 let schema = schema(
746 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
747 <phase id="p1">
748 <active pattern="pa"/>
749 </phase>
750 <phase id="p2">
751 <active pattern="pb"/>
752 </phase>
753 <pattern id="pa">
754 <rule context="item">
755 <assert test="false()">PATTERN-A</assert>
756 </rule>
757 </pattern>
758 <pattern id="pb">
759 <rule context="item">
760 <assert test="false()">PATTERN-B</assert>
761 </rule>
762 </pattern>
763 </schema>"#,
764 );
765
766 let reports_p1 = evaluate_with_phase(&schema, &doc, Some("p1")).unwrap();
767 let messages_p1: Vec<&str> = reports_p1.iter().map(|r| r.message.as_str()).collect();
768 assert_eq!(messages_p1, vec!["PATTERN-A"]);
769
770 let reports_p2 = evaluate_with_phase(&schema, &doc, Some("p2")).unwrap();
771 let messages_p2: Vec<&str> = reports_p2.iter().map(|r| r.message.as_str()).collect();
772 assert_eq!(messages_p2, vec!["PATTERN-B"]);
773
774 let reports_all = evaluate_with_phase(&schema, &doc, None).unwrap();
775 let messages_all: Vec<&str> = reports_all.iter().map(|r| r.message.as_str()).collect();
776 assert_eq!(messages_all, vec!["PATTERN-A", "PATTERN-B"]);
777 }
778
779 #[test]
785 fn evaluate_honors_default_phase() {
786 let doc = doc_with_root_children(&[(None, "item")]);
787 let schema = schema(
788 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="p1">
789 <phase id="p1">
790 <active pattern="pa"/>
791 </phase>
792 <pattern id="pa">
793 <rule context="item">
794 <assert test="false()">PATTERN-A</assert>
795 </rule>
796 </pattern>
797 <pattern id="pb">
798 <rule context="item">
799 <assert test="false()">PATTERN-B</assert>
800 </rule>
801 </pattern>
802 </schema>"#,
803 );
804
805 let reports = evaluate(&schema, &doc).unwrap();
806 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
807 assert_eq!(messages, vec!["PATTERN-A"]);
808 }
809
810 #[test]
811 fn evaluate_with_phase_unknown_id_is_an_error() {
812 let doc = doc_with_root_children(&[(None, "item")]);
813 let schema = schema(
814 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
815 <pattern id="pa">
816 <rule context="item">
817 <assert test="false()">PATTERN-A</assert>
818 </rule>
819 </pattern>
820 </schema>"#,
821 );
822
823 let error = evaluate_with_phase(&schema, &doc, Some("does-not-exist")).unwrap_err();
824 assert_eq!(
825 error,
826 SchematronEvalError::UnknownPhase {
827 phase: "does-not-exist".to_owned(),
828 }
829 );
830 }
831
832 #[test]
833 fn phase_level_let_is_visible_within_that_phase() {
834 let doc = doc_with_root_children(&[(None, "item")]);
835 let schema = schema(
836 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
837 <phase id="p1">
838 <let name="x" value="'phase'"/>
839 <active pattern="pa"/>
840 </phase>
841 <pattern id="pa">
842 <rule context="item">
843 <report test="$x = 'phase'">PHASE-LET</report>
844 </rule>
845 </pattern>
846 </schema>"#,
847 );
848
849 let reports = evaluate_with_phase(&schema, &doc, Some("p1")).unwrap();
850 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
851 assert_eq!(messages, vec!["PHASE-LET"]);
852 }
853
854 #[test]
858 fn plain_text_message_is_trimmed_at_render_time() {
859 let doc = doc_with_root_children(&[(None, "item")]);
860 let schema = schema(
861 "<schema xmlns=\"http://purl.oclc.org/dsdl/schematron\">\
862 <pattern>\
863 <rule context=\"item\">\
864 <assert test=\"false()\">\n first second \n</assert>\
865 </rule>\
866 </pattern>\
867 </schema>",
868 );
869
870 let reports = evaluate(&schema, &doc).unwrap();
871 assert_eq!(reports[0].message, "first second");
872 }
873
874 #[test]
875 fn value_of_interpolates_the_context_node() {
876 let doc = doc_with_root_children(&[(None, "item")]);
877 let schema = schema(
878 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
879 <pattern>
880 <rule context="item">
881 <assert test="false()">Element: <value-of select="local-name()"/>.</assert>
882 </rule>
883 </pattern>
884 </schema>"#,
885 );
886
887 let reports = evaluate(&schema, &doc).unwrap();
888 assert_eq!(reports[0].message, "Element: item.");
889 }
890
891 #[test]
892 fn value_of_can_reference_a_let_variable() {
893 let doc = doc_with_root_children(&[(None, "item")]);
894 let schema = schema(
895 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
896 <pattern>
897 <rule context="item">
898 <let name="x" value="'bound-value'"/>
899 <assert test="false()">x is <value-of select="$x"/></assert>
900 </rule>
901 </pattern>
902 </schema>"#,
903 );
904
905 let reports = evaluate(&schema, &doc).unwrap();
906 assert_eq!(reports[0].message, "x is bound-value");
907 }
908
909 #[test]
912 fn broken_value_of_in_a_non_firing_check_is_not_an_error() {
913 let doc = doc_with_root_children(&[(None, "item")]);
914 let schema = schema(
915 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
916 <pattern>
917 <rule context="item">
918 <assert test="true()">broken: <value-of select="(("/></assert>
919 </rule>
920 </pattern>
921 </schema>"#,
922 );
923
924 let reports = evaluate(&schema, &doc).unwrap();
925 assert_eq!(reports.len(), 0);
926 }
927
928 #[test]
931 fn broken_value_of_in_a_firing_check_is_a_parse_error() {
932 let doc = doc_with_root_children(&[(None, "item")]);
933 let schema = schema(
934 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
935 <pattern>
936 <rule context="item">
937 <assert test="false()">broken: <value-of select="(("/></assert>
938 </rule>
939 </pattern>
940 </schema>"#,
941 );
942
943 let result = evaluate(&schema, &doc);
944 assert!(matches!(result, Err(SchematronEvalError::Parse(_))));
945 }
946
947 #[test]
952 fn extends_inlined_check_fires_during_evaluation() {
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>
957 <rule abstract="true" id="base">
958 <assert test="false()">FROM-BASE</assert>
959 </rule>
960 <rule context="item">
961 <extends rule="base"/>
962 </rule>
963 </pattern>
964 </schema>"#,
965 );
966
967 let reports = evaluate(&schema, &doc).unwrap();
968 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
969 assert_eq!(messages, vec!["FROM-BASE"]);
970 }
971
972 #[test]
975 fn is_a_instantiated_pattern_evaluates_with_substituted_context() {
976 let doc = doc_with_root_children(&[(None, "alpha"), (None, "beta")]);
977 let schema = schema(
978 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
979 <pattern abstract="true" id="tpl">
980 <rule context="$el">
981 <assert test="false()">MATCHED-$el</assert>
982 </rule>
983 </pattern>
984 <pattern is-a="tpl" id="concrete">
985 <param name="el" value="alpha"/>
986 </pattern>
987 </schema>"#,
988 );
989
990 let reports = evaluate(&schema, &doc).unwrap();
991 assert_eq!(reports.len(), 1);
992 assert_eq!(reports[0].message, "MATCHED-$el");
995 assert_eq!(reports[0].node, doc.node(1));
996 }
997
998 #[test]
999 fn fired_check_carries_its_rendered_diagnostics() {
1000 let doc = doc_with_root_children(&[(None, "item")]);
1001 let schema = schema(
1002 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1003 <diagnostics>
1004 <diagnostic id="d1">first</diagnostic>
1005 <diagnostic id="d2">element is <value-of select="local-name()"/></diagnostic>
1006 </diagnostics>
1007 <pattern>
1008 <rule context="item">
1009 <assert test="false()" diagnostics="d1 d2">broken</assert>
1010 </rule>
1011 </pattern>
1012 </schema>"#,
1013 );
1014
1015 let reports = evaluate(&schema, &doc).unwrap();
1016 assert_eq!(reports[0].diagnostics, vec!["first", "element is item"]);
1017 }
1018
1019 #[test]
1020 fn unknown_diagnostic_id_is_silently_skipped() {
1021 let schema_xml = r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1029 <diagnostics>
1030 <diagnostic id="d1">known</diagnostic>
1031 </diagnostics>
1032 <pattern>
1033 <rule context="item">
1034 <assert test="false()" diagnostics="d1 missing">broken</assert>
1035 </rule>
1036 </pattern>
1037 </schema>"#;
1038
1039 assert!(crate::parse(schema_xml).is_err());
1040 }
1041
1042 #[test]
1043 fn check_without_diagnostics_attribute_yields_an_empty_list() {
1044 let doc = doc_with_root_children(&[(None, "item")]);
1045 let schema = schema(
1046 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1047 <pattern>
1048 <rule context="item">
1049 <assert test="false()">broken</assert>
1050 </rule>
1051 </pattern>
1052 </schema>"#,
1053 );
1054
1055 let reports = evaluate(&schema, &doc).unwrap();
1056 assert_eq!(reports[0].diagnostics, Vec::<String>::new());
1057 }
1058}