use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use crate::CompiledSchema;
use crate::datatypes::{DatatypeContext, DatatypeError, DatatypeRegistry};
use crate::grammar::{Context, Param, Pattern};
use crate::simplify::{choice, group, interleave, name_class_matches};
type Definitions = BTreeMap<String, Pattern>;
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ExpandedName {
pub namespace: Option<String>,
pub local: String,
}
impl ExpandedName {
pub fn new(namespace: Option<impl Into<String>>, local: impl Into<String>) -> Self {
Self {
namespace: namespace.map(Into::into),
local: local.into(),
}
}
}
impl fmt::Display for ExpandedName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.namespace {
Some(namespace) => write!(formatter, "{{{namespace}}}{}", self.local),
None => formatter.write_str(&self.local),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Content<E> {
Element(E),
Text(String),
}
pub trait Element: Sized {
type Location;
fn name(&self) -> ExpandedName;
fn attributes(&self) -> impl Iterator<Item = (ExpandedName, String)>;
fn children(&self) -> impl Iterator<Item = Content<Self>>;
fn location(&self) -> Option<Self::Location> {
None
}
fn namespace_bindings(&self) -> impl Iterator<Item = (String, String)> {
std::iter::empty()
}
}
pub fn validate<E: Element>(
schema: &CompiledSchema,
registry: &DatatypeRegistry,
root: &E,
) -> Result<Vec<ValidationError<E::Location>>, DatatypeError> {
let mut checked_refs = BTreeSet::new();
check_datatypes_supported(
&schema.start,
&schema.definitions,
registry,
&mut checked_refs,
)?;
for pattern in schema.definitions.values() {
check_datatypes_supported(pattern, &schema.definitions, registry, &mut checked_refs)?;
}
let mut errors = Vec::new();
let mut path = Vec::new();
match find_element_contents(&schema.start, &root.name(), &schema.definitions) {
Some(content) => validate_element_content(
&content,
root,
&schema.definitions,
registry,
&mut path,
&mut errors,
),
None => errors.push(ValidationError {
kind: ValidationErrorKind::UnexpectedElement(root.name()),
path: Vec::new(),
location: root.location(),
}),
}
Ok(errors)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ValidationError<L> {
kind: ValidationErrorKind,
path: Vec<ExpandedName>,
location: Option<L>,
}
impl<L> ValidationError<L> {
pub fn kind(&self) -> &ValidationErrorKind {
&self.kind
}
pub fn path(&self) -> &[ExpandedName] {
&self.path
}
pub fn location(&self) -> Option<&L> {
self.location.as_ref()
}
}
impl<L: fmt::Display> fmt::Display for ValidationError<L> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.kind)?;
if let Some(location) = &self.location {
write!(formatter, " at {location}")?;
}
Ok(())
}
}
impl<L: fmt::Debug + fmt::Display> std::error::Error for ValidationError<L> {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ValidationErrorKind {
UnexpectedElement(ExpandedName),
UnexpectedText,
UnexpectedAttribute(ExpandedName),
InvalidAttributeValue(ExpandedName, String),
MissingContent,
}
impl fmt::Display for ValidationErrorKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedElement(name) => write!(formatter, "unexpected element `{name}`"),
Self::UnexpectedText => formatter.write_str("unexpected text"),
Self::UnexpectedAttribute(name) => write!(formatter, "unexpected attribute `{name}`"),
Self::InvalidAttributeValue(name, value) => {
write!(formatter, "invalid value `{value}` for attribute `{name}`")
}
Self::MissingContent => formatter.write_str("missing required content"),
}
}
}
fn validate_element_content<E: Element>(
content: &Pattern,
element: &E,
definitions: &Definitions,
registry: &DatatypeRegistry,
path: &mut Vec<ExpandedName>,
errors: &mut Vec<ValidationError<E::Location>>,
) {
path.push(element.name());
let mut pattern = content.clone();
let context = DatatypeContext::from_bindings(element.namespace_bindings().collect());
for (name, value) in element.attributes() {
if !attribute_name_known(&pattern, &name, definitions) {
errors.push(ValidationError {
kind: ValidationErrorKind::UnexpectedAttribute(name),
path: path.clone(),
location: element.location(),
});
continue;
}
let derived = deriv_attribute(&pattern, &name, &value, definitions, registry, &context);
if matches!(derived, Pattern::NotAllowed) {
errors.push(ValidationError {
kind: ValidationErrorKind::InvalidAttributeValue(name, value),
path: path.clone(),
location: element.location(),
});
} else {
pattern = derived;
}
}
let children: Vec<_> = element.children().collect();
let strip_whitespace = children.len() > 1;
for child in children {
match child {
Content::Text(text) if strip_whitespace && text.trim().is_empty() => {}
Content::Text(text) => {
let derived = deriv_text(&pattern, &text, definitions, registry, &context);
if matches!(derived, Pattern::NotAllowed) {
if !text.trim().is_empty() || wants_text(&pattern, definitions) {
errors.push(ValidationError {
kind: ValidationErrorKind::UnexpectedText,
path: path.clone(),
location: element.location(),
});
}
} else {
pattern = derived;
}
}
Content::Element(child_element) => {
let name = child_element.name();
match find_element_contents(&pattern, &name, definitions) {
Some(child_content) => {
validate_element_content(
&child_content,
&child_element,
definitions,
registry,
path,
errors,
);
pattern = deriv_element_event(&pattern, &name, definitions);
}
None => errors.push(ValidationError {
kind: ValidationErrorKind::UnexpectedElement(name),
path: path.clone(),
location: child_element.location(),
}),
}
}
}
}
if !nullable(&pattern, definitions, registry) {
errors.push(ValidationError {
kind: ValidationErrorKind::MissingContent,
path: path.clone(),
location: element.location(),
});
}
path.pop();
}
fn param_pairs(params: &[Param]) -> Vec<(&str, &str)> {
params
.iter()
.map(|param| (param.name.as_str(), param.value.as_str()))
.collect()
}
fn schema_value_context(namespace: &Option<String>, schema_context: &Context) -> DatatypeContext {
let mut bindings: Vec<(String, String)> = schema_context
.namespaces
.iter()
.map(|(prefix, uri)| (prefix.clone(), uri.clone()))
.collect();
bindings.push((String::new(), namespace.clone().unwrap_or_default()));
DatatypeContext::from_bindings(bindings)
}
fn check_datatypes_supported(
pattern: &Pattern,
definitions: &Definitions,
registry: &DatatypeRegistry,
checked_refs: &mut BTreeSet<String>,
) -> Result<(), DatatypeError> {
match pattern {
Pattern::Data {
datatype,
datatype_library,
params,
except,
..
} => {
let library = datatype_library.as_deref().unwrap_or("");
let implementation = registry
.get(library)
.ok_or_else(|| DatatypeError::unsupported_library(library))?;
implementation
.validate_params(datatype, ¶m_pairs(params))
.map_err(|reason| DatatypeError::type_problem(library, datatype, &reason))?;
for child in except {
check_datatypes_supported(child, definitions, registry, checked_refs)?;
}
Ok(())
}
Pattern::Value {
datatype,
datatype_library,
..
} => {
let library = datatype_library.as_deref().unwrap_or("");
let type_name = datatype.as_deref().unwrap_or("token");
let implementation = registry
.get(library)
.ok_or_else(|| DatatypeError::unsupported_library(library))?;
implementation
.validate_params(type_name, &[])
.map_err(|reason| DatatypeError::type_problem(library, type_name, &reason))
}
Pattern::Element { body, .. } | Pattern::Attribute { body, .. } => {
body.iter().try_for_each(|child| {
check_datatypes_supported(child, definitions, registry, checked_refs)
})
}
Pattern::Group(values)
| Pattern::Interleave(values)
| Pattern::Choice(values)
| Pattern::OneOrMore(values)
| Pattern::List(values) => values.iter().try_for_each(|child| {
check_datatypes_supported(child, definitions, registry, checked_refs)
}),
Pattern::Ref(name) => {
if checked_refs.insert(name.clone())
&& let Some(target) = definitions.get(name)
{
check_datatypes_supported(target, definitions, registry, checked_refs)?;
}
Ok(())
}
Pattern::Empty | Pattern::NotAllowed | Pattern::Text => Ok(()),
Pattern::Optional(_)
| Pattern::ZeroOrMore(_)
| Pattern::Mixed(_)
| Pattern::ExternalRef { .. }
| Pattern::ParentRef(_)
| Pattern::Grammar(_) => {
unreachable!("resolved into other patterns by simplify_pattern")
}
}
}
fn resolve_ref<'a>(name: &str, definitions: &'a Definitions) -> &'a Pattern {
definitions
.get(name)
.expect("RELAX NG ยง4.18 guarantees every ref target exists")
}
pub(crate) fn nullable(
pattern: &Pattern,
definitions: &Definitions,
registry: &DatatypeRegistry,
) -> bool {
match pattern {
Pattern::Empty => true,
Pattern::NotAllowed => false,
Pattern::Text => true,
Pattern::Element { .. } | Pattern::Attribute { .. } => false,
Pattern::Data {
datatype,
datatype_library,
params,
except,
..
} => {
let library = datatype_library.as_deref().unwrap_or("");
let implementation = registry
.get(library)
.expect("checked upfront by check_datatypes_supported");
implementation.matches(
datatype,
¶m_pairs(params),
"",
&DatatypeContext::empty(),
) && !text_matches_except(except, "", definitions, registry, &DatatypeContext::empty())
}
Pattern::Value {
value,
datatype,
datatype_library,
namespace,
context: schema_context,
} => {
let library = datatype_library.as_deref().unwrap_or("");
let type_name = datatype.as_deref().unwrap_or("token");
let implementation = registry
.get(library)
.expect("checked upfront by check_datatypes_supported");
implementation.values_equal(
type_name,
value,
&schema_value_context(namespace, schema_context),
"",
&DatatypeContext::empty(),
)
}
Pattern::List(values) => structurally_nullable(&values[0], definitions),
Pattern::Choice(values) => values
.iter()
.any(|value| nullable(value, definitions, registry)),
Pattern::Group(values) | Pattern::Interleave(values) => values
.iter()
.all(|value| nullable(value, definitions, registry)),
Pattern::OneOrMore(values) => nullable(&values[0], definitions, registry),
Pattern::Ref(name) => nullable(resolve_ref(name, definitions), definitions, registry),
Pattern::Optional(_)
| Pattern::ZeroOrMore(_)
| Pattern::Mixed(_)
| Pattern::ExternalRef { .. }
| Pattern::ParentRef(_)
| Pattern::Grammar(_) => {
unreachable!("resolved into other patterns by simplify_pattern")
}
}
}
fn deriv_positions(
values: &[Pattern],
sequential: bool,
wrap: fn(Vec<Pattern>) -> Pattern,
definitions: &Definitions,
deriv_one: &dyn Fn(&Pattern) -> Pattern,
) -> Pattern {
let mut alternatives = Vec::new();
let mut reachable = true;
for (index, value) in values.iter().enumerate() {
if reachable {
let derived = deriv_one(value);
if !matches!(derived, Pattern::NotAllowed) {
let mut branch = values.to_vec();
if sequential {
for earlier in branch.iter_mut().take(index) {
*earlier = close_child_position(earlier, definitions);
}
}
branch[index] = derived;
alternatives.push(wrap(branch));
}
}
if sequential {
reachable = reachable && structurally_nullable(value, definitions);
}
}
choice(alternatives)
}
fn structurally_nullable(pattern: &Pattern, definitions: &Definitions) -> bool {
match pattern {
Pattern::Attribute { .. } | Pattern::Empty | Pattern::Text => true,
Pattern::NotAllowed
| Pattern::Element { .. }
| Pattern::Data { .. }
| Pattern::Value { .. } => false,
Pattern::List(values) => structurally_nullable(&values[0], definitions),
Pattern::Choice(values) => values
.iter()
.any(|value| structurally_nullable(value, definitions)),
Pattern::Group(values) | Pattern::Interleave(values) => values
.iter()
.all(|value| structurally_nullable(value, definitions)),
Pattern::OneOrMore(values) => structurally_nullable(&values[0], definitions),
Pattern::Ref(name) => structurally_nullable(resolve_ref(name, definitions), definitions),
Pattern::Optional(_)
| Pattern::ZeroOrMore(_)
| Pattern::Mixed(_)
| Pattern::ExternalRef { .. }
| Pattern::ParentRef(_)
| Pattern::Grammar(_) => {
unreachable!("resolved into other patterns by simplify_pattern")
}
}
}
fn wants_text(pattern: &Pattern, definitions: &Definitions) -> bool {
match pattern {
Pattern::Text | Pattern::Data { .. } | Pattern::Value { .. } | Pattern::List(_) => true,
Pattern::Attribute { .. }
| Pattern::Empty
| Pattern::NotAllowed
| Pattern::Element { .. } => false,
Pattern::Choice(values) => values.iter().all(|value| wants_text(value, definitions)),
Pattern::Interleave(values) => values.iter().any(|value| wants_text(value, definitions)),
Pattern::Group(values) => {
let mut reachable = true;
let mut found = false;
for value in values {
if reachable {
found = found || wants_text(value, definitions);
}
reachable = reachable && structurally_nullable(value, definitions);
}
found
}
Pattern::OneOrMore(values) => wants_text(&values[0], definitions),
Pattern::Ref(name) => wants_text(resolve_ref(name, definitions), definitions),
Pattern::Optional(_)
| Pattern::ZeroOrMore(_)
| Pattern::Mixed(_)
| Pattern::ExternalRef { .. }
| Pattern::ParentRef(_)
| Pattern::Grammar(_) => {
unreachable!("resolved into other patterns by simplify_pattern")
}
}
}
fn close_child_position(pattern: &Pattern, definitions: &Definitions) -> Pattern {
match pattern {
Pattern::Attribute { .. } => pattern.clone(),
Pattern::Group(values) => group(
values
.iter()
.map(|value| close_child_position(value, definitions))
.collect(),
),
Pattern::Interleave(values) => interleave(
values
.iter()
.map(|value| close_child_position(value, definitions))
.collect(),
),
Pattern::Choice(values) => choice(
values
.iter()
.map(|value| close_child_position(value, definitions))
.collect(),
),
Pattern::OneOrMore(values) => close_child_position(&values[0], definitions),
Pattern::Ref(name) => close_child_position(resolve_ref(name, definitions), definitions),
_ => Pattern::Empty,
}
}
fn deriv_choice(values: &[Pattern], deriv_one: impl Fn(&Pattern) -> Pattern) -> Pattern {
choice(values.iter().map(deriv_one).collect())
}
fn deriv_structural(
pattern: &Pattern,
group_sequential: bool,
definitions: &Definitions,
deriv_one: &dyn Fn(&Pattern) -> Pattern,
) -> Option<Pattern> {
Some(match pattern {
Pattern::Group(values) => {
deriv_positions(values, group_sequential, group, definitions, deriv_one)
}
Pattern::Interleave(values) => {
deriv_positions(values, false, interleave, definitions, deriv_one)
}
Pattern::Choice(values) => deriv_choice(values, deriv_one),
Pattern::OneOrMore(values) => one_or_more_deriv(&values[0], pattern, deriv_one),
Pattern::Ref(name) => deriv_one(resolve_ref(name, definitions)),
_ => return None,
})
}
fn deriv_attribute(
pattern: &Pattern,
name: &ExpandedName,
value: &str,
definitions: &Definitions,
registry: &DatatypeRegistry,
context: &DatatypeContext,
) -> Pattern {
let deriv_one = |v: &Pattern| deriv_attribute(v, name, value, definitions, registry, context);
if let Some(result) = deriv_structural(pattern, false, definitions, &deriv_one) {
return result;
}
match pattern {
Pattern::Attribute {
name: name_class,
body,
..
} => {
if name_class_matches(name_class, &name.namespace, &name.local)
&& value_matches(&body[0], value, definitions, registry, context)
{
Pattern::Empty
} else {
Pattern::NotAllowed
}
}
Pattern::Element { .. }
| Pattern::Text
| Pattern::Data { .. }
| Pattern::Value { .. }
| Pattern::List(_)
| Pattern::Empty
| Pattern::NotAllowed => Pattern::NotAllowed,
Pattern::Optional(_)
| Pattern::ZeroOrMore(_)
| Pattern::Mixed(_)
| Pattern::ExternalRef { .. }
| Pattern::ParentRef(_)
| Pattern::Grammar(_) => {
unreachable!("resolved into other patterns by simplify_pattern")
}
Pattern::Group(_)
| Pattern::Interleave(_)
| Pattern::Choice(_)
| Pattern::OneOrMore(_)
| Pattern::Ref(_) => unreachable!("handled by deriv_structural above"),
}
}
fn deriv_element_event(
pattern: &Pattern,
name: &ExpandedName,
definitions: &Definitions,
) -> Pattern {
let deriv_one = |v: &Pattern| deriv_element_event(v, name, definitions);
if let Some(result) = deriv_structural(pattern, true, definitions, &deriv_one) {
return result;
}
match pattern {
Pattern::Element {
name: name_class, ..
} => {
if name_class_matches(name_class, &name.namespace, &name.local) {
Pattern::Empty
} else {
Pattern::NotAllowed
}
}
Pattern::Attribute { .. }
| Pattern::Text
| Pattern::Data { .. }
| Pattern::Value { .. }
| Pattern::List(_)
| Pattern::Empty
| Pattern::NotAllowed => Pattern::NotAllowed,
Pattern::Optional(_)
| Pattern::ZeroOrMore(_)
| Pattern::Mixed(_)
| Pattern::ExternalRef { .. }
| Pattern::ParentRef(_)
| Pattern::Grammar(_) => {
unreachable!("resolved into other patterns by simplify_pattern")
}
Pattern::Group(_)
| Pattern::Interleave(_)
| Pattern::Choice(_)
| Pattern::OneOrMore(_)
| Pattern::Ref(_) => unreachable!("handled by deriv_structural above"),
}
}
fn deriv_text(
pattern: &Pattern,
text: &str,
definitions: &Definitions,
registry: &DatatypeRegistry,
context: &DatatypeContext,
) -> Pattern {
let deriv_one = |v: &Pattern| deriv_text(v, text, definitions, registry, context);
if let Some(result) = deriv_structural(pattern, true, definitions, &deriv_one) {
return result;
}
match pattern {
Pattern::Text => Pattern::Text,
Pattern::Data {
datatype,
datatype_library,
params,
except,
..
} => {
let library = datatype_library.as_deref().unwrap_or("");
let implementation = registry
.get(library)
.expect("checked upfront by check_datatypes_supported");
if implementation.matches(datatype, ¶m_pairs(params), text, context)
&& !text_matches_except(except, text, definitions, registry, context)
{
Pattern::Empty
} else {
Pattern::NotAllowed
}
}
Pattern::Value {
value,
datatype,
datatype_library,
namespace,
context: schema_context,
} => {
let library = datatype_library.as_deref().unwrap_or("");
let type_name = datatype.as_deref().unwrap_or("token");
let implementation = registry
.get(library)
.expect("checked upfront by check_datatypes_supported");
if implementation.values_equal(
type_name,
value,
&schema_value_context(namespace, schema_context),
text,
context,
) {
Pattern::Empty
} else {
Pattern::NotAllowed
}
}
Pattern::List(values) => {
if list_matches(&values[0], text, definitions, registry, context) {
Pattern::Empty
} else {
Pattern::NotAllowed
}
}
Pattern::Element { .. }
| Pattern::Attribute { .. }
| Pattern::Empty
| Pattern::NotAllowed => Pattern::NotAllowed,
Pattern::Optional(_)
| Pattern::ZeroOrMore(_)
| Pattern::Mixed(_)
| Pattern::ExternalRef { .. }
| Pattern::ParentRef(_)
| Pattern::Grammar(_) => {
unreachable!("resolved into other patterns by simplify_pattern")
}
Pattern::Group(_)
| Pattern::Interleave(_)
| Pattern::Choice(_)
| Pattern::OneOrMore(_)
| Pattern::Ref(_) => unreachable!("handled by deriv_structural above"),
}
}
fn one_or_more_deriv(
inner: &Pattern,
whole: &Pattern,
deriv_one: &dyn Fn(&Pattern) -> Pattern,
) -> Pattern {
group(vec![
deriv_one(inner),
choice(vec![whole.clone(), Pattern::Empty]),
])
}
fn text_matches_except(
except: &[Pattern],
text: &str,
definitions: &Definitions,
registry: &DatatypeRegistry,
context: &DatatypeContext,
) -> bool {
except.iter().any(|item| {
nullable(
&deriv_text(item, text, definitions, registry, context),
definitions,
registry,
)
})
}
fn list_matches(
pattern: &Pattern,
text: &str,
definitions: &Definitions,
registry: &DatatypeRegistry,
context: &DatatypeContext,
) -> bool {
let mut current = pattern.clone();
for token in text.split_ascii_whitespace() {
current = deriv_text(¤t, token, definitions, registry, context);
if matches!(current, Pattern::NotAllowed) {
return false;
}
}
structurally_nullable(¤t, definitions)
}
fn value_matches(
pattern: &Pattern,
value: &str,
definitions: &Definitions,
registry: &DatatypeRegistry,
context: &DatatypeContext,
) -> bool {
let derived = deriv_text(pattern, value, definitions, registry, context);
if !matches!(derived, Pattern::NotAllowed) && structurally_nullable(&derived, definitions) {
return true;
}
value.trim().is_empty() && !wants_text(pattern, definitions)
}
fn attribute_name_known(pattern: &Pattern, name: &ExpandedName, definitions: &Definitions) -> bool {
match pattern {
Pattern::Attribute {
name: name_class, ..
} => name_class_matches(name_class, &name.namespace, &name.local),
Pattern::Group(values) | Pattern::Interleave(values) | Pattern::Choice(values) => values
.iter()
.any(|value| attribute_name_known(value, name, definitions)),
Pattern::OneOrMore(values) => attribute_name_known(&values[0], name, definitions),
Pattern::Ref(ref_name) => {
attribute_name_known(resolve_ref(ref_name, definitions), name, definitions)
}
_ => false,
}
}
fn find_element_contents(
pattern: &Pattern,
name: &ExpandedName,
definitions: &Definitions,
) -> Option<Pattern> {
let mut found = Vec::new();
collect_element_contents(pattern, name, definitions, &mut found);
if found.is_empty() {
None
} else {
Some(choice(found))
}
}
fn collect_element_contents(
pattern: &Pattern,
name: &ExpandedName,
definitions: &Definitions,
out: &mut Vec<Pattern>,
) {
match pattern {
Pattern::Element {
name: name_class,
body,
..
} if name_class_matches(name_class, &name.namespace, &name.local) => {
out.push(body[0].clone());
}
Pattern::Choice(values) | Pattern::Interleave(values) => {
for value in values {
collect_element_contents(value, name, definitions, out);
}
}
Pattern::Group(values) => {
let mut reachable = true;
for value in values {
if !reachable {
break;
}
collect_element_contents(value, name, definitions, out);
reachable = structurally_nullable(value, definitions);
}
}
Pattern::OneOrMore(values) => collect_element_contents(&values[0], name, definitions, out),
Pattern::Ref(ref_name) => {
collect_element_contents(resolve_ref(ref_name, definitions), name, definitions, out)
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::datatypes::DatatypeLibrary;
use crate::{ResolveError, SchemaResolver, SchemaSource, SchemaSyntax};
struct NoResolver;
impl SchemaResolver for NoResolver {
fn resolve(&self, href: &str, _base_uri: &str) -> Result<SchemaSource, ResolveError> {
Err(ResolveError::new(format!(
"validator tests don't use includes (requested `{href}`)"
)))
}
}
fn schema(text: &str) -> CompiledSchema {
let document = crate::parse(&SchemaSource::new(
text,
"memory:/main.rnc",
SchemaSyntax::Compact,
))
.expect("schema parses");
crate::simplify(&document, &NoResolver).expect("schema simplifies")
}
fn xml_schema(text: &str) -> CompiledSchema {
let document = crate::parse(&SchemaSource::new(
text,
"memory:/main.rng",
SchemaSyntax::Xml,
))
.expect("schema parses");
crate::simplify(&document, &NoResolver).expect("schema simplifies")
}
fn registry() -> DatatypeRegistry {
DatatypeRegistry::new()
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct TestLocation {
line: u32,
column: u32,
}
impl fmt::Display for TestLocation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}:{}", self.line, self.column)
}
}
#[derive(Clone, Debug)]
struct TestElement {
name: ExpandedName,
attributes: Vec<(ExpandedName, String)>,
children: Vec<Content<TestElement>>,
namespace_bindings: Vec<(String, String)>,
location: Option<TestLocation>,
}
impl TestElement {
fn new(local: &str) -> Self {
Self {
name: ExpandedName::new(None::<String>, local),
attributes: Vec::new(),
children: Vec::new(),
namespace_bindings: Vec::new(),
location: None,
}
}
fn new_namespaced(namespace: &str, local: &str) -> Self {
Self {
name: ExpandedName::new(Some(namespace), local),
attributes: Vec::new(),
children: Vec::new(),
namespace_bindings: Vec::new(),
location: None,
}
}
fn at(mut self, line: u32, column: u32) -> Self {
self.location = Some(TestLocation { line, column });
self
}
fn attr(mut self, local: &str, value: &str) -> Self {
self.attributes
.push((ExpandedName::new(None::<String>, local), value.to_owned()));
self
}
fn text(mut self, text: &str) -> Self {
self.children.push(Content::Text(text.to_owned()));
self
}
fn child(mut self, element: TestElement) -> Self {
self.children.push(Content::Element(element));
self
}
fn xmlns(mut self, prefix: &str, uri: &str) -> Self {
self.namespace_bindings
.push((prefix.to_owned(), uri.to_owned()));
self
}
}
impl Element for TestElement {
type Location = TestLocation;
fn name(&self) -> ExpandedName {
self.name.clone()
}
fn attributes(&self) -> impl Iterator<Item = (ExpandedName, String)> {
self.attributes.clone().into_iter()
}
fn children(&self) -> impl Iterator<Item = Content<Self>> {
self.children.clone().into_iter()
}
fn location(&self) -> Option<Self::Location> {
self.location.clone()
}
fn namespace_bindings(&self) -> impl Iterator<Item = (String, String)> {
self.namespace_bindings.clone().into_iter()
}
}
fn kinds(errors: &[ValidationError<TestLocation>]) -> Vec<ValidationErrorKind> {
errors.iter().map(|error| error.kind().clone()).collect()
}
fn name(local: &str) -> ExpandedName {
ExpandedName::new(None::<String>, local)
}
#[test]
fn nullable_base_cases() {
let definitions = Definitions::new();
let registry = registry();
assert!(nullable(&Pattern::Empty, &definitions, ®istry));
assert!(!nullable(&Pattern::NotAllowed, &definitions, ®istry));
assert!(nullable(&Pattern::Text, &definitions, ®istry));
}
#[test]
fn nullable_group_requires_every_operand_nullable() {
let definitions = Definitions::new();
let registry = registry();
assert!(nullable(
&Pattern::Group(vec![Pattern::Empty, Pattern::Text]),
&definitions,
®istry
));
assert!(!nullable(
&Pattern::Group(vec![Pattern::Empty, Pattern::NotAllowed]),
&definitions,
®istry
));
}
#[test]
fn nullable_choice_requires_any_operand_nullable() {
let definitions = Definitions::new();
let registry = registry();
assert!(nullable(
&Pattern::Choice(vec![Pattern::NotAllowed, Pattern::Empty]),
&definitions,
®istry
));
assert!(!nullable(
&Pattern::Choice(vec![Pattern::NotAllowed, Pattern::NotAllowed]),
&definitions,
®istry
));
}
#[test]
fn nullable_one_or_more_delegates_to_inner() {
let definitions = Definitions::new();
let registry = registry();
assert!(nullable(
&Pattern::OneOrMore(vec![Pattern::Empty]),
&definitions,
®istry
));
assert!(!nullable(
&Pattern::OneOrMore(vec![Pattern::NotAllowed]),
&definitions,
®istry
));
}
#[test]
fn nullable_list_delegates_to_inner_same_as_zero_tokens_would() {
let definitions = Definitions::new();
let registry = registry();
assert!(nullable(
&Pattern::List(vec![Pattern::Empty]),
&definitions,
®istry
));
assert!(!nullable(
&Pattern::List(vec![Pattern::NotAllowed]),
&definitions,
®istry
));
}
#[test]
fn nullable_ref_resolves_through_definitions() {
let definitions = Definitions::from([("x".to_owned(), Pattern::Empty)]);
let registry = registry();
assert!(nullable(&Pattern::Ref("x".into()), &definitions, ®istry));
}
#[test]
fn deriv_element_event_matches_by_name_and_rejects_others() {
let compiled = schema("start = element foo { empty }");
let registry = registry();
assert!(nullable(
&deriv_element_event(&compiled.start, &name("foo"), &compiled.definitions),
&compiled.definitions,
®istry
));
assert!(matches!(
deriv_element_event(&compiled.start, &name("bar"), &compiled.definitions),
Pattern::NotAllowed
));
}
#[test]
fn deriv_attribute_checks_name_and_value() {
let compiled = schema(r#"start = element foo { attribute id { "x" } }"#);
let registry = registry();
let context = DatatypeContext::empty();
let Pattern::Element { body, .. } = &compiled.start else {
panic!("expected an element pattern");
};
assert!(nullable(
&deriv_attribute(
&body[0],
&name("id"),
"x",
&compiled.definitions,
®istry,
&context
),
&compiled.definitions,
®istry
));
assert!(matches!(
deriv_attribute(
&body[0],
&name("id"),
"y",
&compiled.definitions,
®istry,
&context
),
Pattern::NotAllowed
));
assert!(matches!(
deriv_attribute(
&body[0],
&name("other"),
"x",
&compiled.definitions,
®istry,
&context
),
Pattern::NotAllowed
));
}
#[test]
fn deriv_text_data_accepts_any_string_value_requires_exact_match() {
let registry = registry();
let context = DatatypeContext::empty();
let compiled = schema("start = element foo { token }");
let Pattern::Element { body, .. } = &compiled.start else {
panic!("expected an element pattern");
};
assert!(nullable(
&deriv_text(
&body[0],
"whatever text",
&compiled.definitions,
®istry,
&context
),
&compiled.definitions,
®istry
));
let compiled = schema(r#"start = element foo { "exact" }"#);
let Pattern::Element { body, .. } = &compiled.start else {
panic!("expected an element pattern");
};
assert!(nullable(
&deriv_text(
&body[0],
"exact",
&compiled.definitions,
®istry,
&context
),
&compiled.definitions,
®istry
));
assert!(matches!(
deriv_text(
&body[0],
"different",
&compiled.definitions,
®istry,
&context
),
Pattern::NotAllowed
));
}
#[test]
fn deriv_text_token_equality_collapses_whitespace() {
let registry = registry();
let context = DatatypeContext::empty();
let compiled = schema(r#"start = element foo { token "a b" }"#);
let Pattern::Element { body, .. } = &compiled.start else {
panic!("expected an element pattern");
};
assert!(nullable(
&deriv_text(
&body[0],
"a b",
&compiled.definitions,
®istry,
&context
),
&compiled.definitions,
®istry
));
}
#[test]
fn group_derivative_is_sequential() {
let registry = registry();
let compiled = schema("start = element root { element a { empty }, element b { empty } }");
let Pattern::Element { body, .. } = &compiled.start else {
panic!("expected an element pattern");
};
let content = &body[0];
assert!(matches!(
deriv_element_event(content, &name("b"), &compiled.definitions),
Pattern::NotAllowed
));
let after_a = deriv_element_event(content, &name("a"), &compiled.definitions);
let after_a_b = deriv_element_event(&after_a, &name("b"), &compiled.definitions);
assert!(nullable(&after_a_b, &compiled.definitions, ®istry));
}
#[test]
fn interleave_derivative_allows_either_order() {
let registry = registry();
let compiled = schema("start = element root { element a { empty } & element b { empty } }");
let Pattern::Element { body, .. } = &compiled.start else {
panic!("expected an element pattern");
};
let content = &body[0];
let after_b = deriv_element_event(content, &name("b"), &compiled.definitions);
assert!(!matches!(after_b, Pattern::NotAllowed));
let after_b_a = deriv_element_event(&after_b, &name("a"), &compiled.definitions);
assert!(nullable(&after_b_a, &compiled.definitions, ®istry));
}
#[test]
fn attribute_matches_through_ref_and_interleave_xmloxide_issue_56() {
let compiled = schema(
"start = element foo { attrs & text }\n\
attrs = attribute id { text }",
);
let document = TestElement::new("foo").attr("id", "x").text("hello");
let errors = validate(&compiled, ®istry(), &document).expect("builtin datatypes only");
assert_eq!(kinds(&errors), Vec::<ValidationErrorKind>::new());
}
#[test]
fn attribute_name_known_resolves_ref_through_group_and_choice_too() {
let compiled = schema(
"start = element foo { a, b }\n\
a = attribute x { text } | attribute y { text }\n\
b = empty",
);
let Pattern::Element { body, .. } = &compiled.start else {
panic!("expected an element pattern");
};
assert!(attribute_name_known(
&body[0],
&name("x"),
&compiled.definitions
));
assert!(attribute_name_known(
&body[0],
&name("y"),
&compiled.definitions
));
assert!(!attribute_name_known(
&body[0],
&name("z"),
&compiled.definitions
));
}
#[test]
fn ref_is_transparent_for_child_elements_through_choice_and_group() {
let compiled = schema(
"start = element foo { greeting, farewell }\n\
greeting = element hello { empty } | element hi { empty }\n\
farewell = element bye { empty }",
);
let document = TestElement::new("foo")
.child(TestElement::new("hi"))
.child(TestElement::new("bye"));
let errors = validate(&compiled, ®istry(), &document).expect("builtin datatypes only");
assert_eq!(kinds(&errors), Vec::<ValidationErrorKind>::new());
}
#[test]
fn valid_document_with_required_attribute_and_text() {
let compiled = schema("start = element foo { attribute id { text }, text }");
let document = TestElement::new("foo").attr("id", "1").text("hello");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert!(errors.is_empty());
}
#[test]
fn missing_required_attribute_is_reported() {
let compiled = schema("start = element foo { attribute id { text }, text }");
let document = TestElement::new("foo").text("hello");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert_eq!(kinds(&errors), vec![ValidationErrorKind::MissingContent]);
}
#[test]
fn unexpected_attribute_is_reported() {
let compiled = schema("start = element foo { empty }");
let document = TestElement::new("foo").attr("id", "1");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert_eq!(
kinds(&errors),
vec![ValidationErrorKind::UnexpectedAttribute(name("id"))]
);
}
#[test]
fn structured_location_is_reported_on_the_validation_error() {
let compiled = schema("start = element foo { empty }");
let document = TestElement::new("foo").attr("id", "1").at(3, 7);
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].location(),
Some(&TestLocation { line: 3, column: 7 })
);
assert_eq!(errors[0].to_string(), "unexpected attribute `id` at 3:7");
}
#[test]
fn invalid_attribute_value_is_reported_distinctly_from_unexpected_attribute() {
let compiled = schema(r#"start = element foo { attribute id { "x" } }"#);
let document = TestElement::new("foo").attr("id", "y");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert_eq!(
kinds(&errors),
vec![
ValidationErrorKind::InvalidAttributeValue(name("id"), "y".to_owned()),
ValidationErrorKind::MissingContent,
]
);
}
#[test]
fn unexpected_child_element_is_reported() {
let compiled = schema("start = element foo { element bar { empty } }");
let document = TestElement::new("foo").child(TestElement::new("baz"));
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert_eq!(
kinds(&errors),
vec![
ValidationErrorKind::UnexpectedElement(name("baz")),
ValidationErrorKind::MissingContent,
]
);
}
#[test]
fn unexpected_root_element_is_reported() {
let compiled = schema("start = element foo { empty }");
let document = TestElement::new("bar");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert_eq!(
kinds(&errors),
vec![ValidationErrorKind::UnexpectedElement(name("bar"))]
);
}
#[test]
fn unexpected_text_is_reported() {
let compiled = schema("start = element foo { empty }");
let document = TestElement::new("foo").text("surprise");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert_eq!(kinds(&errors), vec![ValidationErrorKind::UnexpectedText]);
}
#[test]
fn one_or_more_children_are_validated_recursively_and_repeatedly() {
let compiled = schema("start = element foo { element item { text }+ }");
let document = TestElement::new("foo")
.child(TestElement::new("item").text("a"))
.child(TestElement::new("item").text("b"));
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert!(errors.is_empty());
}
#[test]
fn one_or_more_requires_at_least_one_occurrence() {
let compiled = schema("start = element foo { element item { empty }+ }");
let document = TestElement::new("foo");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert_eq!(kinds(&errors), vec![ValidationErrorKind::MissingContent]);
}
#[test]
fn interleaved_text_and_element_children_validate_regardless_of_order() {
let compiled = schema("start = element foo { mixed { element bar { empty } } }");
let document = TestElement::new("foo")
.text("before ")
.child(TestElement::new("bar"))
.text(" after");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert!(errors.is_empty());
}
#[test]
fn list_datatype_matches_whitespace_separated_tokens() {
let compiled = schema("start = element foo { list { token+ } }");
let document = TestElement::new("foo").text("a b c");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert!(errors.is_empty());
}
#[test]
fn list_datatype_rejects_a_token_that_does_not_match() {
let compiled = schema(r#"start = element foo { list { "a", "b" } }"#);
let document = TestElement::new("foo").text("a c");
let errors = validate(&compiled, ®istry(), &document).unwrap();
assert_eq!(
kinds(&errors),
vec![
ValidationErrorKind::UnexpectedText,
ValidationErrorKind::MissingContent,
]
);
}
#[test]
fn unsupported_datatype_library_fails_fast_before_validating() {
let compiled = xml_schema(
r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo">
<data type="non-empty-string" datatypeLibrary="http://whattf.org/datatype-draft"/>
</element>"#,
);
let document = TestElement::new("foo").text("hello");
let error = validate(&compiled, ®istry(), &document).unwrap_err();
assert_eq!(error.library(), "http://whattf.org/datatype-draft");
}
#[test]
fn builtin_only_registry_rejects_xsd_too() {
let compiled = schema(
"datatypes xsd = \"http://www.w3.org/2001/XMLSchema-datatypes\"\n\
start = element foo { xsd:string }",
);
let document = TestElement::new("foo").text("hello");
let error = validate(&compiled, &DatatypeRegistry::builtin_only(), &document).unwrap_err();
assert_eq!(
error.library(),
"http://www.w3.org/2001/XMLSchema-datatypes"
);
}
#[test]
fn xsd_string_and_integer_types_match() {
let compiled = xml_schema(
r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<group>
<attribute name="count"><data type="positiveInteger"/></attribute>
<data type="string"/>
</group>
</element>"#,
);
let valid = TestElement::new("foo").attr("count", "3").text("hello");
assert!(validate(&compiled, ®istry(), &valid).unwrap().is_empty());
let invalid = TestElement::new("foo").attr("count", "0").text("hello");
let errors = validate(&compiled, ®istry(), &invalid).unwrap();
assert_eq!(
kinds(&errors),
vec![
ValidationErrorKind::InvalidAttributeValue(name("count"), "0".to_owned()),
ValidationErrorKind::MissingContent,
]
);
}
#[test]
fn xsd_pattern_facet_is_enforced() {
let compiled = xml_schema(
r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<data type="string"><param name="pattern">[a-z]+</param></data>
</element>"#,
);
let valid = TestElement::new("foo").text("abc");
assert!(validate(&compiled, ®istry(), &valid).unwrap().is_empty());
let invalid = TestElement::new("foo").text("ABC");
assert_eq!(
kinds(&validate(&compiled, ®istry(), &invalid).unwrap()),
vec![
ValidationErrorKind::UnexpectedText,
ValidationErrorKind::MissingContent,
]
);
}
#[test]
fn xsd_decimal_value_equality_ignores_lexical_differences() {
let compiled = xml_schema(
r#"<element xmlns="http://relaxng.org/ns/structure/1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema-datatypes" name="foo">
<value type="decimal" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">1.50</value>
</element>"#,
);
let document = TestElement::new("foo").text("01.500");
assert!(
validate(&compiled, ®istry(), &document)
.unwrap()
.is_empty()
);
}
#[test]
fn xsd_qname_resolves_prefix_via_document_namespace_bindings() {
let compiled = xml_schema(
r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo">
<data type="QName" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"/>
</element>"#,
);
let document = TestElement::new("foo")
.xmlns("h", "urn:example:h")
.text("h:widget");
assert!(
validate(&compiled, ®istry(), &document)
.unwrap()
.is_empty()
);
let unresolved = TestElement::new("foo").text("h:widget");
assert_eq!(
kinds(&validate(&compiled, ®istry(), &unresolved).unwrap()),
vec![
ValidationErrorKind::UnexpectedText,
ValidationErrorKind::MissingContent,
]
);
}
#[test]
fn xsd_unknown_type_name_is_a_datatype_error() {
let compiled = xml_schema(
r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<data type="not-a-real-type"/>
</element>"#,
);
let document = TestElement::new("foo").text("hello");
let error = validate(&compiled, ®istry(), &document).unwrap_err();
assert_eq!(
error.library(),
"http://www.w3.org/2001/XMLSchema-datatypes"
);
}
struct DummyNonEmptyStringLibrary;
impl DatatypeLibrary for DummyNonEmptyStringLibrary {
fn validate_params(&self, type_name: &str, params: &[(&str, &str)]) -> Result<(), String> {
if type_name != "non-empty-string" {
return Err(format!("unknown dummy type `{type_name}`"));
}
if let Some((param, _)) = params.first() {
return Err(format!(
"`non-empty-string` takes no parameters, found `{param}`"
));
}
Ok(())
}
fn matches(
&self,
type_name: &str,
_params: &[(&str, &str)],
value: &str,
_context: &DatatypeContext,
) -> bool {
type_name == "non-empty-string" && !value.is_empty()
}
fn values_equal(
&self,
type_name: &str,
expected: &str,
_expected_context: &DatatypeContext,
actual: &str,
_actual_context: &DatatypeContext,
) -> bool {
type_name == "non-empty-string" && expected == actual
}
}
#[test]
fn external_plugin_library_can_be_registered_and_used() {
let compiled = xml_schema(
r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo">
<data type="non-empty-string" datatypeLibrary="http://whattf.org/datatype-draft"/>
</element>"#,
);
let mut registry = DatatypeRegistry::new();
registry.register(
"http://whattf.org/datatype-draft",
DummyNonEmptyStringLibrary,
);
let valid = TestElement::new("foo").text("hello");
assert!(validate(&compiled, ®istry, &valid).unwrap().is_empty());
let invalid = TestElement::new("foo").text("");
assert_eq!(
kinds(&validate(&compiled, ®istry, &invalid).unwrap()),
vec![
ValidationErrorKind::UnexpectedText,
ValidationErrorKind::MissingContent,
]
);
}
#[test]
fn adapter_smoke_test_with_a_realistic_xhtml_shaped_tree() {
const XHTML: &str = "http://www.w3.org/1999/xhtml";
let compiled = xml_schema(
r#"<element xmlns="http://relaxng.org/ns/structure/1.0" xmlns:xhtml="http://www.w3.org/1999/xhtml" name="xhtml:html">
<group>
<element name="xhtml:head"><empty/></element>
<element name="xhtml:body">
<element name="xhtml:p">
<group>
<attribute name="id"><text/></attribute>
<text/>
</group>
</element>
</element>
</group>
</element>"#,
);
let document = TestElement::new_namespaced(XHTML, "html")
.child(TestElement::new_namespaced(XHTML, "head"))
.child(
TestElement::new_namespaced(XHTML, "body").child(
TestElement::new_namespaced(XHTML, "p")
.attr("id", "intro")
.text("hello"),
),
);
let errors = validate(&compiled, ®istry(), &document).expect("builtin datatypes only");
assert_eq!(kinds(&errors), Vec::<ValidationErrorKind>::new());
}
}