Skip to main content

eure_schema/validate/
context.rs

1//! Validation context and output types
2//!
3//! `ValidationContext` manages state during validation:
4//! - Schema reference
5//! - Current path for error reporting
6//! - Accumulated errors and warnings
7//! - Hole tracking for completeness check
8
9use std::cell::RefCell;
10
11use eure_document::document::EureDocument;
12use eure_document::identifier::Identifier;
13use eure_document::parse::UnionTagMode;
14use eure_document::path::{EurePath, PathSegment};
15use eure_document::value::ObjectKey;
16
17use crate::{SchemaDocument, SchemaNodeContent, SchemaNodeId};
18
19use super::error::{ValidationError, ValidationWarning};
20
21// =============================================================================
22// ValidationOutput (final result for public API)
23// =============================================================================
24
25/// Final validation output returned to callers.
26#[derive(Debug, Clone, Default)]
27pub struct ValidationOutput {
28    /// No type errors (holes are allowed)
29    pub is_valid: bool,
30    /// No type errors AND no holes
31    pub is_complete: bool,
32    /// Type errors encountered during validation
33    pub errors: Vec<ValidationError>,
34    /// Warnings (e.g., unknown extensions)
35    pub warnings: Vec<ValidationWarning>,
36}
37
38// =============================================================================
39// ValidationState (internal mutable state)
40// =============================================================================
41
42/// Internal mutable state during validation.
43///
44/// Wrapped in RefCell to allow interior mutability through shared references.
45#[derive(Debug)]
46pub struct ValidationState {
47    /// Current path in the document (for error reporting)
48    pub path: EurePath,
49    /// Whether any holes have been encountered
50    pub has_holes: bool,
51    /// Accumulated validation errors
52    pub errors: Vec<ValidationError>,
53    /// Accumulated warnings
54    pub warnings: Vec<ValidationWarning>,
55    /// Temporary storage for variant errors during union validation.
56    /// Each entry is (variant_name, variant_schema_id, errors_from_that_variant).
57    pub(crate) variant_errors: Vec<(String, SchemaNodeId, Vec<ValidationError>)>,
58}
59
60impl Default for ValidationState {
61    fn default() -> Self {
62        Self {
63            path: EurePath::root(),
64            has_holes: false,
65            errors: Vec::new(),
66            warnings: Vec::new(),
67            variant_errors: Vec::new(),
68        }
69    }
70}
71
72impl ValidationState {
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Record an error (validation continues).
78    pub fn record_error(&mut self, error: ValidationError) {
79        self.errors.push(error);
80    }
81
82    /// Record a warning.
83    pub fn record_warning(&mut self, warning: ValidationWarning) {
84        self.warnings.push(warning);
85    }
86
87    /// Mark that a hole was encountered.
88    pub fn mark_has_holes(&mut self) {
89        self.has_holes = true;
90    }
91
92    /// Check if any errors have been recorded.
93    pub fn has_errors(&self) -> bool {
94        !self.errors.is_empty()
95    }
96
97    /// Get the number of errors.
98    pub fn error_count(&self) -> usize {
99        self.errors.len()
100    }
101
102    // -------------------------------------------------------------------------
103    // Path management
104    // -------------------------------------------------------------------------
105
106    /// Push an identifier to the path.
107    pub fn push_path_ident(&mut self, ident: Identifier) {
108        self.path.0.push(PathSegment::Ident(ident));
109    }
110
111    /// Push a key to the path.
112    pub fn push_path_key(&mut self, key: ObjectKey) {
113        self.path.0.push(PathSegment::Value(key));
114    }
115
116    /// Push an array index to the path.
117    pub fn push_path_index(&mut self, index: usize) {
118        self.path.0.push(PathSegment::ArrayIndex(Some(index)));
119    }
120
121    /// Push a tuple index to the path.
122    pub fn push_path_tuple_index(&mut self, index: u8) {
123        self.path.0.push(PathSegment::TupleIndex(index));
124    }
125
126    /// Push an extension to the path.
127    pub fn push_path_extension(&mut self, ident: Identifier) {
128        self.path.0.push(PathSegment::Extension(ident));
129    }
130
131    /// Pop the last segment from the path.
132    pub fn pop_path(&mut self) {
133        self.path.0.pop();
134    }
135
136    /// Clone for fork (trial validation).
137    pub fn fork(&self) -> Self {
138        Self {
139            path: self.path.clone(),
140            has_holes: self.has_holes,
141            errors: Vec::new(),
142            warnings: Vec::new(),
143            variant_errors: Vec::new(), // Don't inherit variant errors
144        }
145    }
146
147    /// Merge results from a forked state.
148    pub fn merge(&mut self, other: Self) {
149        self.has_holes |= other.has_holes;
150        self.errors.extend(other.errors);
151        self.warnings.extend(other.warnings);
152    }
153
154    /// Consume and produce final output.
155    pub fn finish(self) -> ValidationOutput {
156        ValidationOutput {
157            is_valid: self.errors.is_empty(),
158            is_complete: self.errors.is_empty() && !self.has_holes,
159            errors: self.errors,
160            warnings: self.warnings,
161        }
162    }
163
164    // -------------------------------------------------------------------------
165    // Variant error collection (for union validation)
166    // -------------------------------------------------------------------------
167
168    /// Record variant attempt errors (for union validation).
169    pub fn record_variant_errors(
170        &mut self,
171        variant_name: String,
172        schema_id: SchemaNodeId,
173        errors: Vec<ValidationError>,
174    ) {
175        self.variant_errors.push((variant_name, schema_id, errors));
176    }
177
178    /// Take collected variant errors (for union error construction).
179    pub fn take_variant_errors(&mut self) -> Vec<(String, SchemaNodeId, Vec<ValidationError>)> {
180        std::mem::take(&mut self.variant_errors)
181    }
182
183    /// Clear variant errors without taking (for successful variant match).
184    pub fn clear_variant_errors(&mut self) {
185        self.variant_errors.clear();
186    }
187}
188
189// =============================================================================
190// ValidationContext (shared immutable + RefCell for state)
191// =============================================================================
192
193/// Validation context combining schema reference and mutable state.
194///
195/// Uses interior mutability (RefCell) to allow validators implementing
196/// `DocumentParser` to record errors without requiring `&mut self`.
197pub struct ValidationContext<'a> {
198    /// Reference to the schema being validated against
199    pub schema: &'a SchemaDocument,
200    /// Reference to the document being validated
201    pub document: &'a EureDocument,
202    /// Mutable state (errors, warnings, path, holes)
203    pub state: RefCell<ValidationState>,
204    /// Union tag mode for this validation context.
205    pub union_tag_mode: UnionTagMode,
206}
207
208impl<'a> ValidationContext<'a> {
209    /// Create a new validation context with default (Eure) mode.
210    pub fn new(document: &'a EureDocument, schema: &'a SchemaDocument) -> Self {
211        Self::with_mode(document, schema, UnionTagMode::default())
212    }
213
214    /// Create a new validation context with specified union tag mode.
215    pub fn with_mode(
216        document: &'a EureDocument,
217        schema: &'a SchemaDocument,
218        union_tag_mode: UnionTagMode,
219    ) -> Self {
220        Self {
221            schema,
222            document,
223            state: RefCell::new(ValidationState::new()),
224            union_tag_mode,
225        }
226    }
227
228    /// Create a context with existing state (for fork/merge).
229    pub fn with_state(
230        document: &'a EureDocument,
231        schema: &'a SchemaDocument,
232        state: ValidationState,
233    ) -> Self {
234        Self::with_state_and_mode(document, schema, state, UnionTagMode::default())
235    }
236
237    /// Create a context with existing state and mode (for fork/merge).
238    pub fn with_state_and_mode(
239        document: &'a EureDocument,
240        schema: &'a SchemaDocument,
241        state: ValidationState,
242        union_tag_mode: UnionTagMode,
243    ) -> Self {
244        Self {
245            schema,
246            document,
247            state: RefCell::new(state),
248            union_tag_mode,
249        }
250    }
251
252    /// Record an error.
253    pub fn record_error(&self, error: ValidationError) {
254        self.state.borrow_mut().record_error(error);
255    }
256
257    /// Record a warning.
258    pub fn record_warning(&self, warning: ValidationWarning) {
259        self.state.borrow_mut().record_warning(warning);
260    }
261
262    /// Mark that a hole was encountered.
263    pub fn mark_has_holes(&self) {
264        self.state.borrow_mut().mark_has_holes();
265    }
266
267    /// Check if any errors have been recorded.
268    pub fn has_errors(&self) -> bool {
269        self.state.borrow().has_errors()
270    }
271
272    /// Get the current error count.
273    pub fn error_count(&self) -> usize {
274        self.state.borrow().error_count()
275    }
276
277    /// Get a clone of the current path.
278    pub fn path(&self) -> EurePath {
279        self.state.borrow().path.clone()
280    }
281
282    /// Push an identifier to the path.
283    pub fn push_path_ident(&self, ident: Identifier) {
284        self.state.borrow_mut().push_path_ident(ident);
285    }
286
287    /// Push a key to the path.
288    pub fn push_path_key(&self, key: ObjectKey) {
289        self.state.borrow_mut().push_path_key(key);
290    }
291
292    /// Push an array index to the path.
293    pub fn push_path_index(&self, index: usize) {
294        self.state.borrow_mut().push_path_index(index);
295    }
296
297    /// Push a tuple index to the path.
298    pub fn push_path_tuple_index(&self, index: u8) {
299        self.state.borrow_mut().push_path_tuple_index(index);
300    }
301
302    /// Push an extension to the path.
303    pub fn push_path_extension(&self, ident: Identifier) {
304        self.state.borrow_mut().push_path_extension(ident);
305    }
306
307    /// Pop the last segment from the path.
308    pub fn pop_path(&self) {
309        self.state.borrow_mut().pop_path();
310    }
311
312    /// Fork for trial validation (returns forked state).
313    pub fn fork_state(&self) -> ValidationState {
314        self.state.borrow().fork()
315    }
316
317    /// Merge forked state back.
318    pub fn merge_state(&self, other: ValidationState) {
319        self.state.borrow_mut().merge(other);
320    }
321
322    /// Resolve type references to get the actual schema content.
323    pub fn resolve_schema_content(&self, schema_id: SchemaNodeId) -> &SchemaNodeContent {
324        let mut current_id = schema_id;
325        // Limit iterations to prevent infinite loops with circular refs
326        for _ in 0..100 {
327            let content = &self.schema.node(current_id).content;
328            match content {
329                SchemaNodeContent::Reference(type_ref) => {
330                    if type_ref.namespace.is_some() {
331                        return content; // Cross-schema refs not resolved
332                    }
333                    if let Some(&resolved_id) = self.schema.types.get(&type_ref.name) {
334                        current_id = resolved_id;
335                    } else {
336                        return content; // Unresolved reference
337                    }
338                }
339                _ => return content,
340            }
341        }
342        &self.schema.node(current_id).content
343    }
344
345    /// Create a ParseContext for the given node with this context's union tag mode.
346    pub fn parse_context(
347        &self,
348        node_id: eure_document::document::NodeId,
349    ) -> eure_document::parse::ParseContext<'a> {
350        eure_document::parse::ParseContext::with_union_tag_mode(
351            self.document,
352            node_id,
353            self.union_tag_mode,
354        )
355    }
356
357    // -------------------------------------------------------------------------
358    // Variant error collection (for union validation)
359    // -------------------------------------------------------------------------
360
361    /// Record variant attempt errors during union validation.
362    pub fn record_variant_errors(
363        &self,
364        variant_name: String,
365        schema_id: SchemaNodeId,
366        errors: Vec<ValidationError>,
367    ) {
368        self.state
369            .borrow_mut()
370            .record_variant_errors(variant_name, schema_id, errors);
371    }
372
373    /// Take collected variant errors.
374    pub fn take_variant_errors(&self) -> Vec<(String, SchemaNodeId, Vec<ValidationError>)> {
375        self.state.borrow_mut().take_variant_errors()
376    }
377
378    /// Clear variant errors.
379    pub fn clear_variant_errors(&self) {
380        self.state.borrow_mut().clear_variant_errors();
381    }
382
383    /// Consume context and produce final output.
384    pub fn finish(self) -> ValidationOutput {
385        self.state.into_inner().finish()
386    }
387}