Skip to main content

relax_ng/
schema.rs

1//! The public entry point: [`Schema`] ties `parse`/`simplify`/`validate`
2//! together into a single "compile once, validate many documents"
3//! workflow, which is almost always what a caller wants — the lower-level
4//! [`crate::parse`]/[`crate::simplify`]/[`crate::validate`] functions
5//! remain available separately for callers who need the intermediate
6//! [`crate::SchemaDocument`]/[`CompiledSchema`] (e.g. to inspect a schema
7//! without validating anything against it).
8
9use std::fmt;
10
11use crate::{
12    CompiledSchema, DatatypeError, DatatypeRegistry, Element, ParseError, SchemaError,
13    SchemaResolver, SchemaSource, ValidationError, parse, simplify, validate,
14};
15
16/// A parsed and composed ("simplified", in RELAX NG spec terms) schema,
17/// ready to validate any number of documents against.
18///
19/// Compiling — parsing the schema text and resolving/composing any
20/// `include`/`externalRef`/`div`/`combine` it uses — is the relatively
21/// expensive step; [`Schema::compile`] does it once, and the resulting
22/// `Schema` is then reused for every document validated against it.
23///
24/// # Example
25///
26/// ```
27/// use relax_ng::{
28///     Content, DatatypeRegistry, Element, ExpandedName, ResolveError, Schema, SchemaResolver,
29///     SchemaSource, SchemaSyntax,
30/// };
31///
32/// // This schema is self-contained (no `include`/`externalRef`), so the
33/// // resolver is never actually called — it only needs to exist to
34/// // satisfy `Schema::compile`'s signature.
35/// struct NoResolver;
36/// impl SchemaResolver for NoResolver {
37///     fn resolve(&self, href: &str, _base_uri: &str) -> Result<SchemaSource, ResolveError> {
38///         Err(ResolveError::new(format!("no such resource: {href}")))
39///     }
40/// }
41///
42/// let source = SchemaSource::new(
43///     "element greeting { attribute lang { text }, text }",
44///     "memory:/greeting.rnc",
45///     SchemaSyntax::Compact,
46/// );
47/// let schema = Schema::compile(&source, &NoResolver).expect("schema compiles");
48///
49/// // A minimal `Element` implementation over an in-memory tree — a real
50/// // caller implements this over whatever tree type it already has (an
51/// // XML DOM, an HTML parser's tree, ...); this crate parses none of
52/// // that itself.
53/// struct Doc {
54///     name: &'static str,
55///     attributes: Vec<(&'static str, &'static str)>,
56///     text: &'static str,
57/// }
58/// impl Element for Doc {
59///     type Location = ();
60///
61///     fn name(&self) -> ExpandedName {
62///         ExpandedName::new(None::<String>, self.name)
63///     }
64///     fn attributes(&self) -> impl Iterator<Item = (ExpandedName, String)> {
65///         self.attributes
66///             .iter()
67///             .map(|(name, value)| (ExpandedName::new(None::<String>, *name), value.to_string()))
68///     }
69///     fn children(&self) -> impl Iterator<Item = Content<Self>> {
70///         std::iter::once(Content::Text(self.text.to_owned()))
71///     }
72/// }
73///
74/// let document = Doc {
75///     name: "greeting",
76///     attributes: vec![("lang", "en")],
77///     text: "hello",
78/// };
79///
80/// // The built-in `string`/`token` datatypes are all this schema needs.
81/// let errors = schema
82///     .validate(&DatatypeRegistry::new(), &document)
83///     .expect("only built-in/XSD datatypes are used");
84/// assert!(errors.is_empty());
85/// ```
86#[derive(Clone, Debug)]
87pub struct Schema {
88    compiled: CompiledSchema,
89}
90
91impl Schema {
92    /// Parses `source` and composes it into a ready-to-validate schema.
93    /// `resolver` is consulted for any `include`/`externalRef` the schema
94    /// (transitively) uses; if the schema is known to be self-contained,
95    /// any resolver that always returns `Err` works (it will never
96    /// actually be called).
97    pub fn compile(
98        source: &SchemaSource,
99        resolver: &impl SchemaResolver,
100    ) -> Result<Schema, CompileError> {
101        let document = parse(source)?;
102        let compiled = simplify(&document, resolver)?;
103        Ok(Schema { compiled })
104    }
105
106    /// Validates `root` (and, transitively, everything reachable from it)
107    /// against this schema. `registry` supplies the `data`/`value`
108    /// pattern semantics — see [`DatatypeRegistry`] for what's built in
109    /// and how to add more.
110    ///
111    /// Returns the list of problems found — empty means `root` is valid.
112    /// Returns [`DatatypeError`] instead of validating at all if the
113    /// schema uses a `datatypeLibrary`/type/parameter `registry` doesn't
114    /// recognize, since whether the document would pass a check that
115    /// can't be performed is unknowable.
116    pub fn validate<E: Element>(
117        &self,
118        registry: &DatatypeRegistry,
119        root: &E,
120    ) -> Result<Vec<ValidationError<E::Location>>, DatatypeError> {
121        validate(&self.compiled, registry, root)
122    }
123
124    /// The underlying compiled representation, for callers that need it
125    /// directly (e.g. [`CompiledSchema::definition_count`] for
126    /// diagnostics) rather than going through [`Schema::validate`].
127    pub fn compiled(&self) -> &CompiledSchema {
128        &self.compiled
129    }
130}
131
132/// Either step [`Schema::compile`] performs can fail on its own: `source`
133/// might not even parse (malformed `.rnc`/`.rng` syntax — [`ParseError`]),
134/// or it might parse but fail composition (an undefined `ref`, illegal
135/// recursion, a §7 restriction violation, ... — [`SchemaError`]).
136#[derive(Clone, Debug, Eq, PartialEq)]
137pub enum CompileError {
138    /// `source` isn't well-formed RELAX NG in its declared syntax.
139    Parse(ParseError),
140    /// `source` parsed, but composing/simplifying it failed (an undefined
141    /// `ref`, illegal recursion, a §7 restriction violation, ...).
142    Schema(SchemaError),
143}
144
145impl fmt::Display for CompileError {
146    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            Self::Parse(error) => write!(formatter, "{error}"),
149            Self::Schema(error) => write!(formatter, "{error}"),
150        }
151    }
152}
153
154impl std::error::Error for CompileError {
155    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
156        match self {
157            Self::Parse(error) => Some(error),
158            Self::Schema(error) => Some(error),
159        }
160    }
161}
162
163impl From<ParseError> for CompileError {
164    fn from(error: ParseError) -> Self {
165        Self::Parse(error)
166    }
167}
168
169impl From<SchemaError> for CompileError {
170    fn from(error: SchemaError) -> Self {
171        Self::Schema(error)
172    }
173}