Skip to main content

eure_document/
plan.rs

1//! Total, verified layout plan for projecting an [`EureDocument`] to
2//! [`SourceDocument`].
3//!
4//! A [`LayoutPlan`] owns the document and assigns every reachable node an
5//! explicit emission shape ([`Form`] for non-Array nodes, [`ArrayForm`] for
6//! Array nodes). Validation runs in [`PlanBuilder::build`]; [`LayoutPlan::emit`]
7//! consumes the plan and produces a [`SourceDocument`] without any silent
8//! fallbacks.
9//!
10//! Compared to the previous `DocLayout` best-effort projection, this module:
11//!
12//! - Totality: every reachable [`NodeId`] is assigned a shape.
13//! - Explicit errors: all failure cases are typed [`PlanError`] variants.
14//! - Orthogonal arrays: array handling is a separate dimension from shape, so
15//!   every grammar pattern (`items[] = v`, `items[] { ... }`, `@ items[]`,
16//!   `@ items[] { ... }`, etc.) is reachable.
17
18pub mod emit;
19pub mod traverse;
20
21use alloc::vec::Vec;
22
23use crate::document::node::NodeValue;
24use crate::document::{EureDocument, NodeId};
25use crate::map::Map;
26use crate::path::PathSegment;
27use crate::value::ValueKind;
28
29/// One of the seven semantic shapes a non-Array node can take.
30///
31/// Six correspond to the grammar patterns documented in `source.rs`;
32/// `Flatten` hoists children into the parent context without emitting the
33/// node itself.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum Form {
36    /// Pattern #1: `path = value`.
37    Inline,
38    /// Pattern #2: `path { ... }`.
39    BindingBlock,
40    /// Pattern #3: `path { = value ... }`.
41    BindingValueBlock,
42    /// Pattern #4: `@ path` with items.
43    Section,
44    /// Pattern #5: `@ path { ... }`.
45    SectionBlock,
46    /// Pattern #6: `@ path { = value ... }`.
47    SectionValueBlock,
48    /// No self-emission. Children are hoisted into the parent.
49    Flatten,
50}
51
52/// How an [`NodeValue::Array`] node is emitted. Orthogonal to [`Form`].
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub enum ArrayForm {
55    /// Single inline binding: `path = [e1, e2, ...]`.
56    Inline,
57    /// Per-element emission with `[]` (push marker). Each element uses the
58    /// given element [`Form`].
59    PerElement(Form),
60    /// Per-element emission with explicit `[i]` indices.
61    PerElementIndexed(Form),
62}
63
64/// A declarative, validated layout plan for a document.
65#[derive(Debug, Clone)]
66pub struct LayoutPlan {
67    doc: EureDocument,
68    forms: Map<NodeId, Form>,
69    array_forms: Map<NodeId, ArrayForm>,
70    order: Map<NodeId, Vec<NodeId>>,
71}
72
73/// Mutable builder for a [`LayoutPlan`].
74#[derive(Debug, Clone)]
75pub struct PlanBuilder {
76    doc: EureDocument,
77    forms: Map<NodeId, Form>,
78    array_forms: Map<NodeId, ArrayForm>,
79    order: Map<NodeId, Vec<NodeId>>,
80}
81
82/// Reason an [`ArrayForm`] assignment is incompatible with the array content.
83#[derive(Debug, Clone, PartialEq)]
84pub enum ArrayFormReason {
85    /// Some element is incompatible with the requested per-element [`Form`].
86    ElementIncompatibleForm { element: NodeId, kind: ValueKind },
87    /// `PerElement(Flatten)` — flattening anonymous array elements into the
88    /// parent path is always rejected (it would collapse distinct elements
89    /// onto the same path).
90    FlattenElementDisallowed,
91}
92
93/// Errors produced during plan validation or construction.
94#[derive(Debug, Clone, PartialEq, thiserror::Error)]
95pub enum PlanError {
96    #[error("path {path:?} does not resolve in document")]
97    PathNotFound { path: Vec<PathSegment> },
98
99    #[error("form {form:?} is incompatible with node {node:?} of kind {kind}")]
100    IncompatibleForm {
101        node: NodeId,
102        form: Form,
103        kind: ValueKind,
104    },
105
106    #[error("array form {form:?} is incompatible with array node {node:?}: {reason:?}")]
107    IncompatibleArrayForm {
108        node: NodeId,
109        form: ArrayForm,
110        reason: ArrayFormReason,
111    },
112
113    #[error("node {0:?} has no assigned form")]
114    MissingForm(NodeId),
115
116    #[error("node {node:?} would be emitted multiple times")]
117    DuplicateEmission { node: NodeId },
118
119    #[error("section form at node {0:?} is not allowed in this context")]
120    SectionInForbiddenContext(NodeId),
121
122    #[error("schema produced conflicting forms for node {node:?}")]
123    ConflictingOverride { node: NodeId },
124
125    #[error("ordered child {child:?} is not a direct child of parent {parent:?}")]
126    OrderChildNotDirect { parent: NodeId, child: NodeId },
127
128    #[error("ordered children for parent {parent:?} contain duplicate {child:?}")]
129    OrderDuplicateChild { parent: NodeId, child: NodeId },
130
131    #[error(
132        "ordered child {child:?} of parent {parent:?} is an extension; extension order is fixed"
133    )]
134    OrderExtensionChild { parent: NodeId, child: NodeId },
135
136    #[error("set_form called on array node {0:?}; use set_array_form")]
137    FormOnArrayNode(NodeId),
138
139    #[error("set_array_form called on non-array node {0:?}; use set_form")]
140    ArrayFormOnNonArray(NodeId),
141
142    #[error("set_form called on root node {0:?}; root form is implicit")]
143    FormOnRoot(NodeId),
144
145    #[error("order requested on array node {0:?}; element order is data")]
146    OrderOnArrayNode(NodeId),
147}
148
149// ============================================================================
150// Compatibility rules
151// ============================================================================
152
153/// Returns `Ok(())` if the given [`Form`] is compatible with the given
154/// [`ValueKind`], otherwise an [`PlanError::IncompatibleForm`].
155pub(crate) fn check_form_compat(id: NodeId, form: Form, kind: ValueKind) -> Result<(), PlanError> {
156    let compatible = match form {
157        Form::Inline => !matches!(kind, ValueKind::PartialMap),
158        Form::BindingBlock | Form::Section | Form::SectionBlock | Form::Flatten => {
159            matches!(kind, ValueKind::Map | ValueKind::PartialMap)
160        }
161        Form::BindingValueBlock | Form::SectionValueBlock => {
162            // A distinguished self-value is only realizable when the node has
163            // primitive-like content plus extensions; grammatically this is
164            // `path { = value ... }` / `@ path { = value ... }`.
165            matches!(
166                kind,
167                ValueKind::Hole
168                    | ValueKind::Null
169                    | ValueKind::Bool
170                    | ValueKind::Integer
171                    | ValueKind::F32
172                    | ValueKind::F64
173                    | ValueKind::Text
174                    | ValueKind::Tuple
175            )
176        }
177    };
178    if compatible {
179        Ok(())
180    } else {
181        Err(PlanError::IncompatibleForm {
182            node: id,
183            form,
184            kind,
185        })
186    }
187}
188
189/// Validate an [`ArrayForm`] assignment on an array node.
190pub(crate) fn check_array_form_compat(
191    doc: &EureDocument,
192    id: NodeId,
193    form: ArrayForm,
194) -> Result<(), PlanError> {
195    match form {
196        ArrayForm::Inline => Ok(()),
197        ArrayForm::PerElement(Form::Flatten) | ArrayForm::PerElementIndexed(Form::Flatten) => {
198            Err(PlanError::IncompatibleArrayForm {
199                node: id,
200                form,
201                reason: ArrayFormReason::FlattenElementDisallowed,
202            })
203        }
204        ArrayForm::PerElement(element) | ArrayForm::PerElementIndexed(element) => {
205            let element_ids = match &doc.node(id).content {
206                NodeValue::Array(arr) => arr.iter().copied().collect::<Vec<_>>(),
207                _ => return Err(PlanError::ArrayFormOnNonArray(id)),
208            };
209            for element_id in element_ids {
210                let kind = doc.node(element_id).content.value_kind();
211                if check_form_compat(element_id, element, kind).is_err() {
212                    return Err(PlanError::IncompatibleArrayForm {
213                        node: id,
214                        form,
215                        reason: ArrayFormReason::ElementIncompatibleForm {
216                            element: element_id,
217                            kind,
218                        },
219                    });
220                }
221            }
222            Ok(())
223        }
224    }
225}
226
227// ============================================================================
228// Builder
229// ============================================================================
230
231impl PlanBuilder {
232    /// Construct a new builder for the given document.
233    pub fn new(doc: EureDocument) -> Self {
234        Self {
235            doc,
236            forms: Map::default(),
237            array_forms: Map::default(),
238            order: Map::default(),
239        }
240    }
241
242    /// Borrow the underlying document.
243    pub fn document(&self) -> &EureDocument {
244        &self.doc
245    }
246
247    /// Resolve a path to a [`NodeId`] relative to the document root.
248    pub fn node_at(&self, path: &[PathSegment]) -> Result<NodeId, PlanError> {
249        let mut current = self.doc.get_root_id();
250        for segment in path {
251            match traverse::child_node_id(&self.doc, current, segment) {
252                Some(id) => current = id,
253                None => {
254                    return Err(PlanError::PathNotFound {
255                        path: path.to_vec(),
256                    });
257                }
258            }
259        }
260        Ok(current)
261    }
262
263    /// Assign a [`Form`] to a non-Array node.
264    pub fn set_form(&mut self, id: NodeId, form: Form) -> Result<&mut Self, PlanError> {
265        if id == self.doc.get_root_id() {
266            return Err(PlanError::FormOnRoot(id));
267        }
268        let kind = self.doc.node(id).content.value_kind();
269        if matches!(kind, ValueKind::Array) {
270            return Err(PlanError::FormOnArrayNode(id));
271        }
272        check_form_compat(id, form, kind)?;
273        self.forms.insert(id, form);
274        Ok(self)
275    }
276
277    /// Assign a [`Form`] to the node at the given path.
278    pub fn set_form_at(
279        &mut self,
280        path: &[PathSegment],
281        form: Form,
282    ) -> Result<&mut Self, PlanError> {
283        let id = self.node_at(path)?;
284        self.set_form(id, form)
285    }
286
287    /// Assign an [`ArrayForm`] to an Array node.
288    pub fn set_array_form(&mut self, id: NodeId, form: ArrayForm) -> Result<&mut Self, PlanError> {
289        let kind = self.doc.node(id).content.value_kind();
290        if !matches!(kind, ValueKind::Array) {
291            return Err(PlanError::ArrayFormOnNonArray(id));
292        }
293        check_array_form_compat(&self.doc, id, form)?;
294        self.array_forms.insert(id, form);
295        Ok(self)
296    }
297
298    /// Assign an [`ArrayForm`] to the Array node at the given path.
299    pub fn set_array_form_at(
300        &mut self,
301        path: &[PathSegment],
302        form: ArrayForm,
303    ) -> Result<&mut Self, PlanError> {
304        let id = self.node_at(path)?;
305        self.set_array_form(id, form)
306    }
307
308    /// Order the direct children of a non-Array parent. Unlisted children
309    /// are appended after the listed ones in document order.
310    pub fn order(&mut self, parent: NodeId, children: Vec<NodeId>) -> Result<&mut Self, PlanError> {
311        if matches!(self.doc.node(parent).content.value_kind(), ValueKind::Array) {
312            return Err(PlanError::OrderOnArrayNode(parent));
313        }
314        let direct = traverse::children_of(&self.doc, parent);
315
316        let mut seen = Vec::with_capacity(children.len());
317        for child in &children {
318            let Some((segment, _)) = direct.iter().find(|(_, id)| id == child) else {
319                return Err(PlanError::OrderChildNotDirect {
320                    parent,
321                    child: *child,
322                });
323            };
324            if matches!(segment, PathSegment::Extension(_)) {
325                return Err(PlanError::OrderExtensionChild {
326                    parent,
327                    child: *child,
328                });
329            }
330            if seen.contains(child) {
331                return Err(PlanError::OrderDuplicateChild {
332                    parent,
333                    child: *child,
334                });
335            }
336            seen.push(*child);
337        }
338        self.order.insert(parent, children);
339        Ok(self)
340    }
341
342    /// Order the direct children of the parent at the given path by child
343    /// path segments.
344    pub fn order_at(
345        &mut self,
346        parent_path: &[PathSegment],
347        segs: Vec<PathSegment>,
348    ) -> Result<&mut Self, PlanError> {
349        let parent = self.node_at(parent_path)?;
350        let direct = traverse::children_of(&self.doc, parent);
351        let mut children = Vec::with_capacity(segs.len());
352        for seg in &segs {
353            let id = direct
354                .iter()
355                .find(|(s, _)| s == seg)
356                .map(|(_, id)| *id)
357                .ok_or_else(|| PlanError::PathNotFound {
358                    path: {
359                        let mut p = parent_path.to_vec();
360                        p.push(seg.clone());
361                        p
362                    },
363                })?;
364            children.push(id);
365        }
366        self.order(parent, children)
367    }
368
369    /// Fill defaults, validate, and finalize.
370    pub fn build(mut self) -> Result<LayoutPlan, PlanError> {
371        // 1. Fill defaults via auto() policy for every unassigned reachable id.
372        //    Walk from root threading `allow_sections` so that auto picks a
373        //    block form (not Section) in contexts where sections are forbidden
374        //    (inside a Section / SectionValueBlock items body). This prevents
375        //    auto-assignment from generating structurally invalid plans.
376        let root = self.doc.get_root_id();
377        self.fill_defaults(root, true);
378
379        // 2. Compatibility check (defensive — set_form already checks, but
380        // auto-filled defaults must also be valid).
381        let all = traverse::all_reachable_ids(&self.doc);
382        for id in &all {
383            let kind = self.doc.node(*id).content.value_kind();
384            if matches!(kind, ValueKind::Array) {
385                let form = *self
386                    .array_forms
387                    .get(id)
388                    .ok_or(PlanError::MissingForm(*id))?;
389                check_array_form_compat(&self.doc, *id, form)?;
390            } else if *id != self.doc.get_root_id() {
391                let form = *self.forms.get(id).ok_or(PlanError::MissingForm(*id))?;
392                check_form_compat(*id, form, kind)?;
393            }
394        }
395
396        // 3. Section context + 4. array feasibility + 5. coverage/uniqueness
397        // are enforced during emission's dry walk.
398        let plan = LayoutPlan {
399            doc: self.doc,
400            forms: self.forms,
401            array_forms: self.array_forms,
402            order: self.order,
403        };
404        plan.validate_structure()?;
405        Ok(plan)
406    }
407
408    fn fill_defaults(&mut self, id: NodeId, allow_sections: bool) {
409        let kind = self.doc.node(id).content.value_kind();
410        let root = self.doc.get_root_id();
411
412        if matches!(kind, ValueKind::Array) {
413            let array_form = match self.array_forms.get(&id).copied() {
414                Some(f) => f,
415                None => {
416                    let auto = auto::auto_array_form(&self.doc, id, allow_sections);
417                    self.array_forms.insert(id, auto);
418                    auto
419                }
420            };
421            let elem_form: Option<Form> = match array_form {
422                ArrayForm::Inline => None,
423                ArrayForm::PerElement(f) | ArrayForm::PerElementIndexed(f) => Some(f),
424            };
425            for (_, elem_id) in traverse::children_of(&self.doc, id) {
426                self.fill_element_defaults(elem_id, allow_sections, elem_form);
427            }
428            return;
429        }
430
431        let child_allow = if id == root {
432            true
433        } else {
434            let form = match self.forms.get(&id).copied() {
435                Some(f) => f,
436                None => {
437                    let auto = auto::auto_form(&self.doc, id, allow_sections);
438                    self.forms.insert(id, auto);
439                    auto
440                }
441            };
442            child_allow_for_form(form, allow_sections)
443        };
444
445        for (_, child_id) in traverse::children_of(&self.doc, id) {
446            self.fill_defaults(child_id, child_allow);
447        }
448    }
449
450    /// Fill defaults for an array element. `elem_form` is the `Form` dictated
451    /// by the array's `ArrayForm::PerElement*` variant; when `None`, the
452    /// element is rendered as an inline value (its descendants are not
453    /// emitted as standalone nodes but we still fill defaults for validation).
454    fn fill_element_defaults(&mut self, id: NodeId, outer_allow: bool, elem_form: Option<Form>) {
455        let kind = self.doc.node(id).content.value_kind();
456
457        if matches!(kind, ValueKind::Array) {
458            self.fill_defaults(id, outer_allow);
459            return;
460        }
461
462        // Set the element's own form. This is NOT the form used to emit the
463        // element (emit_array_child uses `elem_form` directly for PerElement*),
464        // but we still must populate `forms` so the compat pass doesn't raise
465        // `MissingForm`. Prefer `elem_form` when compatible with the element's
466        // kind; otherwise fall back to the context-aware auto policy.
467        if !self.forms.contains_key(&id) {
468            let chosen = match elem_form {
469                Some(f) if check_form_compat(id, f, kind).is_ok() => f,
470                _ => auto::auto_form(&self.doc, id, outer_allow),
471            };
472            self.forms.insert(id, chosen);
473        }
474
475        // Descent into element's children uses the element's effective emit
476        // form to determine whether sections are allowed inside it.
477        let child_allow = match elem_form {
478            Some(f) => child_allow_for_form(f, outer_allow),
479            None => outer_allow,
480        };
481        for (_, child_id) in traverse::children_of(&self.doc, id) {
482            self.fill_defaults(child_id, child_allow);
483        }
484    }
485}
486
487/// Whether children of a node with the given [`Form`] may be emitted as
488/// sections. `build_items` (used by `Section` / `SectionValueBlock`) requires
489/// that its body contain no sections, so those forms propagate
490/// `allow_sections = false` to their descendants.
491fn child_allow_for_form(form: Form, outer_allow: bool) -> bool {
492    match form {
493        Form::Inline => outer_allow,
494        Form::BindingBlock | Form::BindingValueBlock | Form::SectionBlock => true,
495        Form::Section | Form::SectionValueBlock => false,
496        Form::Flatten => outer_allow,
497    }
498}
499
500// ============================================================================
501// auto() policy
502// ============================================================================
503
504pub(crate) mod auto {
505    use super::{ArrayForm, Form};
506    use crate::document::node::NodeValue;
507    use crate::document::{EureDocument, NodeId};
508    use crate::value::ValueKind;
509
510    /// The default [`Form`] for a non-root, non-Array node. When
511    /// `allow_sections` is false, Map/PartialMap candidates for [`Form::Section`]
512    /// are downgraded to [`Form::BindingBlock`] — this is what keeps the
513    /// auto policy structurally valid inside `build_items` bodies.
514    pub(crate) fn auto_form(doc: &EureDocument, id: NodeId, allow_sections: bool) -> Form {
515        let node = doc.node(id);
516        let kind = node.content.value_kind();
517        match kind {
518            ValueKind::Hole
519            | ValueKind::Null
520            | ValueKind::Bool
521            | ValueKind::Integer
522            | ValueKind::F32
523            | ValueKind::F64
524            | ValueKind::Text
525            | ValueKind::Tuple => Form::Inline,
526            ValueKind::Array => unreachable!("arrays use auto_array_form"),
527            ValueKind::Map | ValueKind::PartialMap => {
528                if !node.extensions.is_empty() || map_has_complex_child(doc, &node.content) {
529                    if allow_sections {
530                        Form::Section
531                    } else {
532                        Form::BindingBlock
533                    }
534                } else if map_only_scalar_children(doc, &node.content) {
535                    Form::Inline
536                } else {
537                    Form::BindingBlock
538                }
539            }
540        }
541    }
542
543    /// The default [`ArrayForm`] for an Array node. When `allow_sections` is
544    /// false, `PerElement(Section)` is downgraded to `PerElement(BindingBlock)`.
545    pub(crate) fn auto_array_form(
546        doc: &EureDocument,
547        id: NodeId,
548        allow_sections: bool,
549    ) -> ArrayForm {
550        let NodeValue::Array(arr) = &doc.node(id).content else {
551            return ArrayForm::Inline;
552        };
553        if arr.is_empty() {
554            return ArrayForm::Inline;
555        }
556        let all_maps = arr.iter().all(|&el| {
557            matches!(
558                doc.node(el).content.value_kind(),
559                ValueKind::Map | ValueKind::PartialMap
560            )
561        });
562        if all_maps {
563            let elem_form = if allow_sections {
564                Form::Section
565            } else {
566                Form::BindingBlock
567            };
568            ArrayForm::PerElement(elem_form)
569        } else {
570            ArrayForm::Inline
571        }
572    }
573
574    fn map_only_scalar_children(doc: &EureDocument, content: &NodeValue) -> bool {
575        let ids: alloc::vec::Vec<NodeId> = match content {
576            NodeValue::Map(map) => map.iter().map(|(_, &id)| id).collect(),
577            NodeValue::PartialMap(pm) => pm.iter().map(|(_, &id)| id).collect(),
578            _ => return true,
579        };
580        ids.iter().all(|&child| {
581            let child_node = doc.node(child);
582            child_node.extensions.is_empty()
583                && matches!(
584                    child_node.content.value_kind(),
585                    ValueKind::Null
586                        | ValueKind::Bool
587                        | ValueKind::Integer
588                        | ValueKind::F32
589                        | ValueKind::F64
590                        | ValueKind::Text
591                        | ValueKind::Hole
592                        | ValueKind::Tuple
593                )
594        })
595    }
596
597    fn map_has_complex_child(doc: &EureDocument, content: &NodeValue) -> bool {
598        let ids: alloc::vec::Vec<NodeId> = match content {
599            NodeValue::Map(map) => map.iter().map(|(_, &id)| id).collect(),
600            NodeValue::PartialMap(pm) => pm.iter().map(|(_, &id)| id).collect(),
601            _ => return false,
602        };
603        ids.iter().any(|&child| {
604            let child_node = doc.node(child);
605            !child_node.extensions.is_empty()
606                || matches!(
607                    child_node.content.value_kind(),
608                    ValueKind::Map | ValueKind::PartialMap | ValueKind::Array
609                )
610        })
611    }
612}
613
614// ============================================================================
615// LayoutPlan
616// ============================================================================
617
618impl LayoutPlan {
619    /// Start a new [`PlanBuilder`] for the given document.
620    pub fn builder(doc: EureDocument) -> PlanBuilder {
621        PlanBuilder::new(doc)
622    }
623
624    /// Build a plan using the default automatic policy for every node.
625    pub fn auto(doc: EureDocument) -> Result<Self, PlanError> {
626        Self::builder(doc).build()
627    }
628
629    /// Build a plan that emits maps as sections wherever possible.
630    pub fn sectioned(doc: EureDocument) -> Result<Self, PlanError> {
631        let mut b = Self::builder(doc);
632        let all = traverse::all_reachable_ids(b.document());
633        let root = b.document().get_root_id();
634        for id in all {
635            if id == root {
636                continue;
637            }
638            let kind = b.document().node(id).content.value_kind();
639            match kind {
640                ValueKind::Map | ValueKind::PartialMap => {
641                    b.set_form(id, Form::Section)?;
642                }
643                ValueKind::Array => {
644                    let form = match auto::auto_array_form(b.document(), id, true) {
645                        ArrayForm::PerElement(_) | ArrayForm::PerElementIndexed(_) => {
646                            ArrayForm::PerElement(Form::Section)
647                        }
648                        ArrayForm::Inline => ArrayForm::Inline,
649                    };
650                    b.set_array_form(id, form)?;
651                }
652                _ => {
653                    b.set_form(id, Form::Inline)?;
654                }
655            }
656        }
657        b.build()
658    }
659
660    /// Build a plan with no sections — maps become `BindingBlock` and arrays
661    /// of maps become `PerElement(BindingBlock)`.
662    pub fn flat(doc: EureDocument) -> Result<Self, PlanError> {
663        let mut b = Self::builder(doc);
664        let all = traverse::all_reachable_ids(b.document());
665        let root = b.document().get_root_id();
666        for id in all {
667            if id == root {
668                continue;
669            }
670            let kind = b.document().node(id).content.value_kind();
671            match kind {
672                ValueKind::Map | ValueKind::PartialMap => {
673                    b.set_form(id, Form::BindingBlock)?;
674                }
675                ValueKind::Array => {
676                    let form = match auto::auto_array_form(b.document(), id, false) {
677                        ArrayForm::PerElement(_) | ArrayForm::PerElementIndexed(_) => {
678                            ArrayForm::PerElement(Form::BindingBlock)
679                        }
680                        ArrayForm::Inline => ArrayForm::Inline,
681                    };
682                    b.set_array_form(id, form)?;
683                }
684                _ => {
685                    b.set_form(id, Form::Inline)?;
686                }
687            }
688        }
689        b.build()
690    }
691
692    /// Borrow the underlying document.
693    pub fn document(&self) -> &EureDocument {
694        &self.doc
695    }
696
697    /// Return the assigned [`Form`] for a non-Array node.
698    pub fn form_of(&self, id: NodeId) -> Option<Form> {
699        self.forms.get(&id).copied()
700    }
701
702    /// Return the assigned [`ArrayForm`] for an Array node.
703    pub fn array_form_of(&self, id: NodeId) -> Option<ArrayForm> {
704        self.array_forms.get(&id).copied()
705    }
706
707    /// Return the ordered children override for a parent, if any.
708    pub fn order_of(&self, parent: NodeId) -> Option<&[NodeId]> {
709        self.order.get(&parent).map(|v| v.as_slice())
710    }
711
712    /// Emit a [`crate::source::SourceDocument`] from this plan.
713    pub fn emit(self) -> crate::source::SourceDocument {
714        emit::emit(self)
715    }
716
717    fn validate_structure(&self) -> Result<(), PlanError> {
718        // Structural checks: section placement, coverage, uniqueness.
719        emit::dry_walk_validate(self)
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726    use crate::document::constructor::DocumentConstructor;
727    use crate::path::ArrayIndexKind;
728    use crate::value::{ObjectKey, PrimitiveValue};
729    use alloc::vec;
730
731    fn scalar_doc() -> EureDocument {
732        let mut c = DocumentConstructor::new();
733        c.bind_empty_map().unwrap();
734        let scope = c.begin_scope();
735        c.navigate(PathSegment::Value(ObjectKey::String("name".into())))
736            .unwrap();
737        c.bind_primitive(PrimitiveValue::from("Alice")).unwrap();
738        c.end_scope(scope).unwrap();
739        c.finish()
740    }
741
742    fn array_of_maps_doc() -> EureDocument {
743        let mut c = DocumentConstructor::new();
744        c.bind_empty_map().unwrap();
745        let outer = c.begin_scope();
746        c.navigate(PathSegment::Value(ObjectKey::String("items".into())))
747            .unwrap();
748        c.bind_empty_array().unwrap();
749
750        for name in ["a", "b"] {
751            let elem_scope = c.begin_scope();
752            c.navigate(PathSegment::ArrayIndex(ArrayIndexKind::Push))
753                .unwrap();
754            c.bind_empty_map().unwrap();
755            let inner = c.begin_scope();
756            c.navigate(PathSegment::Value(ObjectKey::String("name".into())))
757                .unwrap();
758            c.bind_primitive(PrimitiveValue::from(name)).unwrap();
759            c.end_scope(inner).unwrap();
760            c.end_scope(elem_scope).unwrap();
761        }
762        c.end_scope(outer).unwrap();
763        c.finish()
764    }
765
766    fn nested_map_doc() -> EureDocument {
767        let mut c = DocumentConstructor::new();
768        c.bind_empty_map().unwrap();
769
770        let outer = c.begin_scope();
771        c.navigate(PathSegment::Value(ObjectKey::String("outer".into())))
772            .unwrap();
773        c.bind_empty_map().unwrap();
774
775        let inner_scope = c.begin_scope();
776        c.navigate(PathSegment::Value(ObjectKey::String("inner".into())))
777            .unwrap();
778        c.bind_empty_map().unwrap();
779
780        let leaf_scope = c.begin_scope();
781        c.navigate(PathSegment::Value(ObjectKey::String("name".into())))
782            .unwrap();
783        c.bind_primitive(PrimitiveValue::from("Ada")).unwrap();
784        c.end_scope(leaf_scope).unwrap();
785
786        c.end_scope(inner_scope).unwrap();
787        c.end_scope(outer).unwrap();
788        c.finish()
789    }
790
791    fn scalar_array_doc() -> EureDocument {
792        let mut c = DocumentConstructor::new();
793        c.bind_empty_map().unwrap();
794        let outer = c.begin_scope();
795        c.navigate(PathSegment::Value(ObjectKey::String("items".into())))
796            .unwrap();
797        c.bind_empty_array().unwrap();
798
799        for value in [1_i64, 2] {
800            let elem_scope = c.begin_scope();
801            c.navigate(PathSegment::ArrayIndex(ArrayIndexKind::Push))
802                .unwrap();
803            c.bind_primitive(PrimitiveValue::Integer(value.into()))
804                .unwrap();
805            c.end_scope(elem_scope).unwrap();
806        }
807
808        c.end_scope(outer).unwrap();
809        c.finish()
810    }
811
812    fn array_of_partial_maps_doc() -> EureDocument {
813        let mut c = DocumentConstructor::new();
814        c.bind_empty_map().unwrap();
815        let outer = c.begin_scope();
816        c.navigate(PathSegment::Value(ObjectKey::String("items".into())))
817            .unwrap();
818        c.bind_empty_array().unwrap();
819
820        let elem_scope = c.begin_scope();
821        c.navigate(PathSegment::ArrayIndex(ArrayIndexKind::Push))
822            .unwrap();
823        c.bind_empty_partial_map().unwrap();
824        let map_scope = c.begin_scope();
825        c.navigate(PathSegment::HoleKey(Some("x".parse().unwrap())))
826            .unwrap();
827        c.bind_primitive(PrimitiveValue::Integer(1.into())).unwrap();
828        c.end_scope(map_scope).unwrap();
829        c.end_scope(elem_scope).unwrap();
830
831        c.end_scope(outer).unwrap();
832        c.finish()
833    }
834
835    fn scalar_with_extension_doc() -> EureDocument {
836        let mut c = DocumentConstructor::new();
837        c.bind_empty_map().unwrap();
838
839        let outer = c.begin_scope();
840        c.navigate(PathSegment::Value(ObjectKey::String("name".into())))
841            .unwrap();
842        c.bind_primitive(PrimitiveValue::from("Alice")).unwrap();
843
844        let ext_scope = c.begin_scope();
845        c.navigate(PathSegment::Extension("meta".parse().unwrap()))
846            .unwrap();
847        c.bind_empty_map().unwrap();
848        let meta_scope = c.begin_scope();
849        c.navigate(PathSegment::Value(ObjectKey::String("alpha".into())))
850            .unwrap();
851        c.bind_primitive(PrimitiveValue::Integer(1.into())).unwrap();
852        c.end_scope(meta_scope).unwrap();
853        c.end_scope(ext_scope).unwrap();
854
855        c.end_scope(outer).unwrap();
856        c.finish()
857    }
858
859    fn root_extension_doc() -> EureDocument {
860        let mut c = DocumentConstructor::new();
861        c.bind_empty_map().unwrap();
862        c.set_extension("meta", true).unwrap();
863        let scope = c.begin_scope();
864        c.navigate(PathSegment::Value(ObjectKey::String("name".into())))
865            .unwrap();
866        c.bind_primitive(PrimitiveValue::from("Alice")).unwrap();
867        c.end_scope(scope).unwrap();
868        c.finish()
869    }
870
871    #[test]
872    fn auto_scalar_produces_inline_binding() {
873        let doc = scalar_doc();
874        let plan = LayoutPlan::auto(doc).unwrap();
875        let src = plan.emit();
876        let root = src.root_source();
877        assert_eq!(root.bindings.len(), 1);
878        assert!(matches!(
879            root.bindings[0].bind,
880            crate::source::BindSource::Value(_)
881        ));
882        assert!(root.sections.is_empty());
883    }
884
885    #[test]
886    fn auto_array_of_maps_emits_array_of_sections() {
887        let doc = array_of_maps_doc();
888        let plan = LayoutPlan::auto(doc).unwrap();
889        let src = plan.emit();
890        let root = src.root_source();
891
892        assert_eq!(root.sections.len(), 2, "expected two `@ items[]` sections");
893        for section in &root.sections {
894            let last = section.path.last().unwrap();
895            assert_eq!(
896                last.array,
897                Some(ArrayIndexKind::Push),
898                "expected push marker `[]` on array section"
899            );
900        }
901    }
902
903    #[test]
904    fn flat_array_of_maps_uses_binding_blocks() {
905        let doc = array_of_maps_doc();
906        let plan = LayoutPlan::flat(doc).unwrap();
907        let src = plan.emit();
908        let root = src.root_source();
909
910        assert_eq!(root.sections.len(), 0);
911        assert_eq!(root.bindings.len(), 2);
912        for b in &root.bindings {
913            assert!(matches!(b.bind, crate::source::BindSource::Block(_)));
914            assert_eq!(b.path.last().unwrap().array, Some(ArrayIndexKind::Push));
915        }
916    }
917
918    #[test]
919    fn set_form_on_array_rejected() {
920        let doc = array_of_maps_doc();
921        let mut b = LayoutPlan::builder(doc);
922        let id = b
923            .node_at(&[PathSegment::Value(ObjectKey::String("items".into()))])
924            .unwrap();
925        let err = b.set_form(id, Form::Section).unwrap_err();
926        assert!(matches!(err, PlanError::FormOnArrayNode(_)));
927    }
928
929    #[test]
930    fn set_array_form_on_non_array_rejected() {
931        let doc = scalar_doc();
932        let mut b = LayoutPlan::builder(doc);
933        let id = b
934            .node_at(&[PathSegment::Value(ObjectKey::String("name".into()))])
935            .unwrap();
936        let err = b.set_array_form(id, ArrayForm::Inline).unwrap_err();
937        assert!(matches!(err, PlanError::ArrayFormOnNonArray(_)));
938    }
939
940    #[test]
941    fn per_element_flatten_rejected() {
942        let doc = array_of_maps_doc();
943        let mut b = LayoutPlan::builder(doc);
944        let id = b
945            .node_at(&[PathSegment::Value(ObjectKey::String("items".into()))])
946            .unwrap();
947        let err = b
948            .set_array_form(id, ArrayForm::PerElement(Form::Flatten))
949            .unwrap_err();
950        assert!(matches!(err, PlanError::IncompatibleArrayForm { .. }));
951    }
952
953    #[test]
954    fn sectioned_nested_maps_rejected_in_items_context() {
955        let err = LayoutPlan::sectioned(nested_map_doc()).unwrap_err();
956        assert!(matches!(err, PlanError::SectionInForbiddenContext(_)));
957    }
958
959    #[test]
960    fn value_block_forms_rejected_for_maps() {
961        let doc = nested_map_doc();
962        let mut b = LayoutPlan::builder(doc);
963        let outer_id = b
964            .node_at(&[PathSegment::Value(ObjectKey::String("outer".into()))])
965            .unwrap();
966
967        let err = b.set_form(outer_id, Form::BindingValueBlock).unwrap_err();
968        assert!(matches!(err, PlanError::IncompatibleForm { .. }));
969
970        let err = b.set_form(outer_id, Form::SectionValueBlock).unwrap_err();
971        assert!(matches!(err, PlanError::IncompatibleForm { .. }));
972    }
973
974    #[test]
975    fn scalar_arrays_support_section_value_block_elements() {
976        let doc = scalar_array_doc();
977        let mut b = LayoutPlan::builder(doc);
978        let items_id = b
979            .node_at(&[PathSegment::Value(ObjectKey::String("items".into()))])
980            .unwrap();
981        b.set_array_form(items_id, ArrayForm::PerElement(Form::SectionValueBlock))
982            .unwrap();
983
984        let src = b.build().unwrap().emit();
985        let root = src.root_source();
986        assert_eq!(root.sections.len(), 2);
987        for section in &root.sections {
988            match &section.body {
989                crate::source::SectionBody::Items { value, bindings } => {
990                    assert!(value.is_some());
991                    assert!(bindings.is_empty());
992                }
993                other => panic!("expected items body, got {other:?}"),
994            }
995        }
996    }
997
998    #[test]
999    fn per_element_inline_rejected_for_partial_map_elements() {
1000        let doc = array_of_partial_maps_doc();
1001        let mut b = LayoutPlan::builder(doc);
1002        let items_id = b
1003            .node_at(&[PathSegment::Value(ObjectKey::String("items".into()))])
1004            .unwrap();
1005        let err = b
1006            .set_array_form(items_id, ArrayForm::PerElement(Form::Inline))
1007            .unwrap_err();
1008        assert!(matches!(
1009            err,
1010            PlanError::IncompatibleArrayForm {
1011                reason: ArrayFormReason::ElementIncompatibleForm {
1012                    kind: ValueKind::PartialMap,
1013                    ..
1014                },
1015                ..
1016            }
1017        ));
1018    }
1019
1020    #[test]
1021    fn inline_extensions_inherit_section_context() {
1022        let doc = scalar_with_extension_doc();
1023        let mut b = LayoutPlan::builder(doc);
1024        b.set_form_at(
1025            &[PathSegment::Value(ObjectKey::String("name".into()))],
1026            Form::Inline,
1027        )
1028        .unwrap();
1029        b.set_form_at(
1030            &[
1031                PathSegment::Value(ObjectKey::String("name".into())),
1032                PathSegment::Extension("meta".parse().unwrap()),
1033            ],
1034            Form::Section,
1035        )
1036        .unwrap();
1037
1038        let src = b.build().unwrap().emit();
1039        let root = src.root_source();
1040        assert_eq!(root.bindings.len(), 1);
1041        assert_eq!(root.sections.len(), 1);
1042    }
1043
1044    #[test]
1045    fn per_element_section_block_roundtrips_path_with_push_marker() {
1046        let doc = array_of_maps_doc();
1047        let mut b = LayoutPlan::builder(doc);
1048        let id = b
1049            .node_at(&[PathSegment::Value(ObjectKey::String("items".into()))])
1050            .unwrap();
1051        b.set_array_form(id, ArrayForm::PerElement(Form::SectionBlock))
1052            .unwrap();
1053        let plan = b.build().unwrap();
1054        let src = plan.emit();
1055        let root = src.root_source();
1056        assert_eq!(root.sections.len(), 2);
1057        for section in &root.sections {
1058            assert!(matches!(section.body, crate::source::SectionBody::Block(_)));
1059            assert_eq!(
1060                section.path.last().unwrap().array,
1061                Some(ArrayIndexKind::Push)
1062            );
1063        }
1064    }
1065
1066    #[test]
1067    fn path_not_found_error() {
1068        let doc = scalar_doc();
1069        let b = LayoutPlan::builder(doc);
1070        let err = b
1071            .node_at(&[PathSegment::Value(ObjectKey::String("missing".into()))])
1072            .unwrap_err();
1073        assert!(matches!(err, PlanError::PathNotFound { .. }));
1074    }
1075
1076    #[test]
1077    fn order_on_array_rejected() {
1078        let doc = array_of_maps_doc();
1079        let mut b = LayoutPlan::builder(doc);
1080        let items_id = b
1081            .node_at(&[PathSegment::Value(ObjectKey::String("items".into()))])
1082            .unwrap();
1083        let err = b.order(items_id, vec![]).unwrap_err();
1084        assert!(matches!(err, PlanError::OrderOnArrayNode(_)));
1085    }
1086
1087    #[test]
1088    fn ordering_extensions_is_rejected() {
1089        let doc = root_extension_doc();
1090        let mut b = LayoutPlan::builder(doc);
1091        let err = b
1092            .order_at(&[], vec![PathSegment::Extension("meta".parse().unwrap())])
1093            .unwrap_err();
1094        assert!(matches!(err, PlanError::OrderExtensionChild { .. }));
1095    }
1096
1097    #[test]
1098    fn set_form_on_root_rejected() {
1099        let doc = scalar_doc();
1100        let root = doc.get_root_id();
1101        let mut b = LayoutPlan::builder(doc);
1102        let err = b.set_form(root, Form::Section).unwrap_err();
1103        assert!(matches!(err, PlanError::FormOnRoot(_)));
1104    }
1105}