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}