use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use super::expand::expand;
use super::include::resolve_includes;
use super::model::{
Assertion, Content, Let, LetValue, Pattern, QueryBinding, Rule, SchemaModel,
};
use super::parse::parse_schema;
use super::resolver::{FileResolver, SharedResolver};
use crate::error::{Error, Result};
use crate::xml::Document;
use crate::xpath::{
check_function, Axis, BinaryOp, Expr, NameTest, Namespaces, NodeTest, PathStart, XPathVersion,
};
#[derive(Clone)]
pub struct SchemaOptions {
pub base_uri: Option<String>,
pub resolver: SharedResolver,
pub max_include_depth: usize,
pub allow_unknown_query_binding: bool,
}
impl std::fmt::Debug for SchemaOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SchemaOptions")
.field("base_uri", &self.base_uri)
.field("max_include_depth", &self.max_include_depth)
.field(
"allow_unknown_query_binding",
&self.allow_unknown_query_binding,
)
.finish_non_exhaustive()
}
}
impl Default for SchemaOptions {
fn default() -> Self {
Self {
base_uri: None,
resolver: Arc::new(FileResolver::new()),
max_include_depth: 64,
allow_unknown_query_binding: false,
}
}
}
impl SchemaOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_base_uri(mut self, uri: impl Into<String>) -> Self {
self.base_uri = Some(uri.into());
self
}
#[must_use]
pub fn with_resolver(mut self, resolver: SharedResolver) -> Self {
self.resolver = resolver;
self
}
#[must_use]
pub fn with_max_include_depth(mut self, depth: usize) -> Self {
self.max_include_depth = depth;
self
}
#[must_use]
pub fn with_allow_unknown_query_binding(mut self, allow: bool) -> Self {
self.allow_unknown_query_binding = allow;
self
}
}
#[derive(Debug, Clone)]
pub struct Schema {
pub(crate) model: SchemaModel,
pub(crate) expressions: HashMap<String, Expr>,
pub(crate) namespaces: Namespaces,
pub(crate) resolver: SharedResolver,
pub(crate) version: XPathVersion,
pub(crate) uses_document_function: bool,
pub(crate) streaming_ineligible_reason: Option<String>,
}
impl Schema {
#[allow(clippy::should_implement_trait)] pub fn from_str(source: &str) -> Result<Schema> {
Schema::from_str_with(source, &SchemaOptions::default())
}
pub fn from_str_with(source: &str, options: &SchemaOptions) -> Result<Schema> {
let mut document = Document::from_str(source)?;
if let Some(base) = &options.base_uri {
document.set_base_uri(base.clone());
}
Schema::from_document(&document, options)
}
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Schema> {
let path = path.as_ref();
let source = std::fs::read_to_string(path).map_err(|source| Error::Io {
path: path.display().to_string(),
source,
})?;
let options = SchemaOptions::new().with_base_uri(path.display().to_string());
Schema::from_str_with(&source, &options)
}
pub fn from_document(document: &Document, options: &SchemaOptions) -> Result<Schema> {
let resolved = resolve_includes(
document,
options.resolver.as_ref(),
options.max_include_depth,
)?;
let mut model = parse_schema(&resolved)?;
if !model.query_binding.is_supported() && !options.allow_unknown_query_binding {
return Err(Error::UnsupportedQueryBinding {
binding: model
.query_binding
.as_str()
.unwrap_or_default()
.to_string(),
});
}
let version = model.query_binding.version();
expand(&mut model)?;
let mut namespaces = Namespaces::new();
for ns in &model.namespaces {
namespaces.insert(ns.prefix.clone(), ns.uri.clone());
}
let mut schema = Schema {
model,
expressions: HashMap::new(),
namespaces,
resolver: Arc::clone(&options.resolver),
version,
uses_document_function: false,
streaming_ineligible_reason: None,
};
schema.compile_expressions()?;
schema.check_references()?;
schema.uses_document_function = schema
.expressions
.values()
.any(calls_document_function);
schema.streaming_ineligible_reason = streaming_ineligibility_reason(&schema);
Ok(schema)
}
fn compile_expressions(&mut self) -> Result<()> {
let mut work: Vec<(String, String, bool)> = Vec::new();
for binding in &self.model.lets {
collect_let(binding, "schema", &mut work);
}
for phase in &self.model.phases {
let location = format!("phase[@id='{}']", phase.id);
for binding in &phase.lets {
collect_let(binding, &location, &mut work);
}
}
for (index, pattern) in self.model.patterns.iter().enumerate() {
collect_pattern(pattern, index, &mut work);
}
for key in &self.model.keys {
let location = format!("key[@name='{}']", key.name);
work.push((
key.match_pattern.clone(),
format!("{location}/@match"),
true,
));
work.push((
key.use_expression.clone(),
format!("{location}/@use"),
false,
));
}
for diagnostic in &self.model.diagnostics {
let location = format!("diagnostic[@id='{}']", diagnostic.id);
collect_content(&diagnostic.content, &location, &mut work);
}
for property in &self.model.properties {
let location = format!("property[@id='{}']", property.id);
collect_content(&property.content, &location, &mut work);
}
let bindable = self.bindable_names();
for (source, location, is_pattern) in work {
let expr = self.compile_one(&source, &location)?;
Schema::check_variables(&expr, &source, &location, &bindable, &mut Vec::new())?;
if is_pattern {
check_match_pattern(&expr, &source, &location)?;
}
self.expressions.insert(source, expr);
}
Ok(())
}
fn bindable_names(&self) -> std::collections::HashSet<String> {
let model = &self.model;
let mut names: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut add = |lets: &[Let]| {
for binding in lets {
names.insert(binding.name.clone());
}
};
add(&model.lets);
for phase in &model.phases {
add(&phase.lets);
}
for pattern in &model.patterns {
add(&pattern.lets);
for rule in &pattern.rules {
add(&rule.lets);
}
}
names
}
fn check_variables(
expr: &Expr,
source: &str,
location: &str,
bindable: &std::collections::HashSet<String>,
enclosing: &mut Vec<String>,
) -> Result<()> {
match expr {
Expr::Literal(_) | Expr::Number(_, _) | Expr::NamedFunctionRef { .. } => {}
Expr::Variable(name) => {
let name = name.to_string();
if bindable.contains(&name) || enclosing.contains(&name) {
return Ok(());
}
let mut known: Vec<&str> =
bindable.iter().map(String::as_str).collect();
known.extend(enclosing.iter().map(String::as_str));
known.sort_unstable();
return Err(Error::xpath_syntax(
location,
source,
0,
format!(
"no <let> anywhere in this schema binds ${name}{}",
if known.is_empty() {
"; the schema declares no variables".to_string()
} else {
format!("; it declares: {}", known.join(", "))
}
),
));
}
Expr::Negate(inner) => {
Schema::check_variables(inner, source, location, bindable, enclosing)?;
}
Expr::Binary(_, left, right) => {
Schema::check_variables(left, source, location, bindable, enclosing)?;
Schema::check_variables(right, source, location, bindable, enclosing)?;
}
Expr::Function { args, .. } => {
for arg in args {
Schema::check_variables(arg, source, location, bindable, enclosing)?;
}
}
Expr::TypeOp { value, .. } => {
Schema::check_variables(value, source, location, bindable, enclosing)?;
}
Expr::Sequence(members) => {
for member in members {
Schema::check_variables(member, source, location, bindable, enclosing)?;
}
}
Expr::Range(from, to) => {
Schema::check_variables(from, source, location, bindable, enclosing)?;
Schema::check_variables(to, source, location, bindable, enclosing)?;
}
Expr::If {
condition,
then_branch,
else_branch,
} => {
Schema::check_variables(condition, source, location, bindable, enclosing)?;
Schema::check_variables(then_branch, source, location, bindable, enclosing)?;
Schema::check_variables(else_branch, source, location, bindable, enclosing)?;
}
Expr::For {
variable,
input,
body,
} => {
Schema::check_binding(input, variable, body, source, location, bindable, enclosing)?;
}
Expr::Let {
variable,
value,
body,
} => {
Schema::check_binding(value, variable, body, source, location, bindable, enclosing)?;
}
Expr::Quantified {
variable,
input,
test,
..
} => {
Schema::check_binding(input, variable, test, source, location, bindable, enclosing)?;
}
Expr::InlineFunction { .. } | Expr::DynamicCall { .. } | Expr::Arrow(_) => {
Schema::check_variables_v3(expr, source, location, bindable, enclosing)?;
}
Expr::Path(path) => {
if let PathStart::Expr(start, predicates) = &path.start {
Schema::check_variables(start, source, location, bindable, enclosing)?;
for predicate in predicates {
Schema::check_variables(predicate, source, location, bindable, enclosing)?;
}
}
for step in &path.steps {
for predicate in &step.predicates {
Schema::check_variables(predicate, source, location, bindable, enclosing)?;
}
}
}
}
Ok(())
}
fn check_variables_v3(
expr: &Expr,
source: &str,
location: &str,
bindable: &std::collections::HashSet<String>,
enclosing: &mut Vec<String>,
) -> Result<()> {
match expr {
Expr::InlineFunction { params, body } => {
let pushed = params.len();
enclosing.extend(params.iter().cloned());
let checked = Schema::check_variables(body, source, location, bindable, enclosing);
enclosing.truncate(enclosing.len() - pushed);
checked
}
Expr::DynamicCall { function, args } => {
Schema::check_variables(function, source, location, bindable, enclosing)?;
for arg in args {
Schema::check_variables(arg, source, location, bindable, enclosing)?;
}
Ok(())
}
Expr::Arrow(called) => {
Schema::check_variables(called, source, location, bindable, enclosing)
}
_ => unreachable!("caller matches only these three XPath 3.0 variants"),
}
}
fn check_binding(
outer: &Expr,
variable: &NameTest,
inner: &Expr,
source: &str,
location: &str,
bindable: &std::collections::HashSet<String>,
enclosing: &mut Vec<String>,
) -> Result<()> {
Schema::check_variables(outer, source, location, bindable, enclosing)?;
enclosing.push(variable.to_string());
let checked = Schema::check_variables(inner, source, location, bindable, enclosing);
enclosing.pop();
checked
}
fn compile_one(&self, source: &str, location: &str) -> Result<Expr> {
let expr = crate::xpath::parse(source).map_err(|error| {
Error::xpath_syntax(location, source, error.position, error.message)
})?;
self.check_expression(&expr, source, location)?;
Ok(expr)
}
fn check_expression(&self, expr: &Expr, source: &str, location: &str) -> Result<()> {
match expr {
Expr::Literal(_) | Expr::Number(_, _) | Expr::Variable(_) => {}
Expr::Negate(inner) => self.check_expression(inner, source, location)?,
Expr::Binary(op, left, right) => {
self.check_binary_op_version(*op, source, location)?;
self.check_expression(left, source, location)?;
self.check_expression(right, source, location)?;
}
Expr::Function { name, args } => {
check_function(name, args.len(), self.version).map_err(|message| {
Error::xpath_syntax(location, source, 0, message)
})?;
check_literal_regex(name, args, source, location)?;
for arg in args {
self.check_expression(arg, source, location)?;
}
}
Expr::If {
condition,
then_branch,
else_branch,
} => {
self.require_v2("`if (…) then … else …`", source, location)?;
self.check_expression(condition, source, location)?;
self.check_expression(then_branch, source, location)?;
self.check_expression(else_branch, source, location)?;
}
Expr::TypeOp { op, value, .. } => {
self.require_v2(&format!("`{}`", op.as_str()), source, location)?;
self.check_expression(value, source, location)?;
}
Expr::Sequence(members) => {
self.require_v2("a sequence written with `,`", source, location)?;
for member in members {
self.check_expression(member, source, location)?;
}
}
Expr::Range(from, to) => {
self.require_v2("the `to` range operator", source, location)?;
self.check_expression(from, source, location)?;
self.check_expression(to, source, location)?;
}
Expr::For { input, body, .. } => {
self.require_v2("`for … in … return …`", source, location)?;
self.check_expression(input, source, location)?;
self.check_expression(body, source, location)?;
}
Expr::Let { value, body, .. } => {
self.require_v3("`let $v := … return …`", source, location)?;
self.check_expression(value, source, location)?;
self.check_expression(body, source, location)?;
}
Expr::Quantified {
quantifier,
input,
test,
..
} => {
self.require_v2(
&format!("`{} … in … satisfies …`", quantifier.as_str()),
source,
location,
)?;
self.check_expression(input, source, location)?;
self.check_expression(test, source, location)?;
}
Expr::InlineFunction { .. } | Expr::NamedFunctionRef { .. } | Expr::DynamicCall { .. } => {
self.check_v3_expression(expr, source, location)?;
}
Expr::Arrow(called) => {
self.require_v3("the arrow operator `=>`", source, location)?;
self.check_expression(called, source, location)?;
}
Expr::Path(path) => {
if let PathStart::Expr(start, predicates) = &path.start {
self.check_expression(start, source, location)?;
for predicate in predicates {
self.check_expression(predicate, source, location)?;
}
}
for step in &path.steps {
self.check_node_test(&step.node_test, source, location)?;
for predicate in &step.predicates {
self.check_expression(predicate, source, location)?;
}
}
}
}
Ok(())
}
fn check_v3_expression(&self, expr: &Expr, source: &str, location: &str) -> Result<()> {
match expr {
Expr::InlineFunction { body, .. } => {
self.require_v3("an inline function expression", source, location)?;
self.check_expression(body, source, location)?;
}
Expr::NamedFunctionRef { name, arity } => {
self.require_v3("a named function reference", source, location)?;
check_function(name, *arity, self.version)
.map_err(|message| Error::xpath_syntax(location, source, 0, message))?;
}
Expr::DynamicCall { function, args } => {
self.require_v3("a dynamic function call", source, location)?;
self.check_expression(function, source, location)?;
for arg in args {
self.check_expression(arg, source, location)?;
}
}
_ => unreachable!("caller matches only the three XPath 3.0 variants"),
}
Ok(())
}
fn check_node_test(&self, test: &NodeTest, source: &str, location: &str) -> Result<()> {
match test {
NodeTest::Name(NameTest { uri: Some(_), .. }) => {
self.require_v3("an EQName (Q{uri}local)", source, location)
}
NodeTest::Name(NameTest {
prefix: Some(prefix),
..
})
| NodeTest::NamespaceWildcard(prefix) => self.check_prefix(prefix, source, location),
NodeTest::Kind { kind, name } => {
let written = match kind {
crate::xml::NodeKind::Element => "element",
crate::xml::NodeKind::Attribute => "attribute",
_ => "document-node",
};
self.require_v2(&format!("the {written}() kind test"), source, location)?;
if let Some(NameTest {
prefix: Some(prefix),
..
}) = name
{
self.check_prefix(prefix, source, location)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn require_v2(&self, construct: &str, source: &str, location: &str) -> Result<()> {
if self.version.is_v2() {
return Ok(());
}
Err(Error::xpath_syntax(
location,
source,
0,
format!(
"{construct} is XPath 2.0 syntax; this schema's query binding is \
XPath 1.0. Set queryBinding=\"xslt2\" to use it, and see \
spec/xpath2/ for what that enables."
),
))
}
fn require_v3(&self, construct: &str, source: &str, location: &str) -> Result<()> {
if self.version.is_v3() {
return Ok(());
}
Err(Error::xpath_syntax(
location,
source,
0,
format!(
"{construct} is XPath 3.0 syntax; this schema's query binding is \
{}. Set queryBinding=\"xslt3\" to use it, and see spec/xpath3/ \
for what that enables.",
self.version.as_str()
),
))
}
fn check_binary_op_version(&self, op: BinaryOp, source: &str, location: &str) -> Result<()> {
if op.is_value_comparison() {
self.require_v2(
&format!("the `{}` value comparison", op.as_str()),
source,
location,
)?;
}
if op.is_node_comparison() {
self.require_v2(
&format!("the `{}` node comparison", op.as_str()),
source,
location,
)?;
}
if op.is_string_concat() {
self.require_v3("the `||` string concatenation operator", source, location)?;
}
if op.is_simple_map() {
self.require_v3("the `!` simple map operator", source, location)?;
}
Ok(())
}
fn check_prefix(&self, prefix: &str, source: &str, location: &str) -> Result<()> {
if self.namespaces.resolve(prefix).is_some() {
return Ok(());
}
let declared: Vec<&str> = self.model.namespaces.iter().map(|n| n.prefix.as_str()).collect();
Err(Error::xpath_syntax(
location,
source,
0,
format!(
"namespace prefix {prefix:?} is not declared; add <ns prefix=\"{prefix}\" uri=\"…\"/> \
to the schema{}",
if declared.is_empty() {
", which currently declares none".to_string()
} else {
format!(", which declares: {}", declared.join(", "))
}
),
))
}
fn check_references(&self) -> Result<()> {
for pattern in &self.model.patterns {
for rule in &pattern.rules {
for assertion in rule.assertions() {
for id in &assertion.diagnostics {
if self.model.diagnostic(id).is_none() {
return Err(Error::schema(
assertion.kind.as_str(),
pattern.id.clone(),
format!("no <diagnostic> with @id={id:?}"),
));
}
}
for id in &assertion.properties {
if self.model.property(id).is_none() {
return Err(Error::schema(
assertion.kind.as_str(),
pattern.id.clone(),
format!("no <property> with @id={id:?}"),
));
}
}
}
}
}
for phase in &self.model.phases {
for active in &phase.actives {
if self.model.pattern(active).is_none() {
return Err(Error::schema(
"active",
Some(format!("phase[@id='{}']", phase.id)),
format!(
"no <pattern> with @id={active:?}; an abstract pattern \
cannot be activated either, since it never runs"
),
));
}
}
}
for (index, key) in self.model.keys.iter().enumerate() {
if self.model.keys[..index].iter().any(|earlier| earlier.name == key.name) {
return Err(Error::schema(
"key",
Some(format!("key[@name='{}']", key.name)),
"two keys share this name; a lookup would be ambiguous",
));
}
}
if let Some(default) = &self.model.default_phase {
if self.model.phase(default).is_none() {
return Err(Error::schema(
"schema",
None,
format!("@defaultPhase names {default:?}, which is not a declared <phase>"),
));
}
}
Ok(())
}
pub fn validate(&self, document: &Document) -> Result<crate::Report> {
self.validate_with(document, &crate::ValidateOptions::new())
}
pub fn validate_with(
&self,
document: &Document,
options: &crate::ValidateOptions,
) -> Result<crate::Report> {
crate::validate::validate(self, document, options)
}
pub fn validate_streaming<R: std::io::Read>(
&self,
source: R,
options: &crate::ValidateOptions,
) -> Result<crate::Report> {
crate::validate::validate_streaming(self, source, options)
}
pub fn streaming_eligible(&self) -> std::result::Result<(), &str> {
match &self.streaming_ineligible_reason {
None => Ok(()),
Some(reason) => Err(reason.as_str()),
}
}
pub(crate) fn resolver(&self) -> &dyn super::resolver::Resolver {
self.resolver.as_ref()
}
pub(crate) fn expression(&self, source: &str) -> Result<&Expr> {
self.expressions.get(source).ok_or_else(|| {
Error::xpath_eval(
source,
"this expression was not compiled; this is a bug in the crate",
)
})
}
#[must_use]
pub fn uses_document_function(&self) -> bool {
self.uses_document_function
}
#[must_use]
pub fn model(&self) -> &SchemaModel {
&self.model
}
#[must_use]
pub fn namespaces(&self) -> &Namespaces {
&self.namespaces
}
#[must_use]
pub fn id(&self) -> Option<&str> {
self.model.id.as_deref()
}
#[must_use]
pub fn title(&self) -> Option<&str> {
self.model.title.as_deref()
}
#[must_use]
pub fn schema_version(&self) -> Option<&str> {
self.model.schema_version.as_deref()
}
#[must_use]
pub fn version(&self) -> XPathVersion {
self.version
}
#[must_use]
pub fn query_binding(&self) -> &QueryBinding {
&self.model.query_binding
}
pub fn phases(&self) -> impl Iterator<Item = &str> {
self.model.phases.iter().map(|p| p.id.as_str())
}
#[must_use]
pub fn default_phase(&self) -> Option<&str> {
self.model.default_phase.as_deref()
}
#[must_use]
pub fn keys(&self) -> &[crate::schema::Key] {
&self.model.keys
}
#[must_use]
pub fn patterns(&self) -> &[Pattern] {
&self.model.patterns
}
}
fn collect_let(binding: &Let, location: &str, work: &mut Vec<(String, String, bool)>) {
match &binding.value {
LetValue::Expression(expression) => work.push((
expression.clone(),
format!("{location}/let[@name='{}']", binding.name),
false,
)),
LetValue::Content(content) => collect_content(
content,
&format!("{location}/let[@name='{}']", binding.name),
work,
),
}
}
fn collect_pattern(pattern: &Pattern, index: usize, work: &mut Vec<(String, String, bool)>) {
let location = pattern.id.as_ref().map_or_else(
|| format!("pattern[{}]", index + 1),
|id| format!("pattern[@id='{id}']"),
);
if let Some(documents) = &pattern.documents {
work.push((documents.clone(), format!("{location}/@documents"), false));
}
for binding in &pattern.lets {
collect_let(binding, &location, work);
}
for rule in &pattern.rules {
collect_rule(rule, &location, work);
}
}
fn collect_rule(rule: &Rule, pattern_location: &str, work: &mut Vec<(String, String, bool)>) {
let location = match &rule.context {
Some(context) => format!("{pattern_location}/rule[@context='{context}']"),
None => format!("{pattern_location}/rule"),
};
if let Some(context) = &rule.context {
work.push((context.clone(), format!("{location}/@context"), true));
}
if let Some(subject) = &rule.subject {
work.push((subject.clone(), format!("{location}/@subject"), false));
}
for binding in &rule.lets {
collect_let(binding, &location, work);
}
for (index, assertion) in rule.assertions().enumerate() {
collect_assertion(assertion, &location, index, work);
}
}
fn collect_assertion(
assertion: &Assertion,
rule_location: &str,
index: usize,
work: &mut Vec<(String, String, bool)>,
) {
let location = format!("{rule_location}/{}[{}]", assertion.kind.as_str(), index + 1);
work.push((assertion.test.clone(), format!("{location}/@test"), false));
if let Some(subject) = &assertion.subject {
work.push((subject.clone(), format!("{location}/@subject"), false));
}
collect_content(&assertion.content, &location, work);
}
fn collect_content(content: &[Content], location: &str, work: &mut Vec<(String, String, bool)>) {
for fragment in content {
match fragment {
Content::Text(_) | Content::Name { path: None } => {}
Content::ValueOf { select } => {
work.push((select.clone(), format!("{location}/value-of/@select"), false));
}
Content::Name { path: Some(path) } => {
work.push((path.clone(), format!("{location}/name/@path"), false));
}
Content::Emph(inner)
| Content::Span { content: inner, .. }
| Content::Dir { content: inner, .. } => collect_content(inner, location, work),
}
}
}
fn check_literal_regex(
name: &str,
args: &[Expr],
source: &str,
location: &str,
) -> Result<()> {
let (pattern_index, flags_index) = match name {
"matches" => (1, 2),
"replace" => (1, 3),
_ => return Ok(()),
};
let Some(Expr::Literal(pattern)) = args.get(pattern_index) else {
return Ok(());
};
let flags = match args.get(flags_index) {
Some(Expr::Literal(flags)) => Some(flags.as_str()),
Some(_) => return Ok(()),
None => None,
};
crate::xpath::check_regex(pattern, flags)
.map_err(|message| Error::xpath_syntax(location, source, 0, message))
}
fn calls_document_function(expr: &Expr) -> bool {
match expr {
Expr::Literal(_) | Expr::Number(_, _) | Expr::Variable(_) => false,
Expr::Negate(inner) => calls_document_function(inner),
Expr::Binary(_, left, right) => {
calls_document_function(left) || calls_document_function(right)
}
Expr::Function { name, args } => {
name == "document" || args.iter().any(calls_document_function)
}
Expr::If {
condition,
then_branch,
else_branch,
} => {
calls_document_function(condition)
|| calls_document_function(then_branch)
|| calls_document_function(else_branch)
}
Expr::TypeOp { value, .. } => calls_document_function(value),
Expr::Sequence(members) => members.iter().any(calls_document_function),
Expr::Range(from, to) => {
calls_document_function(from) || calls_document_function(to)
}
Expr::For { input, body, .. } => {
calls_document_function(input) || calls_document_function(body)
}
Expr::Let { value, body, .. } => {
calls_document_function(value) || calls_document_function(body)
}
Expr::Quantified { input, test, .. } => {
calls_document_function(input) || calls_document_function(test)
}
Expr::InlineFunction { body, .. } => calls_document_function(body),
Expr::NamedFunctionRef { name, .. } => name == "document",
Expr::DynamicCall { function, args } => {
calls_document_function(function) || args.iter().any(calls_document_function)
}
Expr::Arrow(called) => calls_document_function(called),
Expr::Path(path) => {
let start = match &path.start {
PathStart::Expr(expr, predicates) => {
calls_document_function(expr)
|| predicates.iter().any(calls_document_function)
}
PathStart::Root | PathStart::Context => false,
};
start
|| path
.steps
.iter()
.any(|step| step.predicates.iter().any(calls_document_function))
}
}
}
fn streaming_ineligibility_reason(schema: &Schema) -> Option<String> {
if !schema.model.keys.is_empty() {
return Some(
"the schema declares <key>; a key indexes the whole document, and a \
per-record index would silently answer key() differently than a real \
one would"
.to_string(),
);
}
if !schema.model.lets.is_empty() {
return Some(
"the schema declares a schema-scoped <let>, which is evaluated once \
against the document root before any record has been parsed"
.to_string(),
);
}
if let Some(phase) = schema.model.phases.iter().find(|p| !p.lets.is_empty()) {
return Some(format!(
"phase {:?} declares a phase-scoped <let>, which is evaluated once \
against the document root before any record has been parsed",
phase.id
));
}
for pattern in &schema.model.patterns {
if pattern.documents.is_some() {
return Some(format!(
"pattern {} declares @documents, which validates other whole \
documents against the same pattern rather than the streamed one",
pattern_label(pattern)
));
}
if !pattern.lets.is_empty() {
return Some(format!(
"pattern {} declares a pattern-scoped <let>, which is evaluated \
once against the document root before any record has been parsed",
pattern_label(pattern)
));
}
}
for expr in schema.expressions.values() {
if let Some(what) = streaming_unsafe_construct(expr) {
return Some(format!(
"an expression uses {what}, which needs more of the document than \
one record's own subtree"
));
}
}
None
}
fn pattern_label(pattern: &Pattern) -> String {
match &pattern.id {
Some(id) => format!("{id:?}"),
None => "(unnamed)".to_string(),
}
}
fn streaming_unsafe_construct(expr: &Expr) -> Option<&'static str> {
const UNSAFE_FUNCTIONS: [&str; 3] = ["document", "id", "key"];
match expr {
Expr::Literal(_) | Expr::Number(_, _) | Expr::Variable(_) => None,
Expr::Negate(inner) => streaming_unsafe_construct(inner),
Expr::Binary(_, left, right) => {
streaming_unsafe_construct(left).or_else(|| streaming_unsafe_construct(right))
}
Expr::Function { name, args } => UNSAFE_FUNCTIONS
.iter()
.find(|&&f| f == name)
.map(|&f| function_reason(f))
.or_else(|| args.iter().find_map(streaming_unsafe_construct)),
Expr::If {
condition,
then_branch,
else_branch,
} => streaming_unsafe_construct(condition)
.or_else(|| streaming_unsafe_construct(then_branch))
.or_else(|| streaming_unsafe_construct(else_branch)),
Expr::TypeOp { value, .. } => streaming_unsafe_construct(value),
Expr::Sequence(members) => members.iter().find_map(streaming_unsafe_construct),
Expr::Range(from, to) => {
streaming_unsafe_construct(from).or_else(|| streaming_unsafe_construct(to))
}
Expr::For { input, body, .. } => {
streaming_unsafe_construct(input).or_else(|| streaming_unsafe_construct(body))
}
Expr::Let { value, body, .. } => {
streaming_unsafe_construct(value).or_else(|| streaming_unsafe_construct(body))
}
Expr::Quantified { input, test, .. } => {
streaming_unsafe_construct(input).or_else(|| streaming_unsafe_construct(test))
}
Expr::InlineFunction { body, .. } => streaming_unsafe_construct(body),
Expr::NamedFunctionRef { name, .. } => UNSAFE_FUNCTIONS
.iter()
.find(|&&f| f == name)
.map(|&f| function_reason(f)),
Expr::DynamicCall { function, args } => streaming_unsafe_construct(function)
.or_else(|| args.iter().find_map(streaming_unsafe_construct)),
Expr::Arrow(called) => streaming_unsafe_construct(called),
Expr::Path(path) => {
let start = match &path.start {
PathStart::Expr(expr, predicates) => streaming_unsafe_construct(expr)
.or_else(|| predicates.iter().find_map(streaming_unsafe_construct)),
PathStart::Root | PathStart::Context => None,
};
start.or_else(|| path.steps.iter().find_map(|step| {
axis_reason(step.axis)
.or_else(|| step.predicates.iter().find_map(streaming_unsafe_construct))
}))
}
}
}
fn axis_reason(axis: Axis) -> Option<&'static str> {
match axis {
Axis::Following => Some("the following:: axis"),
Axis::Preceding => Some("the preceding:: axis"),
Axis::FollowingSibling => Some("the following-sibling:: axis"),
_ => None,
}
}
fn function_reason(name: &'static str) -> &'static str {
match name {
"document" => "document()",
"id" => "id()",
"key" => "key()",
_ => unreachable!("only called with a name from UNSAFE_FUNCTIONS"),
}
}
fn check_match_pattern(expr: &Expr, source: &str, location: &str) -> Result<()> {
match expr {
Expr::Binary(crate::xpath::BinaryOp::Union, left, right) => {
check_match_pattern(left, source, location)?;
check_match_pattern(right, source, location)
}
Expr::Path(path) => {
for step in &path.steps {
let allowed = matches!(
step.axis,
Axis::Child | Axis::Attribute | Axis::DescendantOrSelf | Axis::SelfAxis
);
if !allowed {
return Err(Error::xpath_syntax(
location,
source,
0,
format!(
"the {}:: axis is not allowed in a rule context, which must be an \
XSLT match pattern; express the constraint as a predicate instead, \
such as `a[ancestor::b]`",
step.axis.as_str()
),
));
}
}
Ok(())
}
_ => Err(Error::xpath_syntax(
location,
source,
0,
"a rule context must be a location path or a union of location paths",
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
const MINIMAL: &str = r#"
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern><rule context="a"><assert test="b">m</assert></rule></pattern>
</schema>
"#;
fn error(source: &str) -> String {
Schema::from_str(source).unwrap_err().to_string()
}
#[test]
fn compiles_a_minimal_schema() {
let schema = Schema::from_str(MINIMAL).unwrap();
assert_eq!(schema.patterns().len(), 1);
assert!(schema.expression("a").is_ok());
assert!(schema.expression("b").is_ok());
}
#[test]
fn reports_syntax_errors_at_compile_time_with_a_location() {
let message = error(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern><rule context="a"><assert test="count(b">m</assert></rule></pattern>
</schema>"#,
);
assert!(message.contains("XPath syntax error"), "{message}");
assert!(message.contains("@test"), "{message}");
}
#[test]
fn reports_unknown_functions_at_compile_time() {
let message = error(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern><rule context="a"><assert test="nonsense()">m</assert></rule></pattern>
</schema>"#,
);
assert!(message.contains("unknown function"), "{message}");
}
#[test]
fn names_xpath_two_functions_as_such() {
let message = error(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern><rule context="a">
<assert test="matches(., 'x')">m</assert>
</rule></pattern>
</schema>"#,
);
assert!(message.contains("XPath 2.0"), "{message}");
}
#[test]
fn reports_undeclared_prefixes_at_compile_time() {
let message = error(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern><rule context="p:a"><assert test="b">m</assert></rule></pattern>
</schema>"#,
);
assert!(message.contains("is not declared"), "{message}");
}
#[test]
fn accepts_declared_prefixes() {
let schema = Schema::from_str(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<ns prefix="p" uri="urn:p"/>
<pattern><rule context="p:a"><assert test="p:b">m</assert></rule></pattern>
</schema>"#,
)
.unwrap();
assert_eq!(schema.namespaces().resolve("p"), Some("urn:p"));
}
#[test]
fn rejects_query_bindings_above_xpath_three() {
let source = r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xpath31">
<pattern><rule context="a"><assert test="b">m</assert></rule></pattern>
</schema>"#;
assert!(matches!(
Schema::from_str(source).unwrap_err(),
Error::UnsupportedQueryBinding { .. }
));
let options = SchemaOptions::new().with_allow_unknown_query_binding(true);
assert!(Schema::from_str_with(source, &options).is_ok());
}
#[test]
fn accepts_the_xpath_two_bindings() {
for binding in ["xslt2", "xpath2"] {
let source = format!(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="{binding}">
<pattern><rule context="a"><assert test="b">m</assert></rule></pattern>
</schema>"#
);
let schema = Schema::from_str(&source)
.unwrap_or_else(|e| panic!("{binding} should compile: {e}"));
assert_eq!(schema.version(), crate::xpath::XPathVersion::V2);
}
}
#[test]
fn accepts_the_xpath_three_bindings() {
for binding in ["xslt3", "xpath3"] {
let source = format!(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="{binding}">
<pattern><rule context="a"><assert test="b">m</assert></rule></pattern>
</schema>"#
);
let schema = Schema::from_str(&source)
.unwrap_or_else(|e| panic!("{binding} should compile: {e}"));
assert_eq!(schema.version(), crate::xpath::XPathVersion::V3);
}
}
#[test]
fn a_forced_unknown_binding_is_treated_as_xpath_one() {
let source = r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xpath31">
<pattern><rule context="a"><assert test="b">m</assert></rule></pattern>
</schema>"#;
let options = SchemaOptions::new().with_allow_unknown_query_binding(true);
let schema = Schema::from_str_with(source, &options).unwrap();
assert_eq!(schema.version(), crate::xpath::XPathVersion::V1);
}
#[test]
fn rejects_reverse_axes_in_a_rule_context() {
let message = error(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern><rule context="ancestor::a"><assert test="b">m</assert></rule></pattern>
</schema>"#,
);
assert!(message.contains("not allowed in a rule context"), "{message}");
}
#[test]
fn accepts_the_usual_match_pattern_shapes() {
for context in ["a", "a/b", "//a", "/a", "@x", "a[@x]", "a | b", "*", "text()"] {
let source = format!(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern><rule context="{context}"><assert test="true()">m</assert></rule></pattern>
</schema>"#
);
assert!(Schema::from_str(&source).is_ok(), "rejected context {context:?}");
}
}
#[test]
fn rejects_references_to_missing_diagnostics() {
let message = error(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern><rule context="a">
<assert test="b" diagnostics="nope">m</assert>
</rule></pattern>
</schema>"#,
);
assert!(message.contains("no <diagnostic>"), "{message}");
}
#[test]
fn rejects_phases_that_activate_missing_patterns() {
let message = error(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<phase id="p"><active pattern="nope"/></phase>
<pattern id="q"><rule context="a"><assert test="b">m</assert></rule></pattern>
</schema>"#,
);
assert!(message.contains("no <pattern>"), "{message}");
}
#[test]
fn rejects_a_default_phase_that_does_not_exist() {
let message = error(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="nope">
<pattern><rule context="a"><assert test="b">m</assert></rule></pattern>
</schema>"#,
);
assert!(message.contains("defaultPhase"), "{message}");
}
#[test]
fn identical_expressions_compile_once() {
let schema = Schema::from_str(
r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern>
<rule context="a"><assert test="b">one</assert></rule>
<rule context="c"><assert test="b">two</assert></rule>
</pattern>
</schema>"#,
)
.unwrap();
assert_eq!(schema.expressions.len(), 3);
}
#[test]
fn schema_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Schema>();
}
}