#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::xpath::XPathVersion;
pub const SCHEMATRON_NAMESPACE: &str = "http://purl.oclc.org/dsdl/schematron";
pub const SCHEMATRON_1_5_NAMESPACE: &str = "http://www.ascc.net/xml/schematron";
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum QueryBinding {
#[default]
Default,
Xslt,
Xpath,
Xslt2,
Xpath2,
Xslt3,
Xpath3,
Other(String),
}
impl QueryBinding {
#[must_use]
pub fn parse(value: &str) -> QueryBinding {
match value.to_ascii_lowercase().as_str() {
"xslt" => QueryBinding::Xslt,
"xpath" => QueryBinding::Xpath,
"xslt2" => QueryBinding::Xslt2,
"xpath2" => QueryBinding::Xpath2,
"xslt3" => QueryBinding::Xslt3,
"xpath3" => QueryBinding::Xpath3,
other => QueryBinding::Other(other.to_string()),
}
}
#[must_use]
pub const fn version(&self) -> XPathVersion {
match self {
QueryBinding::Xslt2 | QueryBinding::Xpath2 => XPathVersion::V2,
QueryBinding::Xslt3 | QueryBinding::Xpath3 => XPathVersion::V3,
_ => XPathVersion::V1,
}
}
#[must_use]
pub const fn is_supported(&self) -> bool {
matches!(
self,
QueryBinding::Default
| QueryBinding::Xslt
| QueryBinding::Xpath
| QueryBinding::Xslt2
| QueryBinding::Xpath2
| QueryBinding::Xslt3
| QueryBinding::Xpath3
)
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
QueryBinding::Default => None,
QueryBinding::Xslt => Some("xslt"),
QueryBinding::Xpath => Some("xpath"),
QueryBinding::Xslt2 => Some("xslt2"),
QueryBinding::Xpath2 => Some("xpath2"),
QueryBinding::Xslt3 => Some("xslt3"),
QueryBinding::Xpath3 => Some("xpath3"),
QueryBinding::Other(name) => Some(name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Ns {
pub prefix: String,
pub uri: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Key {
pub name: String,
pub match_pattern: String,
pub use_expression: String,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Let {
pub name: String,
pub value: LetValue,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum LetValue {
Expression(String),
Content(Vec<Content>),
}
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Phase {
pub id: String,
pub actives: Vec<String>,
pub lets: Vec<Let>,
pub paragraphs: Vec<Paragraph>,
}
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Pattern {
pub id: Option<String>,
pub is_abstract: bool,
pub is_a: Option<String>,
pub documents: Option<String>,
pub title: Option<String>,
pub lets: Vec<Let>,
pub rules: Vec<Rule>,
pub params: Vec<Param>,
pub paragraphs: Vec<Paragraph>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Param {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Rule {
pub context: Option<String>,
pub id: Option<String>,
pub is_abstract: bool,
pub flag: Option<String>,
pub role: Option<String>,
pub subject: Option<String>,
pub lets: Vec<Let>,
pub body: Vec<RuleChild>,
}
impl Rule {
pub fn assertions(&self) -> impl Iterator<Item = &Assertion> {
self.body.iter().filter_map(|child| match child {
RuleChild::Assertion(assertion) => Some(assertion),
RuleChild::Extends(_) => None,
})
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum RuleChild {
Assertion(Assertion),
Extends(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum AssertionKind {
Assert,
Report,
}
impl AssertionKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
AssertionKind::Assert => "assert",
AssertionKind::Report => "report",
}
}
#[must_use]
pub const fn is_reported(self, outcome: bool) -> bool {
match self {
AssertionKind::Assert => !outcome,
AssertionKind::Report => outcome,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Assertion {
pub kind: AssertionKind,
pub test: String,
pub id: Option<String>,
pub flag: Option<String>,
pub role: Option<String>,
pub subject: Option<String>,
pub diagnostics: Vec<String>,
pub properties: Vec<String>,
pub see: Option<String>,
pub icon: Option<String>,
pub fpi: Option<String>,
pub content: Vec<Content>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Content {
Text(String),
ValueOf {
select: String,
},
Name {
path: Option<String>,
},
Emph(Vec<Content>),
Span {
class: Option<String>,
content: Vec<Content>,
},
Dir {
value: Option<String>,
content: Vec<Content>,
},
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Diagnostic {
pub id: String,
pub lang: Option<String>,
pub content: Vec<Content>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Property {
pub id: String,
pub role: Option<String>,
pub scheme: Option<String>,
pub content: Vec<Content>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Paragraph {
pub id: Option<String>,
pub class: Option<String>,
pub icon: Option<String>,
pub content: Vec<Content>,
}
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SchemaModel {
pub id: Option<String>,
pub title: Option<String>,
pub schema_version: Option<String>,
pub default_phase: Option<String>,
pub query_binding: QueryBinding,
pub lang: Option<String>,
pub namespaces: Vec<Ns>,
pub lets: Vec<Let>,
pub phases: Vec<Phase>,
pub patterns: Vec<Pattern>,
pub keys: Vec<Key>,
pub diagnostics: Vec<Diagnostic>,
pub properties: Vec<Property>,
pub paragraphs: Vec<Paragraph>,
}
impl SchemaModel {
#[must_use]
pub fn pattern(&self, id: &str) -> Option<&Pattern> {
self.patterns
.iter()
.find(|p| p.id.as_deref() == Some(id))
}
#[must_use]
pub fn key(&self, name: &str) -> Option<&Key> {
self.keys.iter().find(|key| key.name == name)
}
#[must_use]
pub fn phase(&self, id: &str) -> Option<&Phase> {
self.phases.iter().find(|p| p.id == id)
}
#[must_use]
pub fn diagnostic(&self, id: &str) -> Option<&Diagnostic> {
self.diagnostics.iter().find(|d| d.id == id)
}
#[must_use]
pub fn property(&self, id: &str) -> Option<&Property> {
self.properties.iter().find(|p| p.id == id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assert_reports_on_false_and_report_on_true() {
assert!(AssertionKind::Assert.is_reported(false));
assert!(!AssertionKind::Assert.is_reported(true));
assert!(AssertionKind::Report.is_reported(true));
assert!(!AssertionKind::Report.is_reported(false));
}
#[test]
fn query_binding_recognises_the_supported_bindings() {
for binding in ["xslt", "xpath", "xslt2", "xpath2", "xslt3", "xpath3"] {
assert!(
QueryBinding::parse(binding).is_supported(),
"{binding} should be supported"
);
}
assert!(QueryBinding::Default.is_supported());
}
#[test]
fn bindings_above_xpath_three_are_still_refused() {
for binding in ["xpath31", "xslt31"] {
assert!(
!QueryBinding::parse(binding).is_supported(),
"{binding} should be refused"
);
}
}
#[test]
fn the_binding_selects_the_xpath_version() {
assert_eq!(QueryBinding::Default.version(), XPathVersion::V1);
assert_eq!(QueryBinding::parse("xslt").version(), XPathVersion::V1);
assert_eq!(QueryBinding::parse("xpath").version(), XPathVersion::V1);
assert_eq!(QueryBinding::parse("xslt2").version(), XPathVersion::V2);
assert_eq!(QueryBinding::parse("xpath2").version(), XPathVersion::V2);
assert_eq!(QueryBinding::parse("xslt3").version(), XPathVersion::V3);
assert_eq!(QueryBinding::parse("xpath3").version(), XPathVersion::V3);
}
#[test]
fn query_binding_is_case_insensitive() {
assert_eq!(QueryBinding::parse("XSLT"), QueryBinding::Xslt);
}
#[test]
fn rule_assertions_skips_unexpanded_extends() {
let rule = Rule {
body: vec![
RuleChild::Extends("other".into()),
RuleChild::Assertion(Assertion {
kind: AssertionKind::Assert,
test: "true()".into(),
id: None,
flag: None,
role: None,
subject: None,
diagnostics: vec![],
properties: vec![],
see: None,
icon: None,
fpi: None,
content: vec![],
}),
],
..Rule::default()
};
assert_eq!(rule.assertions().count(), 1);
}
}