Skip to main content

eure_schema/
type_path_trace.rs

1use std::collections::HashSet;
2
3use eure_document::document::{EureDocument, NodeId};
4use eure_document::layout::{DocLayout, LayoutStyle};
5use eure_document::path::{EurePath, PathSegment};
6use indexmap::IndexMap;
7use thiserror::Error;
8
9use crate::SchemaNodeId;
10
11pub type LayoutStrategy = LayoutStyle;
12pub type NodeTypeTraceMap = IndexMap<NodeId, ResolvedTypeTrace>;
13pub type SchemaNodePathMap = IndexMap<SchemaNodeId, EurePath>;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct TypePathTrace(Vec<EurePath>);
17
18#[derive(Debug, Error, Clone, PartialEq, Eq)]
19pub enum TypePathTraceError {
20    #[error("type path trace must contain at least one hop")]
21    EmptyTrace,
22}
23
24impl TypePathTrace {
25    pub fn single(path: EurePath) -> Self {
26        Self(vec![path])
27    }
28
29    pub fn from_hops(hops: Vec<EurePath>) -> Result<Self, TypePathTraceError> {
30        if hops.is_empty() {
31            return Err(TypePathTraceError::EmptyTrace);
32        }
33        Ok(Self(hops))
34    }
35
36    pub fn with_hop(&self, path: EurePath) -> Self {
37        let mut hops = self.0.clone();
38        hops.push(path);
39        Self(hops)
40    }
41
42    pub fn hops(&self) -> &[EurePath] {
43        &self.0
44    }
45
46    pub fn current(&self) -> &EurePath {
47        debug_assert!(!self.0.is_empty(), "TypePathTrace must be non-empty");
48        &self.0[self.0.len() - 1]
49    }
50
51    pub fn is_single_hop(&self) -> bool {
52        self.0.len() == 1
53    }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum TypeTraceUnresolvedReason {
58    NotVisited,
59    UnknownField { field: String },
60    UnknownExtension { extension: String },
61    UndefinedTypeReference { name: String },
62    CrossSchemaReference { namespace: String, name: String },
63    AmbiguousUnion { candidates: Vec<TypePathTrace> },
64    NoMatchingUnionVariant { candidates: Vec<TypePathTrace> },
65    InvalidVariantTag { tag: String },
66    RequiresExplicitVariant { variant: String },
67    ReferenceCycle,
68    InternalInvariant,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum ResolvedTypeTrace {
73    Resolved(TypePathTrace),
74    Ambiguous(Vec<TypePathTrace>),
75    Unresolved(TypeTraceUnresolvedReason),
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct LayoutStrategies {
80    pub by_path: IndexMap<EurePath, LayoutStrategy>,
81    pub order_by_path: IndexMap<EurePath, Vec<PathSegment>>,
82    pub schema_node_paths: SchemaNodePathMap,
83}
84
85impl Default for LayoutStrategies {
86    fn default() -> Self {
87        Self {
88            by_path: IndexMap::new(),
89            order_by_path: IndexMap::new(),
90            schema_node_paths: IndexMap::new(),
91        }
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct ResolvedLayout {
97    pub strategy: LayoutStrategy,
98    pub matched_path: EurePath,
99    pub hop_index: usize,
100}
101
102impl LayoutStrategies {
103    pub fn resolve(&self, trace: &TypePathTrace) -> Option<ResolvedLayout> {
104        for (hop_index, hop) in trace.hops().iter().enumerate() {
105            if let Some(strategy) = self.by_path.get(hop) {
106                return Some(ResolvedLayout {
107                    strategy: *strategy,
108                    matched_path: hop.clone(),
109                    hop_index,
110                });
111            }
112        }
113        None
114    }
115}
116
117pub fn materialize_doc_layout(
118    doc: &EureDocument,
119    node_traces: &NodeTypeTraceMap,
120    strategies: &LayoutStrategies,
121    fallback_style: LayoutStrategy,
122) -> DocLayout {
123    let mut layout = DocLayout::new();
124    layout.fallback_style = fallback_style;
125
126    let node_paths = collect_document_node_paths(doc);
127    for (node_id, node_path) in node_paths {
128        let Some(trace) = node_traces.get(&node_id) else {
129            continue;
130        };
131
132        if !node_path.is_empty()
133            && let Some(style) = resolve_style_for_trace(strategies, trace)
134        {
135            layout.add_style_rule(node_path.clone(), style);
136        }
137
138        if let Some(order) = resolve_order_for_trace(strategies, trace)
139            && !order.is_empty()
140        {
141            layout.add_order_rule(node_path, order, false);
142        }
143    }
144
145    layout
146}
147
148fn resolve_style_for_trace(
149    strategies: &LayoutStrategies,
150    trace: &ResolvedTypeTrace,
151) -> Option<LayoutStrategy> {
152    match trace {
153        ResolvedTypeTrace::Resolved(trace) => {
154            strategies.resolve(trace).map(|resolved| resolved.strategy)
155        }
156        ResolvedTypeTrace::Ambiguous(candidates) => {
157            let mut resolved: Option<LayoutStrategy> = None;
158            for candidate in candidates {
159                let candidate_style = strategies.resolve(candidate).map(|r| r.strategy)?;
160                if let Some(existing) = resolved {
161                    if existing != candidate_style {
162                        return None;
163                    }
164                } else {
165                    resolved = Some(candidate_style);
166                }
167            }
168            resolved
169        }
170        ResolvedTypeTrace::Unresolved(_) => None,
171    }
172}
173
174fn resolve_order_for_trace(
175    strategies: &LayoutStrategies,
176    trace: &ResolvedTypeTrace,
177) -> Option<Vec<PathSegment>> {
178    match trace {
179        ResolvedTypeTrace::Resolved(trace) => resolve_order_for_hops(strategies, trace),
180        ResolvedTypeTrace::Ambiguous(candidates) => {
181            let mut resolved: Option<Vec<PathSegment>> = None;
182            for candidate in candidates {
183                let candidate_order = resolve_order_for_hops(strategies, candidate)?;
184                if let Some(existing) = resolved.as_ref() {
185                    if *existing != candidate_order {
186                        return None;
187                    }
188                } else {
189                    resolved = Some(candidate_order);
190                }
191            }
192            resolved
193        }
194        ResolvedTypeTrace::Unresolved(_) => None,
195    }
196}
197
198fn resolve_order_for_hops(
199    strategies: &LayoutStrategies,
200    trace: &TypePathTrace,
201) -> Option<Vec<PathSegment>> {
202    for hop in trace.hops() {
203        if let Some(order) = strategies.order_by_path.get(hop) {
204            return Some(order.clone());
205        }
206    }
207    None
208}
209
210fn collect_document_node_paths(doc: &EureDocument) -> IndexMap<NodeId, Vec<PathSegment>> {
211    let mut out = IndexMap::new();
212    let mut visited = HashSet::new();
213    collect_document_node_paths_rec(
214        doc,
215        doc.get_root_id(),
216        &mut Vec::new(),
217        &mut out,
218        &mut visited,
219    );
220    out
221}
222
223fn collect_document_node_paths_rec(
224    doc: &EureDocument,
225    node_id: NodeId,
226    path: &mut Vec<PathSegment>,
227    out: &mut IndexMap<NodeId, Vec<PathSegment>>,
228    visited: &mut HashSet<NodeId>,
229) {
230    if !visited.insert(node_id) {
231        return;
232    }
233    out.insert(node_id, path.clone());
234    let node = doc.node(node_id);
235
236    for (ext, &child_id) in node.extensions.iter() {
237        path.push(PathSegment::Extension(ext.clone()));
238        collect_document_node_paths_rec(doc, child_id, path, out, visited);
239        path.pop();
240    }
241
242    match &node.content {
243        eure_document::document::node::NodeValue::Array(array) => {
244            for (index, &child_id) in array.iter().enumerate() {
245                path.push(PathSegment::ArrayIndex(Some(index)));
246                collect_document_node_paths_rec(doc, child_id, path, out, visited);
247                path.pop();
248            }
249        }
250        eure_document::document::node::NodeValue::Tuple(tuple) => {
251            for (index, &child_id) in tuple.iter().enumerate() {
252                path.push(PathSegment::TupleIndex(index as u8));
253                collect_document_node_paths_rec(doc, child_id, path, out, visited);
254                path.pop();
255            }
256        }
257        eure_document::document::node::NodeValue::Map(map) => {
258            for (key, &child_id) in map.iter() {
259                path.push(PathSegment::Value(key.clone()));
260                collect_document_node_paths_rec(doc, child_id, path, out, visited);
261                path.pop();
262            }
263        }
264        eure_document::document::node::NodeValue::PartialMap(map) => {
265            for (key, &child_id) in map.iter() {
266                path.push(PathSegment::from_partial_object_key(key.clone()));
267                collect_document_node_paths_rec(doc, child_id, path, out, visited);
268                path.pop();
269            }
270        }
271        eure_document::document::node::NodeValue::Primitive(_)
272        | eure_document::document::node::NodeValue::Hole(_) => {}
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use eure_document::layout::LayoutStyle;
280    use eure_document::value::ObjectKey;
281
282    #[test]
283    fn resolve_first_hop_wins() {
284        let first = EurePath::root();
285        let second = EurePath(vec![PathSegment::Value(ObjectKey::String("a".to_string()))]);
286        let trace = TypePathTrace::from_hops(vec![first.clone(), second.clone()]).unwrap();
287
288        let mut layout = LayoutStrategies::default();
289        layout.by_path.insert(second, LayoutStyle::Section);
290        layout.by_path.insert(first.clone(), LayoutStyle::Binding);
291
292        let resolved = layout.resolve(&trace).expect("should resolve");
293        assert_eq!(resolved.strategy, LayoutStyle::Binding);
294        assert_eq!(resolved.matched_path, first);
295        assert_eq!(resolved.hop_index, 0);
296    }
297
298    #[test]
299    fn resolve_no_match() {
300        let trace = TypePathTrace::single(EurePath::root());
301        let layout = LayoutStrategies::default();
302        assert!(layout.resolve(&trace).is_none());
303    }
304
305    #[test]
306    fn type_path_trace_rejects_empty_hops() {
307        let err = TypePathTrace::from_hops(Vec::new()).expect_err("empty trace must be rejected");
308        assert_eq!(err, TypePathTraceError::EmptyTrace);
309    }
310
311    #[test]
312    fn resolve_exact_match_only() {
313        let parent = EurePath(vec![PathSegment::Value(ObjectKey::String(
314            "item".to_string(),
315        ))]);
316        let child = EurePath(vec![
317            PathSegment::Value(ObjectKey::String("item".to_string())),
318            PathSegment::Value(ObjectKey::String("value".to_string())),
319        ]);
320        let trace = TypePathTrace::single(child);
321
322        let mut layout = LayoutStrategies::default();
323        layout.by_path.insert(parent, LayoutStyle::Section);
324
325        assert!(layout.resolve(&trace).is_none());
326    }
327
328    #[test]
329    fn ambiguous_trace_resolves_when_all_candidates_have_same_strategy() {
330        let hop_a = EurePath(vec![PathSegment::Value(ObjectKey::String("a".to_string()))]);
331        let hop_b = EurePath(vec![PathSegment::Value(ObjectKey::String("b".to_string()))]);
332        let mut strategies = LayoutStrategies::default();
333        strategies
334            .by_path
335            .insert(hop_a.clone(), LayoutStyle::Binding);
336        strategies
337            .by_path
338            .insert(hop_b.clone(), LayoutStyle::Binding);
339
340        let trace = ResolvedTypeTrace::Ambiguous(vec![
341            TypePathTrace::single(hop_a),
342            TypePathTrace::single(hop_b),
343        ]);
344        assert_eq!(
345            resolve_style_for_trace(&strategies, &trace),
346            Some(LayoutStyle::Binding)
347        );
348    }
349
350    #[test]
351    fn ambiguous_trace_falls_back_when_candidates_conflict() {
352        let hop_a = EurePath(vec![PathSegment::Value(ObjectKey::String("a".to_string()))]);
353        let hop_b = EurePath(vec![PathSegment::Value(ObjectKey::String("b".to_string()))]);
354        let mut strategies = LayoutStrategies::default();
355        strategies
356            .by_path
357            .insert(hop_a.clone(), LayoutStyle::Binding);
358        strategies
359            .by_path
360            .insert(hop_b.clone(), LayoutStyle::SectionBinding);
361
362        let trace = ResolvedTypeTrace::Ambiguous(vec![
363            TypePathTrace::single(hop_a),
364            TypePathTrace::single(hop_b),
365        ]);
366        assert!(resolve_style_for_trace(&strategies, &trace).is_none());
367    }
368}