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 extend_lets<'n, N: Node<'n>>(
231 vars: &mut HashMap<String, Value<N>>,
232 lets: &[LetBinding],
233 context_node: N,
234 ns_lookup: &dyn Fn(&str) -> Option<String>,
235) -> Result<(), SchematronEvalError> {
236 for binding in lets {
237 let expr = xpath_eval::parse(&binding.value).map_err(SchematronEvalError::Parse)?;
238 let value = {
239 let lookup = |name: &QName| {
240 if name.prefix.is_some() {
241 None
242 } else {
243 vars.get(&name.local).cloned()
244 }
245 };
246 let mut ctx = EvaluationContext::new(context_node);
247 ctx.namespaces = Some(ns_lookup);
248 ctx.variables = Some(&lookup);
249 xpath_eval::evaluate(&expr, &ctx).map_err(SchematronEvalError::Eval)?
250 };
251 vars.insert(binding.name.clone(), value);
252 }
253 Ok(())
254}
255
256fn evaluate_check<'n, N: Node<'n>>(
260 check: &Check,
261 node: N,
262 pattern_id: Option<String>,
263 ns_lookup: &dyn Fn(&str) -> Option<String>,
264 vars: &HashMap<String, Value<N>>,
265 diagnostics: &[Diagnostic],
266) -> Result<Option<Report<N>>, SchematronEvalError> {
267 let expr = xpath_eval::parse(&check.test).map_err(SchematronEvalError::Parse)?;
268 let lookup = |name: &QName| {
269 if name.prefix.is_some() {
270 None
271 } else {
272 vars.get(&name.local).cloned()
273 }
274 };
275 let mut ctx = EvaluationContext::new(node);
276 ctx.namespaces = Some(ns_lookup);
277 ctx.variables = Some(&lookup);
278 let value = xpath_eval::evaluate(&expr, &ctx).map_err(SchematronEvalError::Eval)?;
279 let boolean = value.to_boolean();
280
281 let fires = match check.kind {
282 CheckKind::Assert => !boolean,
283 CheckKind::Report => boolean,
284 };
285
286 if !fires {
287 return Ok(None);
288 }
289
290 let message = render_message(&check.message, node, ns_lookup, vars)?;
291
292 let rendered_diagnostics = check
293 .diagnostics
294 .iter()
295 .filter_map(|id| diagnostics.iter().find(|d| &d.id == id))
296 .map(|d| render_message(&d.message, node, ns_lookup, vars))
297 .collect::<Result<Vec<_>, _>>()?;
298
299 Ok(Some(Report {
300 pattern_id,
301 check_id: check.id.clone(),
302 role: check.role.clone(),
303 kind: check.kind,
304 message,
305 diagnostics: rendered_diagnostics,
306 node,
307 }))
308}
309
310fn render_message<'n, N: Node<'n>>(
320 parts: &[MessagePart],
321 node: N,
322 ns_lookup: &dyn Fn(&str) -> Option<String>,
323 vars: &HashMap<String, Value<N>>,
324) -> Result<String, SchematronEvalError> {
325 let mut message = String::new();
326 for part in parts {
327 match part {
328 MessagePart::Text(text) => message.push_str(text),
329 MessagePart::ValueOf(select) => {
330 let expr = xpath_eval::parse(select).map_err(SchematronEvalError::Parse)?;
331 let lookup = |name: &QName| {
332 if name.prefix.is_some() {
333 None
334 } else {
335 vars.get(&name.local).cloned()
336 }
337 };
338 let mut ctx = EvaluationContext::new(node);
339 ctx.namespaces = Some(ns_lookup);
340 ctx.variables = Some(&lookup);
341 let value = xpath_eval::evaluate(&expr, &ctx).map_err(SchematronEvalError::Eval)?;
342 message.push_str(&value.to_xpath_string());
343 }
344 }
345 }
346 Ok(message.trim().to_owned())
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use std::cmp::Ordering;
353 use xpath_eval::{ExpandedName, NodeKind};
354
355 #[derive(Debug)]
361 struct ElementData {
362 namespace_uri: Option<String>,
363 local_name: String,
364 parent: Option<usize>,
365 children: Vec<usize>,
366 }
367
368 #[derive(Debug)]
369 struct Arena {
370 elements: Vec<ElementData>,
371 }
372
373 struct TestDoc {
374 arena: Arena,
375 }
376
377 impl TestDoc {
378 fn node(&self, idx: usize) -> TestNode<'_> {
379 TestNode {
380 arena: &self.arena,
381 idx,
382 }
383 }
384 }
385
386 impl xpath_eval::Document for TestDoc {
387 type N<'a> = TestNode<'a>;
388
389 fn root(&self) -> Self::N<'_> {
390 self.node(0)
391 }
392 }
393
394 #[derive(Clone, Copy, Debug)]
395 struct TestNode<'a> {
396 arena: &'a Arena,
397 idx: usize,
398 }
399
400 impl<'a> PartialEq for TestNode<'a> {
401 fn eq(&self, other: &Self) -> bool {
402 std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
403 }
404 }
405
406 impl<'a> Eq for TestNode<'a> {}
407
408 impl<'a> Node<'a> for TestNode<'a> {
409 fn kind(self) -> NodeKind {
410 if self.idx == 0 {
411 NodeKind::Root
412 } else {
413 NodeKind::Element
414 }
415 }
416
417 fn parent(self) -> Option<Self> {
418 self.arena.elements[self.idx].parent.map(|p| TestNode {
419 arena: self.arena,
420 idx: p,
421 })
422 }
423
424 fn children(self) -> impl Iterator<Item = Self> + 'a {
425 let arena = self.arena;
426 self.arena.elements[self.idx]
427 .children
428 .clone()
429 .into_iter()
430 .map(move |i| TestNode { arena, idx: i })
431 }
432
433 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
434 std::iter::empty()
435 }
436
437 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
438 std::iter::empty()
439 }
440
441 fn expanded_name(self) -> Option<ExpandedName> {
442 if self.idx == 0 {
443 None
444 } else {
445 let element = &self.arena.elements[self.idx];
446 Some(ExpandedName {
447 namespace_uri: element.namespace_uri.clone(),
448 local_name: element.local_name.clone(),
449 })
450 }
451 }
452
453 fn string_value(self) -> String {
454 String::new()
455 }
456
457 fn document_order(self, other: Self) -> Ordering {
458 self.idx.cmp(&other.idx)
459 }
460 }
461
462 fn doc_with_root_children(children: &[(Option<&str>, &str)]) -> TestDoc {
465 let mut elements = vec![ElementData {
466 namespace_uri: None,
467 local_name: String::new(),
468 parent: None,
469 children: Vec::new(),
470 }];
471 for (namespace_uri, local_name) in children {
472 let idx = elements.len();
473 elements.push(ElementData {
474 namespace_uri: namespace_uri.map(str::to_owned),
475 local_name: (*local_name).to_owned(),
476 parent: Some(0),
477 children: Vec::new(),
478 });
479 elements[0].children.push(idx);
480 }
481 TestDoc {
482 arena: Arena { elements },
483 }
484 }
485
486 fn schema(sch_xml: &str) -> Schema {
487 crate::parse(sch_xml).expect("test schema must parse")
488 }
489
490 #[test]
491 fn assert_true_produces_no_report() {
492 let doc = doc_with_root_children(&[(None, "item")]);
493 let schema = schema(
494 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
495 <pattern>
496 <rule context="item">
497 <assert test="true()">should not fire</assert>
498 </rule>
499 </pattern>
500 </schema>"#,
501 );
502
503 let reports = evaluate(&schema, &doc).unwrap();
504 assert_eq!(reports.len(), 0);
505 }
506
507 #[test]
508 fn assert_false_produces_one_report() {
509 let doc = doc_with_root_children(&[(None, "item")]);
510 let schema = schema(
511 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
512 <pattern id="p1">
513 <rule context="item">
514 <assert id="a1" role="error" test="false()">item is invalid</assert>
515 </rule>
516 </pattern>
517 </schema>"#,
518 );
519
520 let reports = evaluate(&schema, &doc).unwrap();
521 assert_eq!(reports.len(), 1);
522 let report = &reports[0];
523 assert_eq!(report.pattern_id.as_deref(), Some("p1"));
524 assert_eq!(report.check_id.as_deref(), Some("a1"));
525 assert_eq!(report.role.as_deref(), Some("error"));
526 assert_eq!(report.kind, CheckKind::Assert);
527 assert_eq!(report.message, "item is invalid");
528 assert_eq!(report.node, doc.node(1));
529 }
530
531 #[test]
532 fn report_true_fires() {
533 let doc = doc_with_root_children(&[(None, "item")]);
534 let schema = schema(
535 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
536 <pattern>
537 <rule context="item">
538 <report test="true()">observed item</report>
539 </rule>
540 </pattern>
541 </schema>"#,
542 );
543
544 let reports = evaluate(&schema, &doc).unwrap();
545 assert_eq!(reports.len(), 1);
546 assert_eq!(reports[0].kind, CheckKind::Report);
547 assert_eq!(reports[0].message, "observed item");
548 }
549
550 #[test]
556 fn same_pattern_first_matching_rule_wins() {
557 let doc = doc_with_root_children(&[(None, "item")]);
558 let schema = schema(
559 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
560 <pattern>
561 <rule context="item">
562 <assert test="false()">RULE-ONE-FIRED</assert>
563 </rule>
564 <rule context="item">
565 <assert test="false()">RULE-TWO-FIRED</assert>
566 </rule>
567 </pattern>
568 </schema>"#,
569 );
570
571 let reports = evaluate(&schema, &doc).unwrap();
572 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
573 assert_eq!(messages, vec!["RULE-ONE-FIRED"]);
574 }
575
576 #[test]
580 fn independent_patterns_both_fire() {
581 let doc = doc_with_root_children(&[(None, "item")]);
582 let schema = schema(
583 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
584 <pattern>
585 <rule context="item">
586 <assert test="false()">PATTERN-ONE-FIRED</assert>
587 </rule>
588 </pattern>
589 <pattern>
590 <rule context="item">
591 <assert test="false()">PATTERN-TWO-FIRED</assert>
592 </rule>
593 </pattern>
594 </schema>"#,
595 );
596
597 let reports = evaluate(&schema, &doc).unwrap();
598 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
599 assert_eq!(messages, vec!["PATTERN-ONE-FIRED", "PATTERN-TWO-FIRED"]);
600 }
601
602 #[test]
609 fn namespace_binding_resolves_prefixed_context() {
610 let doc = doc_with_root_children(&[(Some("urn:test"), "item")]);
611 let schema = schema(
612 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
613 <ns prefix="t" uri="urn:test"/>
614 <pattern>
615 <rule context="t:item">
616 <assert test="false()">NS-MATCH</assert>
617 </rule>
618 </pattern>
619 </schema>"#,
620 );
621
622 let reports = evaluate(&schema, &doc).unwrap();
623 assert_eq!(reports.len(), 1);
624 assert_eq!(reports[0].message, "NS-MATCH");
625 }
626
627 #[test]
628 fn invalid_test_expression_is_a_parse_error_not_a_panic() {
629 let doc = doc_with_root_children(&[(None, "item")]);
630 let schema = schema(
631 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
632 <pattern>
633 <rule context="item">
634 <assert test="((">broken</assert>
635 </rule>
636 </pattern>
637 </schema>"#,
638 );
639
640 let result = evaluate(&schema, &doc);
641 assert!(matches!(result, Err(SchematronEvalError::Parse(_))));
642 }
643
644 #[test]
645 fn schema_level_let_is_visible_to_checks() {
646 let doc = doc_with_root_children(&[(None, "item")]);
647 let schema = schema(
648 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
649 <let name="x" value="'schema'"/>
650 <pattern>
651 <rule context="item">
652 <report test="$x = 'schema'">SCHEMA-LET</report>
653 </rule>
654 </pattern>
655 </schema>"#,
656 );
657
658 let reports = evaluate(&schema, &doc).unwrap();
659 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
660 assert_eq!(messages, vec!["SCHEMA-LET"]);
661 }
662
663 #[test]
668 fn pattern_level_let_shadows_schema_level_let() {
669 let doc = doc_with_root_children(&[(None, "item")]);
670 let schema = schema(
671 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
672 <let name="x" value="'schema'"/>
673 <pattern>
674 <let name="x" value="'pattern'"/>
675 <rule context="item">
676 <report test="$x = 'pattern'">PATTERN-WINS</report>
677 </rule>
678 </pattern>
679 </schema>"#,
680 );
681
682 let reports = evaluate(&schema, &doc).unwrap();
683 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
684 assert_eq!(messages, vec!["PATTERN-WINS"]);
685 }
686
687 #[test]
691 fn rule_level_let_is_reevaluated_per_context_node() {
692 let doc = doc_with_root_children(&[(None, "alpha"), (None, "beta")]);
693 let schema = schema(
694 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
695 <pattern>
696 <rule context="*">
697 <let name="n" value="local-name()"/>
698 <assert test="$n = 'alpha'">NOT-ALPHA</assert>
699 </rule>
700 </pattern>
701 </schema>"#,
702 );
703
704 let reports = evaluate(&schema, &doc).unwrap();
705 assert_eq!(reports.len(), 1);
706 assert_eq!(reports[0].message, "NOT-ALPHA");
707 assert_eq!(reports[0].node, doc.node(2));
708 }
709
710 #[test]
713 fn let_can_reference_earlier_let_in_same_scope() {
714 let doc = doc_with_root_children(&[(None, "item")]);
715 let schema = schema(
716 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
717 <let name="a" value="'A'"/>
718 <let name="b" value="concat($a, 'B')"/>
719 <pattern>
720 <rule context="item">
721 <report test="$b = 'AB'">SEQUENTIAL</report>
722 </rule>
723 </pattern>
724 </schema>"#,
725 );
726
727 let reports = evaluate(&schema, &doc).unwrap();
728 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
729 assert_eq!(messages, vec!["SEQUENTIAL"]);
730 }
731
732 #[test]
733 fn unbound_variable_is_an_eval_error_not_a_panic() {
734 let doc = doc_with_root_children(&[(None, "item")]);
735 let schema = schema(
736 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
737 <pattern>
738 <rule context="item">
739 <assert test="$missing">broken</assert>
740 </rule>
741 </pattern>
742 </schema>"#,
743 );
744
745 let result = evaluate(&schema, &doc);
746 assert!(matches!(result, Err(SchematronEvalError::Eval(_))));
747 }
748
749 #[test]
752 fn evaluate_with_phase_restricts_to_activated_patterns() {
753 let doc = doc_with_root_children(&[(None, "item")]);
754 let schema = schema(
755 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
756 <phase id="p1">
757 <active pattern="pa"/>
758 </phase>
759 <phase id="p2">
760 <active pattern="pb"/>
761 </phase>
762 <pattern id="pa">
763 <rule context="item">
764 <assert test="false()">PATTERN-A</assert>
765 </rule>
766 </pattern>
767 <pattern id="pb">
768 <rule context="item">
769 <assert test="false()">PATTERN-B</assert>
770 </rule>
771 </pattern>
772 </schema>"#,
773 );
774
775 let reports_p1 = evaluate_with_phase(&schema, &doc, Some("p1")).unwrap();
776 let messages_p1: Vec<&str> = reports_p1.iter().map(|r| r.message.as_str()).collect();
777 assert_eq!(messages_p1, vec!["PATTERN-A"]);
778
779 let reports_p2 = evaluate_with_phase(&schema, &doc, Some("p2")).unwrap();
780 let messages_p2: Vec<&str> = reports_p2.iter().map(|r| r.message.as_str()).collect();
781 assert_eq!(messages_p2, vec!["PATTERN-B"]);
782
783 let reports_all = evaluate_with_phase(&schema, &doc, None).unwrap();
784 let messages_all: Vec<&str> = reports_all.iter().map(|r| r.message.as_str()).collect();
785 assert_eq!(messages_all, vec!["PATTERN-A", "PATTERN-B"]);
786 }
787
788 #[test]
794 fn evaluate_honors_default_phase() {
795 let doc = doc_with_root_children(&[(None, "item")]);
796 let schema = schema(
797 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="p1">
798 <phase id="p1">
799 <active pattern="pa"/>
800 </phase>
801 <pattern id="pa">
802 <rule context="item">
803 <assert test="false()">PATTERN-A</assert>
804 </rule>
805 </pattern>
806 <pattern id="pb">
807 <rule context="item">
808 <assert test="false()">PATTERN-B</assert>
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-A"]);
817 }
818
819 #[test]
820 fn evaluate_with_phase_unknown_id_is_an_error() {
821 let doc = doc_with_root_children(&[(None, "item")]);
822 let schema = schema(
823 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
824 <pattern id="pa">
825 <rule context="item">
826 <assert test="false()">PATTERN-A</assert>
827 </rule>
828 </pattern>
829 </schema>"#,
830 );
831
832 let error = evaluate_with_phase(&schema, &doc, Some("does-not-exist")).unwrap_err();
833 assert_eq!(
834 error,
835 SchematronEvalError::UnknownPhase {
836 phase: "does-not-exist".to_owned(),
837 }
838 );
839 }
840
841 #[test]
842 fn phase_level_let_is_visible_within_that_phase() {
843 let doc = doc_with_root_children(&[(None, "item")]);
844 let schema = schema(
845 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
846 <phase id="p1">
847 <let name="x" value="'phase'"/>
848 <active pattern="pa"/>
849 </phase>
850 <pattern id="pa">
851 <rule context="item">
852 <report test="$x = 'phase'">PHASE-LET</report>
853 </rule>
854 </pattern>
855 </schema>"#,
856 );
857
858 let reports = evaluate_with_phase(&schema, &doc, Some("p1")).unwrap();
859 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
860 assert_eq!(messages, vec!["PHASE-LET"]);
861 }
862
863 #[test]
867 fn plain_text_message_is_trimmed_at_render_time() {
868 let doc = doc_with_root_children(&[(None, "item")]);
869 let schema = schema(
870 "<schema xmlns=\"http://purl.oclc.org/dsdl/schematron\">\
871 <pattern>\
872 <rule context=\"item\">\
873 <assert test=\"false()\">\n first second \n</assert>\
874 </rule>\
875 </pattern>\
876 </schema>",
877 );
878
879 let reports = evaluate(&schema, &doc).unwrap();
880 assert_eq!(reports[0].message, "first second");
881 }
882
883 #[test]
884 fn value_of_interpolates_the_context_node() {
885 let doc = doc_with_root_children(&[(None, "item")]);
886 let schema = schema(
887 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
888 <pattern>
889 <rule context="item">
890 <assert test="false()">Element: <value-of select="local-name()"/>.</assert>
891 </rule>
892 </pattern>
893 </schema>"#,
894 );
895
896 let reports = evaluate(&schema, &doc).unwrap();
897 assert_eq!(reports[0].message, "Element: item.");
898 }
899
900 #[test]
901 fn value_of_can_reference_a_let_variable() {
902 let doc = doc_with_root_children(&[(None, "item")]);
903 let schema = schema(
904 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
905 <pattern>
906 <rule context="item">
907 <let name="x" value="'bound-value'"/>
908 <assert test="false()">x is <value-of select="$x"/></assert>
909 </rule>
910 </pattern>
911 </schema>"#,
912 );
913
914 let reports = evaluate(&schema, &doc).unwrap();
915 assert_eq!(reports[0].message, "x is bound-value");
916 }
917
918 #[test]
921 fn broken_value_of_in_a_non_firing_check_is_not_an_error() {
922 let doc = doc_with_root_children(&[(None, "item")]);
923 let schema = schema(
924 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
925 <pattern>
926 <rule context="item">
927 <assert test="true()">broken: <value-of select="(("/></assert>
928 </rule>
929 </pattern>
930 </schema>"#,
931 );
932
933 let reports = evaluate(&schema, &doc).unwrap();
934 assert_eq!(reports.len(), 0);
935 }
936
937 #[test]
940 fn broken_value_of_in_a_firing_check_is_a_parse_error() {
941 let doc = doc_with_root_children(&[(None, "item")]);
942 let schema = schema(
943 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
944 <pattern>
945 <rule context="item">
946 <assert test="false()">broken: <value-of select="(("/></assert>
947 </rule>
948 </pattern>
949 </schema>"#,
950 );
951
952 let result = evaluate(&schema, &doc);
953 assert!(matches!(result, Err(SchematronEvalError::Parse(_))));
954 }
955
956 #[test]
961 fn extends_inlined_check_fires_during_evaluation() {
962 let doc = doc_with_root_children(&[(None, "item")]);
963 let schema = schema(
964 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
965 <pattern>
966 <rule abstract="true" id="base">
967 <assert test="false()">FROM-BASE</assert>
968 </rule>
969 <rule context="item">
970 <extends rule="base"/>
971 </rule>
972 </pattern>
973 </schema>"#,
974 );
975
976 let reports = evaluate(&schema, &doc).unwrap();
977 let messages: Vec<&str> = reports.iter().map(|r| r.message.as_str()).collect();
978 assert_eq!(messages, vec!["FROM-BASE"]);
979 }
980
981 #[test]
984 fn is_a_instantiated_pattern_evaluates_with_substituted_context() {
985 let doc = doc_with_root_children(&[(None, "alpha"), (None, "beta")]);
986 let schema = schema(
987 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
988 <pattern abstract="true" id="tpl">
989 <rule context="$el">
990 <assert test="false()">MATCHED-$el</assert>
991 </rule>
992 </pattern>
993 <pattern is-a="tpl" id="concrete">
994 <param name="el" value="alpha"/>
995 </pattern>
996 </schema>"#,
997 );
998
999 let reports = evaluate(&schema, &doc).unwrap();
1000 assert_eq!(reports.len(), 1);
1001 assert_eq!(reports[0].message, "MATCHED-$el");
1004 assert_eq!(reports[0].node, doc.node(1));
1005 }
1006
1007 #[test]
1008 fn fired_check_carries_its_rendered_diagnostics() {
1009 let doc = doc_with_root_children(&[(None, "item")]);
1010 let schema = schema(
1011 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1012 <diagnostics>
1013 <diagnostic id="d1">first</diagnostic>
1014 <diagnostic id="d2">element is <value-of select="local-name()"/></diagnostic>
1015 </diagnostics>
1016 <pattern>
1017 <rule context="item">
1018 <assert test="false()" diagnostics="d1 d2">broken</assert>
1019 </rule>
1020 </pattern>
1021 </schema>"#,
1022 );
1023
1024 let reports = evaluate(&schema, &doc).unwrap();
1025 assert_eq!(reports[0].diagnostics, vec!["first", "element is item"]);
1026 }
1027
1028 #[test]
1029 fn unknown_diagnostic_id_is_silently_skipped() {
1030 let schema_xml = r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1038 <diagnostics>
1039 <diagnostic id="d1">known</diagnostic>
1040 </diagnostics>
1041 <pattern>
1042 <rule context="item">
1043 <assert test="false()" diagnostics="d1 missing">broken</assert>
1044 </rule>
1045 </pattern>
1046 </schema>"#;
1047
1048 assert!(crate::parse(schema_xml).is_err());
1049 }
1050
1051 #[test]
1052 fn check_without_diagnostics_attribute_yields_an_empty_list() {
1053 let doc = doc_with_root_children(&[(None, "item")]);
1054 let schema = schema(
1055 r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
1056 <pattern>
1057 <rule context="item">
1058 <assert test="false()">broken</assert>
1059 </rule>
1060 </pattern>
1061 </schema>"#,
1062 );
1063
1064 let reports = evaluate(&schema, &doc).unwrap();
1065 assert_eq!(reports[0].diagnostics, Vec::<String>::new());
1066 }
1067}