1use std::collections::HashSet;
15
16use eure_document::identifier::Identifier;
17use eure_document::parse::variant_path::VariantPath;
18use eure_document::path::PathSegment;
19use eure_document::value::ObjectKey;
20use indexmap::IndexMap;
21
22use crate::{
23 RecordFieldSchema, SchemaDocument, SchemaNodeContent, SchemaNodeId, UnknownFieldsPolicy,
24};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct VariantHint {
29 pub prefix_len: usize,
31 pub variant: VariantPath,
33}
34
35#[derive(Debug, Clone, Copy)]
37pub struct SchemaNavigator<'a> {
38 schema: &'a SchemaDocument,
39}
40
41impl<'a> SchemaNavigator<'a> {
42 pub fn new(schema: &'a SchemaDocument) -> Self {
43 Self { schema }
44 }
45
46 pub fn schema(&self) -> &'a SchemaDocument {
47 self.schema
48 }
49
50 pub fn resolve(&self, path: &[PathSegment], hints: &[VariantHint]) -> Vec<SchemaNodeId> {
56 self.resolve_from(self.schema.root, path, hints)
57 }
58
59 pub fn resolve_from(
61 &self,
62 start: SchemaNodeId,
63 path: &[PathSegment],
64 hints: &[VariantHint],
65 ) -> Vec<SchemaNodeId> {
66 let mut candidates = self.concretize(start, hint_at(hints, 0));
67 for (index, segment) in path.iter().enumerate() {
68 let hint = hint_at(hints, index + 1);
69 let mut next = Vec::new();
70 for candidate in candidates {
71 if let Some(stepped) = self.step(candidate, segment) {
72 for concrete in self.concretize(stepped, hint) {
73 if !next.contains(&concrete) {
74 next.push(concrete);
75 }
76 }
77 }
78 }
79 if next.is_empty() {
80 return next;
81 }
82 candidates = next;
83 }
84 candidates
85 }
86
87 pub fn resolve_to_union(
92 &self,
93 path: &[PathSegment],
94 hints: &[VariantHint],
95 ) -> Vec<SchemaNodeId> {
96 let Some((last, prefix)) = path.split_last() else {
97 return self
98 .deref_references(self.schema.root)
99 .into_iter()
100 .collect();
101 };
102 let parents = self.resolve(prefix, hints);
103 let mut result = Vec::new();
104 for parent in parents {
105 if let Some(stepped) = self.step(parent, last)
106 && let Some(node) = self.deref_references(stepped)
107 && !result.contains(&node)
108 {
109 result.push(node);
110 }
111 }
112 result
113 }
114
115 pub fn descend_variants(&self, id: SchemaNodeId, path: &VariantPath) -> Vec<SchemaNodeId> {
120 let mut current = self.deref_references(id).into_iter().collect::<Vec<_>>();
121 for name in path.segments() {
122 let mut next = Vec::new();
123 for node in current {
124 if let SchemaNodeContent::Union(union) = &self.schema.node(node).content
125 && let Some(&variant) = union.variants.get(name.as_ref())
126 && let Some(variant) = self.deref_references(variant)
127 && !next.contains(&variant)
128 {
129 next.push(variant);
130 }
131 }
132 current = next;
133 }
134 current
135 }
136
137 pub fn deref_references(&self, mut id: SchemaNodeId) -> Option<SchemaNodeId> {
141 let mut visited = HashSet::new();
142 loop {
143 if !visited.insert(id) {
144 return None;
145 }
146 match &self.schema.node(id).content {
147 SchemaNodeContent::Reference(reference) => {
148 id = self.schema.resolve_reference(reference)?;
149 }
150 _ => return Some(id),
151 }
152 }
153 }
154
155 pub fn concretize(&self, id: SchemaNodeId, hint: Option<&VariantPath>) -> Vec<SchemaNodeId> {
158 let mut out = Vec::new();
159 let mut visited = HashSet::new();
160 self.concretize_into(id, hint, &mut out, &mut visited);
161 out
162 }
163
164 fn concretize_into(
165 &self,
166 id: SchemaNodeId,
167 hint: Option<&VariantPath>,
168 out: &mut Vec<SchemaNodeId>,
169 visited: &mut HashSet<SchemaNodeId>,
170 ) {
171 if !visited.insert(id) {
172 return;
173 }
174 match &self.schema.node(id).content {
175 SchemaNodeContent::Reference(reference) => {
176 if let Some(target) = self.schema.resolve_reference(reference) {
177 self.concretize_into(target, hint, out, visited);
178 }
179 }
180 SchemaNodeContent::Union(union) => match hint {
181 Some(variant_path) => {
182 let Some(first) = variant_path.first() else {
183 return;
184 };
185 if let Some(&variant_id) = union.variants.get(first.as_ref()) {
186 let rest = variant_path.rest();
187 self.concretize_into(variant_id, rest.as_ref(), out, visited);
188 }
189 }
190 None => {
191 for &variant_id in union.variants.values() {
192 self.concretize_into(variant_id, None, out, visited);
193 }
194 }
195 },
196 _ => {
197 if !out.contains(&id) {
198 out.push(id);
199 }
200 }
201 }
202 }
203
204 pub fn step(&self, from: SchemaNodeId, segment: &PathSegment) -> Option<SchemaNodeId> {
209 let node = self.schema.node(from);
210 match segment {
211 PathSegment::Ident(ident) => self.step_string_key(from, ident.as_ref()),
212 PathSegment::Value(ObjectKey::String(name)) => self.step_string_key(from, name),
213 PathSegment::Value(_) | PathSegment::PartialValue(_) => match &node.content {
214 SchemaNodeContent::Map(map) => Some(map.value),
215 _ => None,
216 },
217 PathSegment::ArrayIndex(_) => match &node.content {
218 SchemaNodeContent::Array(array) => Some(array.item),
219 _ => None,
220 },
221 PathSegment::TupleIndex(index) => match &node.content {
222 SchemaNodeContent::Tuple(tuple) => tuple.elements.get(*index as usize).copied(),
223 _ => None,
224 },
225 PathSegment::Extension(name) => node.ext_types.get(name).map(|ext| ext.schema),
226 PathSegment::HoleKey(_) => None,
227 }
228 }
229
230 fn step_string_key(&self, from: SchemaNodeId, name: &str) -> Option<SchemaNodeId> {
231 match &self.schema.node(from).content {
232 SchemaNodeContent::Record(record) => {
233 if let Some(field) = self.record_fields(from).get(name) {
234 return Some(field.schema);
235 }
236 match &record.unknown_fields {
237 UnknownFieldsPolicy::Schema(id) => Some(*id),
238 UnknownFieldsPolicy::Deny | UnknownFieldsPolicy::Allow => None,
239 }
240 }
241 SchemaNodeContent::Map(map) => Some(map.value),
242 _ => None,
243 }
244 }
245
246 pub fn record_fields(&self, id: SchemaNodeId) -> IndexMap<String, &'a RecordFieldSchema> {
250 let mut fields = IndexMap::new();
251 let mut visited = HashSet::new();
252 self.collect_record_fields(id, &mut fields, &mut visited);
253 fields
254 }
255
256 fn collect_record_fields(
257 &self,
258 id: SchemaNodeId,
259 fields: &mut IndexMap<String, &'a RecordFieldSchema>,
260 visited: &mut HashSet<SchemaNodeId>,
261 ) {
262 if !visited.insert(id) {
263 return;
264 }
265 let SchemaNodeContent::Record(record) = &self.schema.node(id).content else {
266 return;
267 };
268 for (name, field) in &record.properties {
269 fields.entry(name.clone()).or_insert(field);
270 }
271 for &flatten_id in &record.flatten {
272 for concrete in self.concretize(flatten_id, None) {
273 self.collect_record_fields(concrete, fields, visited);
274 }
275 }
276 }
277
278 pub fn extension_names(&self, id: SchemaNodeId) -> impl Iterator<Item = &'a Identifier> {
280 self.schema.node(id).ext_types.keys()
281 }
282}
283
284pub fn hint_at(hints: &[VariantHint], prefix_len: usize) -> Option<&VariantPath> {
287 hints
288 .iter()
289 .find(|hint| hint.prefix_len == prefix_len)
290 .map(|hint| &hint.variant)
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use crate::{ArraySchema, RecordSchema, TextSchema, TypeReference, UnionSchema};
297 use eure_document::path::ArrayIndexKind;
298 use indexmap::IndexSet;
299
300 fn ident(s: &str) -> PathSegment {
301 PathSegment::Ident(s.parse().unwrap())
302 }
303
304 fn field(schema: SchemaNodeId) -> RecordFieldSchema {
305 RecordFieldSchema {
306 schema,
307 optional: false,
308 binding_style: None,
309 field_codegen: Default::default(),
310 }
311 }
312
313 fn fixture() -> SchemaDocument {
315 let mut schema = SchemaDocument::new();
316 let text = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
317 let mut a = RecordSchema::default();
318 a.properties.insert("x".into(), field(text));
319 let a = schema.create_node(SchemaNodeContent::Record(a));
320 let mut b = RecordSchema::default();
321 b.properties.insert("y".into(), field(text));
322 let b = schema.create_node(SchemaNodeContent::Record(b));
323 let mut variants = IndexMap::new();
324 variants.insert("a".to_string(), a);
325 variants.insert("b".to_string(), b);
326 let union = schema.create_node(SchemaNodeContent::Union(UnionSchema {
327 variants,
328 unambiguous: IndexSet::new(),
329 interop: Default::default(),
330 deny_untagged: IndexSet::new(),
331 }));
332 schema.register_type("item".parse().unwrap(), union);
333 let reference = schema.create_node(SchemaNodeContent::Reference(TypeReference::Named {
334 namespace: None,
335 name: "item".parse().unwrap(),
336 }));
337 let array = schema.create_node(SchemaNodeContent::Array(ArraySchema {
338 item: reference,
339 min_length: None,
340 max_length: None,
341 unique: false,
342 contains: None,
343 binding_style: None,
344 }));
345 let mut root = RecordSchema::default();
346 root.properties.insert("items".into(), field(array));
347 schema.node_mut(schema.root).content = SchemaNodeContent::Record(root);
348 schema
349 }
350
351 #[test]
352 fn untagged_union_expands_to_all_variants() {
353 let schema = fixture();
354 let nav = SchemaNavigator::new(&schema);
355 let path = [
356 ident("items"),
357 PathSegment::ArrayIndex(ArrayIndexKind::Push),
358 ];
359 let resolved = nav.resolve(&path, &[]);
360 assert_eq!(resolved.len(), 2);
361 let names: Vec<_> = resolved
362 .iter()
363 .flat_map(|id| nav.record_fields(*id).into_keys())
364 .collect();
365 assert_eq!(names, vec!["x".to_string(), "y".to_string()]);
366 }
367
368 #[test]
369 fn variant_hint_selects_single_variant() {
370 let schema = fixture();
371 let nav = SchemaNavigator::new(&schema);
372 let path = [
373 ident("items"),
374 PathSegment::ArrayIndex(ArrayIndexKind::Push),
375 ];
376 let hints = [VariantHint {
377 prefix_len: 2,
378 variant: VariantPath::parse("b").unwrap(),
379 }];
380 let resolved = nav.resolve(&path, &hints);
381 assert_eq!(resolved.len(), 1);
382 let names: Vec<_> = nav.record_fields(resolved[0]).into_keys().collect();
383 assert_eq!(names, vec!["y".to_string()]);
384 }
385
386 #[test]
387 fn resolve_to_union_keeps_union_node() {
388 let schema = fixture();
389 let nav = SchemaNavigator::new(&schema);
390 let path = [
391 ident("items"),
392 PathSegment::ArrayIndex(ArrayIndexKind::Push),
393 ];
394 let unions = nav.resolve_to_union(&path, &[]);
395 assert_eq!(unions.len(), 1);
396 assert!(matches!(
397 schema.node(unions[0]).content,
398 SchemaNodeContent::Union(_)
399 ));
400 }
401
402 #[test]
403 fn unknown_path_yields_nothing() {
404 let schema = fixture();
405 let nav = SchemaNavigator::new(&schema);
406 assert!(nav.resolve(&[ident("missing")], &[]).is_empty());
407 }
408}