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