Skip to main content

relax_ng/datatypes/
registry.rs

1//! The plugin surface: a [`DatatypeLibrary`] trait one `datatypeLibrary` URI
2//! implements, and a [`DatatypeRegistry`] mapping URIs to implementations
3//! that [`crate::validate`] takes as a parameter.
4//!
5//! [`DatatypeRegistry::new`] always has the RELAX NG built-in (empty-string)
6//! library and the XSD library (`http://www.w3.org/2001/XMLSchema-datatypes`)
7//! registered — see `plan/DECISIONS.md`'s Phase 06 entry for why both are
8//! treated as "this crate natively supports them" rather than something a
9//! caller must supply. Anything else (e.g. vnu's own
10//! `http://whattf.org/datatype-draft`) is the caller's responsibility to
11//! implement and [`DatatypeRegistry::register`] — this crate has no
12//! knowledge of any non-standard library's types.
13
14use std::collections::BTreeMap;
15use std::fmt;
16
17use super::{builtin, xsd};
18
19/// The namespace URI identifying the XSD datatype library, as used in
20/// `datatypeLibrary="..."` — see `plan/DECISIONS.md`.
21pub const XSD_DATATYPE_LIBRARY: &str = "http://www.w3.org/2001/XMLSchema-datatypes";
22
23/// The in-scope XML namespace prefix bindings at the point a value occurs
24/// in the *document* (not the schema) — needed to resolve `QName`/
25/// `NOTATION`-typed values, whose lexical form (`prefix:local`) only
26/// denotes a value together with a prefix-to-URI binding. Built once per
27/// element from [`crate::Element::namespace_bindings`].
28#[derive(Clone, Debug, Default)]
29pub struct DatatypeContext {
30    bindings: Vec<(String, String)>,
31}
32
33impl DatatypeContext {
34    pub(crate) fn empty() -> Self {
35        Self {
36            bindings: Vec::new(),
37        }
38    }
39
40    pub(crate) fn from_bindings(bindings: Vec<(String, String)>) -> Self {
41        Self { bindings }
42    }
43
44    /// The namespace URI bound to `prefix` (`""` for the default
45    /// namespace), or `None` if no such binding is in scope. Later
46    /// bindings win over earlier ones, matching normal XML shadowing.
47    pub fn resolve(&self, prefix: &str) -> Option<&str> {
48        self.bindings
49            .iter()
50            .rev()
51            .find(|(bound_prefix, _)| bound_prefix == prefix)
52            .map(|(_, uri)| uri.as_str())
53    }
54}
55
56/// A `datatypeLibrary` implementation: recognizes a set of type names,
57/// each with its own applicable parameters (facets, in XSD's terminology).
58///
59/// Modeled on the datatype interface the RELAX NG spec itself describes
60/// (type validity, parameter validity, value matching, value equality) —
61/// independently designed for Rust, not ported from any existing
62/// implementation.
63pub trait DatatypeLibrary: Send + Sync {
64    /// Checks that `type_name` is known to this library and that `params`
65    /// are individually well-formed and applicable to it (e.g. XSD's
66    /// `pattern` facet takes a regular expression; `minInclusive` only
67    /// applies to ordered types). Called once per `data`/`value` pattern,
68    /// before any document is validated, so a bad schema fails fast with a
69    /// message identifying the problem.
70    fn validate_params(&self, type_name: &str, params: &[(&str, &str)]) -> Result<(), String>;
71
72    /// Whether `value` is a legal value of `type_name` (already checked
73    /// valid via [`Self::validate_params`]) satisfying every param. Schema
74    /// `except` branches are handled by the caller, not here.
75    fn matches(
76        &self,
77        type_name: &str,
78        params: &[(&str, &str)],
79        value: &str,
80        context: &DatatypeContext,
81    ) -> bool;
82
83    /// Whether `expected` (the schema's `<value>` literal, resolved
84    /// against `expected_context` — the schema XML's *own* in-scope
85    /// namespace bindings and its `ns` attribute, for `QName`/`NOTATION`-
86    /// like types) and `actual` (the document's text, resolved against
87    /// `actual_context` — the document's bindings at that point) denote
88    /// the same value of `type_name`. The two contexts are deliberately
89    /// separate: a `<value>` literal's prefixes are the *schema* author's,
90    /// never the validated document's.
91    fn values_equal(
92        &self,
93        type_name: &str,
94        expected: &str,
95        expected_context: &DatatypeContext,
96        actual: &str,
97        actual_context: &DatatypeContext,
98    ) -> bool;
99}
100
101/// The schema uses a `datatypeLibrary`/type/parameter this
102/// [`DatatypeRegistry`] can't handle.
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct DatatypeError {
105    library: String,
106    message: String,
107}
108
109impl DatatypeError {
110    pub(crate) fn unsupported_library(library: impl Into<String>) -> Self {
111        let library = library.into();
112        Self {
113            message: format!("unsupported datatype library `{library}`"),
114            library,
115        }
116    }
117
118    pub(crate) fn type_problem(library: impl Into<String>, type_name: &str, reason: &str) -> Self {
119        let library = library.into();
120        Self {
121            message: format!("datatype `{type_name}` in library `{library}`: {reason}"),
122            library,
123        }
124    }
125
126    /// The offending `datatypeLibrary` URI (the empty string denotes the
127    /// built-in library).
128    pub fn library(&self) -> &str {
129        &self.library
130    }
131}
132
133impl fmt::Display for DatatypeError {
134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135        formatter.write_str(&self.message)
136    }
137}
138
139impl std::error::Error for DatatypeError {}
140
141/// Maps `datatypeLibrary` URIs to [`DatatypeLibrary`] implementations for
142/// one [`crate::validate`] call.
143pub struct DatatypeRegistry {
144    libraries: BTreeMap<String, Box<dyn DatatypeLibrary>>,
145}
146
147impl DatatypeRegistry {
148    /// A registry with the RELAX NG built-in library (`""`) and the XSD
149    /// library registered — the two this crate implements itself. Use
150    /// [`Self::builtin_only`] to reject `xsd:`-typed schemas explicitly, or
151    /// [`Self::register`] to add further libraries (e.g. a caller's own
152    /// non-standard one).
153    pub fn new() -> Self {
154        let mut registry = Self::builtin_only();
155        registry.register(XSD_DATATYPE_LIBRARY, xsd::XsdLibrary);
156        registry
157    }
158
159    /// A registry with only the mandatory RELAX NG built-in library (`""`)
160    /// registered — every other `datatypeLibrary`, including XSD, fails
161    /// fast with [`DatatypeError`].
162    pub fn builtin_only() -> Self {
163        let mut libraries: BTreeMap<String, Box<dyn DatatypeLibrary>> = BTreeMap::new();
164        libraries.insert(String::new(), Box::new(builtin::BuiltinLibrary));
165        Self { libraries }
166    }
167
168    /// Registers `library` under `library_uri`, replacing any previous
169    /// registration for that URI (including the built-in ones, should a
170    /// caller want to override them — not recommended, but not prevented).
171    pub fn register(
172        &mut self,
173        library_uri: impl Into<String>,
174        library: impl DatatypeLibrary + 'static,
175    ) -> &mut Self {
176        self.libraries.insert(library_uri.into(), Box::new(library));
177        self
178    }
179
180    pub(crate) fn get(&self, library_uri: &str) -> Option<&dyn DatatypeLibrary> {
181        self.libraries.get(library_uri).map(Box::as_ref)
182    }
183}
184
185impl Default for DatatypeRegistry {
186    fn default() -> Self {
187        Self::new()
188    }
189}