Skip to main content

yaml_schema/validation/
context.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::rc::Rc;
5
6use crate::RootSchema;
7use crate::YamlSchema;
8use crate::validation::ArrayUnevaluatedAnnotations;
9use crate::validation::ObjectEvaluatedNames;
10use crate::validation::ValidationError;
11
12/// The validation context
13#[derive(Debug)]
14pub struct Context<'r> {
15    /// We use an Option here so tests can be run without a root schema
16    pub root_schema: Option<&'r RootSchema>,
17    pub current_schema: Option<&'r YamlSchema>,
18    pub current_path: Vec<String>,
19    pub stream_started: bool,
20    pub stream_ended: bool,
21    pub errors: Rc<RefCell<Vec<ValidationError>>>,
22    pub fail_fast: bool,
23    /// Tracks `($ref, value_position)` pairs currently being resolved to detect circular references.
24    /// The value position is the byte offset of the YAML value's span start, so the same ref
25    /// applied to a nested value is allowed (legitimate recursion) while the same ref
26    /// on the same value is detected as a cycle.
27    pub resolving_refs: Rc<RefCell<HashSet<(String, usize)>>>,
28    /// Cache of externally loaded schemas by absolute URI (without fragment) or `$id` when valid.
29    pub schemas: Rc<RefCell<HashMap<String, Rc<RootSchema>>>>,
30    /// Property names successfully evaluated for JSON Schema `unevaluatedProperties` (same instance).
31    pub object_evaluated: Option<ObjectEvaluatedNames>,
32    /// Array annotation state for JSON Schema `unevaluatedItems` (same instance).
33    pub array_unevaluated: Option<Rc<RefCell<ArrayUnevaluatedAnnotations>>>,
34}
35
36impl Default for Context<'_> {
37    fn default() -> Self {
38        Self {
39            root_schema: None,
40            current_schema: None,
41            current_path: Vec::new(),
42            stream_started: false,
43            stream_ended: false,
44            errors: Rc::new(RefCell::new(Vec::new())),
45            fail_fast: false,
46            resolving_refs: Rc::new(RefCell::new(HashSet::new())),
47            schemas: Rc::new(RefCell::new(HashMap::new())),
48            object_evaluated: None,
49            array_unevaluated: None,
50        }
51    }
52}
53
54impl<'r> Context<'r> {
55    /// Returns true if there are any errors in the context
56    pub fn has_errors(&self) -> bool {
57        !self.errors.borrow().is_empty()
58    }
59
60    /// Returns the current path as a string separated by "."
61    pub fn path(&self) -> String {
62        self.current_path.join(".")
63    }
64
65    pub fn new(fail_fast: bool) -> Context<'r> {
66        Context {
67            fail_fast,
68            ..Default::default()
69        }
70    }
71
72    pub fn get_sub_context(&self) -> Context<'r> {
73        Context {
74            root_schema: self.root_schema,
75            current_schema: self.current_schema,
76            current_path: self.current_path.clone(),
77            stream_started: self.stream_started,
78            stream_ended: self.stream_ended,
79            errors: Rc::new(RefCell::new(Vec::new())),
80            fail_fast: self.fail_fast,
81            resolving_refs: self.resolving_refs.clone(),
82            schemas: self.schemas.clone(),
83            object_evaluated: self.object_evaluated.clone(),
84            array_unevaluated: self.array_unevaluated.clone(),
85        }
86    }
87
88    /// Like [`get_sub_context`], but with fresh unevaluated annotation carriers (for `anyOf` / `oneOf` branches).
89    pub fn get_sub_context_fresh_eval(&self) -> Context<'r> {
90        Context {
91            root_schema: self.root_schema,
92            current_schema: self.current_schema,
93            current_path: self.current_path.clone(),
94            stream_started: self.stream_started,
95            stream_ended: self.stream_ended,
96            errors: Rc::new(RefCell::new(Vec::new())),
97            fail_fast: self.fail_fast,
98            resolving_refs: self.resolving_refs.clone(),
99            schemas: self.schemas.clone(),
100            object_evaluated: Some(ObjectEvaluatedNames::new()),
101            array_unevaluated: Some(ArrayUnevaluatedAnnotations::new_shared()),
102        }
103    }
104
105    pub fn with_root_schema(root_schema: &'r RootSchema, fail_fast: bool) -> Context<'r> {
106        Context {
107            root_schema: Some(root_schema),
108            fail_fast,
109            ..Default::default()
110        }
111    }
112
113    /// Create a context with root schema and pre-loaded schemas (e.g. for CLI -f multiple).
114    pub fn with_root_schema_and_schemas(
115        root_schema: &'r RootSchema,
116        fail_fast: bool,
117        schemas: HashMap<String, Rc<RootSchema>>,
118    ) -> Context<'r> {
119        Context {
120            root_schema: Some(root_schema),
121            fail_fast,
122            schemas: Rc::new(RefCell::new(schemas)),
123            ..Default::default()
124        }
125    }
126
127    fn push_error(&self, error: ValidationError) {
128        self.errors.borrow_mut().push(error);
129    }
130
131    pub fn add_doc_error<V: Into<String>>(&self, error: V) {
132        let path = self.path();
133        self.push_error(ValidationError {
134            path,
135            marker: None,
136            error: error.into(),
137        });
138    }
139
140    /// Adds an error message to the current context, with the current path and with location marker
141    pub fn add_error<V: Into<String>>(&self, marked_yaml: &saphyr::MarkedYaml, error: V) {
142        let path = self.path();
143        self.push_error(ValidationError {
144            path,
145            marker: Some(marked_yaml.span.start),
146            error: error.into(),
147        });
148    }
149
150    /// Appends all the errors to the current context
151    pub fn extend_errors(&self, errors: Vec<ValidationError>) {
152        self.errors.borrow_mut().extend(errors);
153    }
154
155    /// Append a path to the current path
156    pub fn append_path<V: Into<String>>(&self, path: V) -> Context<'r> {
157        let mut new_path = self.current_path.clone();
158        new_path.push(path.into());
159        Context {
160            root_schema: self.root_schema,
161            current_schema: self.current_schema,
162            current_path: new_path,
163            errors: self.errors.clone(),
164            fail_fast: self.fail_fast,
165            stream_ended: self.stream_ended,
166            stream_started: self.stream_started,
167            resolving_refs: self.resolving_refs.clone(),
168            schemas: self.schemas.clone(),
169            object_evaluated: None,
170            array_unevaluated: None,
171        }
172    }
173
174    /// Record a successfully evaluated object property name (`properties` / `patternProperties` / `additionalProperties`).
175    pub fn record_evaluated_property(&self, name: &str) {
176        if let Some(oe) = &self.object_evaluated {
177            oe.insert(name.to_string());
178        }
179    }
180
181    pub fn with_object_evaluated(
182        &self,
183        object_evaluated: Option<ObjectEvaluatedNames>,
184    ) -> Context<'r> {
185        Context {
186            root_schema: self.root_schema,
187            current_schema: self.current_schema,
188            current_path: self.current_path.clone(),
189            stream_started: self.stream_started,
190            stream_ended: self.stream_ended,
191            errors: self.errors.clone(),
192            fail_fast: self.fail_fast,
193            resolving_refs: self.resolving_refs.clone(),
194            schemas: self.schemas.clone(),
195            object_evaluated,
196            array_unevaluated: self.array_unevaluated.clone(),
197        }
198    }
199
200    pub fn with_array_unevaluated(
201        &self,
202        array_unevaluated: Option<Rc<RefCell<ArrayUnevaluatedAnnotations>>>,
203    ) -> Context<'r> {
204        Context {
205            root_schema: self.root_schema,
206            current_schema: self.current_schema,
207            current_path: self.current_path.clone(),
208            stream_started: self.stream_started,
209            stream_ended: self.stream_ended,
210            errors: self.errors.clone(),
211            fail_fast: self.fail_fast,
212            resolving_refs: self.resolving_refs.clone(),
213            schemas: self.schemas.clone(),
214            object_evaluated: self.object_evaluated.clone(),
215            array_unevaluated,
216        }
217    }
218
219    /// Returns `true` if the given ref is already being resolved for the given
220    /// YAML value (identified by its span start index), indicating a cycle.
221    pub fn is_resolving_ref(&self, ref_name: &str, value: &saphyr::MarkedYaml) -> bool {
222        let key = (ref_name.to_string(), value.span.start.index());
223        self.resolving_refs.borrow().contains(&key)
224    }
225
226    /// Mark a `(ref, value_position)` pair as currently being resolved.
227    pub fn begin_resolving_ref(&self, ref_name: &str, value: &saphyr::MarkedYaml) {
228        let key = (ref_name.to_string(), value.span.start.index());
229        self.resolving_refs.borrow_mut().insert(key);
230    }
231
232    /// Remove a `(ref, value_position)` pair from the resolving set after resolution completes.
233    pub fn end_resolving_ref(&self, ref_name: &str, value: &saphyr::MarkedYaml) {
234        let key = (ref_name.to_string(), value.span.start.index());
235        self.resolving_refs.borrow_mut().remove(&key);
236    }
237}