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