Skip to main content

xsd_schema/validation/
validator.rs

1//! Core `SchemaValidator` — immutable validation configuration.
2//!
3//! `SchemaValidator` holds the compiled schema reference, substitution groups,
4//! and validation flags. It is reusable across multiple validation runs.
5//!
6//! Callers create a per-run [`super::runtime::ValidationRuntime`] via
7//! [`SchemaValidator::start_run()`] to perform actual validation.
8
9use crate::compiler::{build_substitution_group_map, SubstitutionGroupMap};
10use crate::schema::SchemaSet;
11
12use super::errors::ValidationError;
13use super::info::ValidationFlags;
14use super::runtime::ValidationRuntime;
15
16// ---------------------------------------------------------------------------
17// ValidationSink trait
18// ---------------------------------------------------------------------------
19
20/// Sink for validation errors and warnings
21///
22/// Implement this trait to receive validation messages from `SchemaValidator`.
23pub trait ValidationSink {
24    /// Report a validation error
25    fn on_error(&mut self, error: ValidationError);
26    /// Report a validation warning
27    fn on_warning(&mut self, warning: ValidationWarning);
28}
29
30/// A validation warning (non-fatal)
31#[derive(Debug, Clone)]
32pub struct ValidationWarning {
33    /// Warning code
34    pub code: &'static str,
35    /// Human-readable message
36    pub message: String,
37    /// Source location in the instance document
38    pub location: Option<crate::parser::location::SourceLocation>,
39}
40
41impl std::fmt::Display for ValidationWarning {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "[{}] {}", self.code, self.message)?;
44        if let Some(loc) = &self.location {
45            write!(f, " at {}", loc)?;
46        }
47        Ok(())
48    }
49}
50
51// ---------------------------------------------------------------------------
52// Built-in sinks
53// ---------------------------------------------------------------------------
54
55/// Collects errors into a `Vec<ValidationError>` and warnings into a `Vec<ValidationWarning>`
56pub struct CollectingValidationSink<'a> {
57    pub errors: &'a mut Vec<ValidationError>,
58    pub warnings: &'a mut Vec<ValidationWarning>,
59}
60
61impl<'a> ValidationSink for CollectingValidationSink<'a> {
62    fn on_error(&mut self, error: ValidationError) {
63        self.errors.push(error);
64    }
65    fn on_warning(&mut self, warning: ValidationWarning) {
66        self.warnings.push(warning);
67    }
68}
69
70/// Collects errors only; discards warnings
71pub struct ErrorOnlySink<'a> {
72    pub errors: &'a mut Vec<ValidationError>,
73}
74
75impl<'a> ValidationSink for ErrorOnlySink<'a> {
76    fn on_error(&mut self, error: ValidationError) {
77        self.errors.push(error);
78    }
79    fn on_warning(&mut self, _warning: ValidationWarning) {
80        // Discarded
81    }
82}
83
84// ---------------------------------------------------------------------------
85// AssertionSource — mutual exclusion for assertion evaluation paths
86// ---------------------------------------------------------------------------
87
88/// Selects which assertion evaluation path is active.
89///
90/// XSD 1.1 assertions can be evaluated via two mutually exclusive paths:
91/// - `FragmentBuffer` — inline fragment buffering during streaming validation
92/// - `MainDocument` — external `BufferDocument`, assertions deferred to Phase 2
93///
94/// The `Disabled` default means no assertion evaluation occurs.
95#[cfg(feature = "xsd11")]
96#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
97pub enum AssertionSource {
98    /// No assertion evaluation. `PROCESS_ASSERTIONS` must NOT be set.
99    #[default]
100    Disabled,
101    /// Inline fragment buffering. `PROCESS_ASSERTIONS` MUST be set.
102    FragmentBuffer,
103    /// External `BufferDocument`. `PROCESS_ASSERTIONS` must NOT be set.
104    MainDocument,
105}
106
107// ---------------------------------------------------------------------------
108// SchemaValidator — immutable configuration
109// ---------------------------------------------------------------------------
110
111/// Immutable validation configuration — reusable across runs.
112///
113/// Holds the compiled schema reference, substitution groups, and validation
114/// flags. Create a per-run [`ValidationRuntime`] via [`Self::start_run()`].
115pub struct SchemaValidator<'a> {
116    /// The compiled schema set to validate against
117    pub(crate) schema_set: &'a SchemaSet,
118    /// Pre-built substitution group map (if any)
119    pub(crate) subst_groups: Option<SubstitutionGroupMap>,
120    /// Validation flags controlling behaviour
121    pub(crate) flags: ValidationFlags,
122    /// Which assertion evaluation path is active (XSD 1.1 only)
123    #[cfg(feature = "xsd11")]
124    pub(crate) assertion_source: AssertionSource,
125}
126
127impl<'a> SchemaValidator<'a> {
128    /// Create a new `SchemaValidator` with default assertion mode (`Disabled`).
129    ///
130    /// `PROCESS_ASSERTIONS` is silently stripped from `flags` because the
131    /// default mode is `Disabled`, and the two must agree. Use
132    /// [`Self::new_fragment_buffer()`] or [`Self::new_main_document()`] to
133    /// enable assertion processing.
134    pub fn new(schema_set: &'a SchemaSet, flags: ValidationFlags) -> Self {
135        #[cfg(feature = "xsd11")]
136        let flags = flags & !ValidationFlags::PROCESS_ASSERTIONS;
137        let subst_groups = build_substitution_group_map(schema_set);
138        SchemaValidator {
139            schema_set,
140            subst_groups: Some(subst_groups),
141            flags,
142            #[cfg(feature = "xsd11")]
143            assertion_source: AssertionSource::default(),
144        }
145    }
146
147    /// Create a new `SchemaValidator` with pre-built substitution groups.
148    pub fn with_substitution_groups(
149        schema_set: &'a SchemaSet,
150        flags: ValidationFlags,
151        subst_groups: SubstitutionGroupMap,
152    ) -> Self {
153        SchemaValidator {
154            subst_groups: Some(subst_groups),
155            ..Self::new(schema_set, flags)
156        }
157    }
158
159    /// XSD 1.1: forces `PROCESS_ASSERTIONS` flag, sets `FragmentBuffer` mode.
160    #[cfg(feature = "xsd11")]
161    pub fn new_fragment_buffer(schema_set: &'a SchemaSet, flags: ValidationFlags) -> Self {
162        let mut v = Self::new(schema_set, flags);
163        v.flags |= ValidationFlags::PROCESS_ASSERTIONS;
164        v.assertion_source = AssertionSource::FragmentBuffer;
165        v
166    }
167
168    /// XSD 1.1: clears `PROCESS_ASSERTIONS` flag, sets `MainDocument` mode.
169    #[cfg(feature = "xsd11")]
170    pub fn new_main_document(schema_set: &'a SchemaSet, flags: ValidationFlags) -> Self {
171        let flags = flags & !ValidationFlags::PROCESS_ASSERTIONS;
172        let mut v = Self::new(schema_set, flags);
173        v.assertion_source = AssertionSource::MainDocument;
174        v
175    }
176
177    /// Set the assertion evaluation source.
178    ///
179    /// Enforces the mutual exclusion contract between `PROCESS_ASSERTIONS`
180    /// flag and the chosen `AssertionSource` mode:
181    /// - `FragmentBuffer` requires `PROCESS_ASSERTIONS` to be set
182    /// - `Disabled` and `MainDocument` require it to NOT be set
183    ///
184    /// # Panics
185    /// Panics if the flag/mode combination is invalid.
186    #[cfg(feature = "xsd11")]
187    #[allow(dead_code)] // Future non-test callers will use this
188    pub(crate) fn set_assertion_source(&mut self, source: AssertionSource) -> &mut Self {
189        let has_flag = self.flags.contains(ValidationFlags::PROCESS_ASSERTIONS);
190        match source {
191            AssertionSource::FragmentBuffer => {
192                assert!(
193                    has_flag,
194                    "AssertionSource::FragmentBuffer requires ValidationFlags::PROCESS_ASSERTIONS"
195                );
196            }
197            AssertionSource::Disabled | AssertionSource::MainDocument => {
198                assert!(
199                    !has_flag,
200                    "AssertionSource::{:?} requires PROCESS_ASSERTIONS to NOT be set",
201                    source
202                );
203            }
204        }
205        self.assertion_source = source;
206        self
207    }
208
209    /// Create a mutable runtime for one validation pass.
210    pub fn start_run<S: ValidationSink>(&self, sink: S) -> ValidationRuntime<'_, S> {
211        ValidationRuntime::new(
212            self.schema_set,
213            &self.subst_groups,
214            self.flags,
215            sink,
216            #[cfg(feature = "xsd11")]
217            self.assertion_source,
218        )
219    }
220}
221
222// ---------------------------------------------------------------------------
223// Tests (config-only — no validation method calls)
224// ---------------------------------------------------------------------------
225
226#[cfg(test)]
227#[cfg(feature = "xsd11")]
228mod assertion_source_tests {
229    use super::*;
230    use crate::pipeline::load_and_process_schema;
231
232    fn load_schema(xsd: &str) -> SchemaSet {
233        let mut schema_set = SchemaSet::new();
234        load_and_process_schema(xsd.as_bytes(), "test.xsd", &mut schema_set, None)
235            .expect("failed to load schema");
236        schema_set
237    }
238
239    #[test]
240    fn test_assertion_source_default_is_disabled() {
241        let schema_set = load_schema(
242            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
243                <xs:element name="root" type="xs:string"/>
244            </xs:schema>"#,
245        );
246        let v = SchemaValidator::new(&schema_set, ValidationFlags::default());
247        assert_eq!(v.assertion_source, AssertionSource::Disabled);
248    }
249
250    #[test]
251    fn test_fragment_buffer_constructor() {
252        let schema_set = load_schema(
253            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
254                <xs:element name="root" type="xs:string"/>
255            </xs:schema>"#,
256        );
257        let v = SchemaValidator::new_fragment_buffer(&schema_set, ValidationFlags::default());
258        assert_eq!(v.assertion_source, AssertionSource::FragmentBuffer);
259        assert!(v.flags.contains(ValidationFlags::PROCESS_ASSERTIONS));
260    }
261
262    #[test]
263    fn test_new_strips_process_assertions_flag() {
264        let schema_set = load_schema(
265            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
266                <xs:element name="root" type="xs:string"/>
267            </xs:schema>"#,
268        );
269        // Passing PROCESS_ASSERTIONS to new() is silently stripped
270        let flags = ValidationFlags::default() | ValidationFlags::PROCESS_ASSERTIONS;
271        let v = SchemaValidator::new(&schema_set, flags);
272        assert!(!v.flags.contains(ValidationFlags::PROCESS_ASSERTIONS));
273        assert_eq!(v.assertion_source, AssertionSource::Disabled);
274    }
275
276    #[test]
277    #[should_panic(expected = "PROCESS_ASSERTIONS")]
278    fn test_fragment_buffer_without_flag_panics() {
279        let schema_set = load_schema(
280            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
281                <xs:element name="root" type="xs:string"/>
282            </xs:schema>"#,
283        );
284        let mut v = SchemaValidator::new(&schema_set, ValidationFlags::default());
285        v.set_assertion_source(AssertionSource::FragmentBuffer);
286    }
287
288    #[test]
289    #[should_panic(expected = "PROCESS_ASSERTIONS")]
290    fn test_disabled_with_flag_panics() {
291        let schema_set = load_schema(
292            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
293                <xs:element name="root" type="xs:string"/>
294            </xs:schema>"#,
295        );
296        // Use new_fragment_buffer to get PROCESS_ASSERTIONS set, then
297        // attempt to switch to Disabled — should panic.
298        let mut v = SchemaValidator::new_fragment_buffer(&schema_set, ValidationFlags::default());
299        v.set_assertion_source(AssertionSource::Disabled);
300    }
301
302    #[test]
303    #[should_panic(expected = "PROCESS_ASSERTIONS")]
304    fn test_main_document_with_flag_panics() {
305        let schema_set = load_schema(
306            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
307                <xs:element name="root" type="xs:string"/>
308            </xs:schema>"#,
309        );
310        // Use new_fragment_buffer to get PROCESS_ASSERTIONS set, then
311        // attempt to switch to MainDocument — should panic.
312        let mut v = SchemaValidator::new_fragment_buffer(&schema_set, ValidationFlags::default());
313        v.set_assertion_source(AssertionSource::MainDocument);
314    }
315
316    #[test]
317    fn test_main_document_without_flag_ok() {
318        let schema_set = load_schema(
319            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
320                <xs:element name="root" type="xs:string"/>
321            </xs:schema>"#,
322        );
323        let mut v = SchemaValidator::new(&schema_set, ValidationFlags::default());
324        v.set_assertion_source(AssertionSource::MainDocument);
325        assert_eq!(v.assertion_source, AssertionSource::MainDocument);
326    }
327}