Skip to main content

eure_schema/
navigate.rs

1//! Path-based navigation over a [`SchemaDocument`].
2//!
3//! Editor tooling (completion, hover) needs to answer "which schema node
4//! describes the document node at this path?" without having a complete
5//! [`EureDocument`](eure_document::document::EureDocument) at hand: while the
6//! user is typing, the document usually does not parse.
7//!
8//! [`SchemaNavigator`] walks a list of [`PathSegment`]s against the schema
9//! only. References are dereferenced transparently. Unions are resolved with
10//! an explicit variant when the caller knows one (from a sibling `$variant`
11//! binding); otherwise every variant becomes a candidate, so a position inside
12//! an untagged union yields several candidate nodes instead of none.
13
14use 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/// Explicit variant selection for a union located at a path prefix.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct VariantHint {
29    /// Number of path segments consumed when the union is reached.
30    pub prefix_len: usize,
31    /// Variant path (possibly nested, e.g. `ok.some.left`).
32    pub variant: VariantPath,
33}
34
35/// Navigates a [`SchemaDocument`] along document paths.
36#[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    /// Resolve `path` starting from the schema root.
51    ///
52    /// Returns every concrete (non-reference, non-union) schema node that may
53    /// describe the document node at `path`. The list is empty when the path
54    /// does not exist in the schema.
55    pub fn resolve(&self, path: &[PathSegment], hints: &[VariantHint]) -> Vec<SchemaNodeId> {
56        self.resolve_from(self.schema.root, path, hints)
57    }
58
59    /// Resolve `path` relative to `start`.
60    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    /// Resolve `path` but stop at unions instead of expanding them.
88    ///
89    /// Used to enumerate variant names: the returned nodes are references-free
90    /// but may be `Union` nodes.
91    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    /// Descend from `id` through the variants named by `path`.
116    ///
117    /// Returns the reference-free nodes reached, which are typically nested
118    /// unions. Used to complete the tail of a dotted variant path.
119    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    /// Follow references until a non-reference node is reached.
138    ///
139    /// Returns `None` for undefined references and reference cycles.
140    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    /// Expand `id` into concrete nodes: references are dereferenced and unions
156    /// are replaced by their variants (or the hinted variant only).
157    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    /// Step from a concrete node to the child described by `segment`.
205    ///
206    /// The result may be a reference or union; callers usually pass it through
207    /// [`Self::concretize`].
208    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    /// All fields of a record, including fields contributed by `flatten`
247    /// targets, in declaration order. Own fields take precedence over
248    /// flattened ones with the same name.
249    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    /// Extension names accepted on the node, as declared by `$ext-type`.
279    pub fn extension_names(&self, id: SchemaNodeId) -> impl Iterator<Item = &'a Identifier> {
280        self.schema.node(id).ext_types.keys()
281    }
282}
283
284/// The variant selected for the union reached after `prefix_len` path
285/// segments, if a hint declares one.
286pub 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    /// root = { items: [ $types.item ] }, item = union { a: { x: text }, b: { y: text } }
314    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}