eure_document/
source.rs

1//! Source-level document representation for programmatic construction and formatting.
2//!
3//! This module provides types for representing Eure source structure with layout metadata,
4//! while actual values are referenced via [`NodeId`] into an [`EureDocument`].
5//!
6//! # Design
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────┐
10//! │              SourceDocument                  │
11//! │  ┌─────────────────┐  ┌──────────────────┐  │
12//! │  │  EureDocument   │  │     Layout       │  │
13//! │  │  ┌───────────┐  │  │                  │  │
14//! │  │  │ NodeId(0) │◄─┼──┼─ Binding.node    │  │
15//! │  │  │ NodeId(1) │◄─┼──┼─ Binding.node    │  │
16//! │  │  │ NodeId(2) │◄─┼──┼─ Binding.node    │  │
17//! │  │  └───────────┘  │  │                  │  │
18//! │  └─────────────────┘  └──────────────────┘  │
19//! └─────────────────────────────────────────────┘
20//! ```
21//!
22//! - **EureDocument**: Holds semantic data (values)
23//! - **Layout**: Holds presentation metadata (comments, ordering, section structure)
24//!
25//! # Example
26//!
27//! ```ignore
28//! // Convert from TOML, preserving comments and section ordering
29//! let source = eure_toml::to_source_document(&toml_doc);
30//!
31//! // Modify values (layout is preserved)
32//! let node = source.find_binding(&["server", "port"]).unwrap();
33//! source.document.node_mut(node).set_primitive(8080.into());
34//!
35//! // Format to Eure string
36//! let output = eure_fmt::format_source(&source, &config);
37//! ```
38
39use std::collections::HashSet;
40
41use crate::document::{EureDocument, NodeId};
42use crate::prelude_internal::*;
43
44/// A document with layout/presentation metadata.
45///
46/// Combines semantic data ([`EureDocument`]) with presentation information ([`Layout`])
47/// for round-trip conversions from formats like TOML, preserving comments and ordering.
48#[derive(Debug, Clone)]
49pub struct SourceDocument {
50    /// The semantic data (values, structure)
51    pub document: EureDocument,
52    /// The presentation layout (comments, ordering, sections)
53    pub layout: Layout,
54}
55
56impl SourceDocument {
57    /// Create a new source document with the given document and layout.
58    pub fn new(document: EureDocument, layout: Layout) -> Self {
59        Self { document, layout }
60    }
61
62    /// Create an empty source document.
63    pub fn empty() -> Self {
64        Self {
65            document: EureDocument::new_empty(),
66            layout: Layout::new(),
67        }
68    }
69}
70
71/// Layout information describing how to render the document.
72#[derive(Debug, Clone, Default)]
73pub struct Layout {
74    /// Top-level items in order
75    pub items: Vec<LayoutItem>,
76    /// Nodes that should be formatted with multiple lines
77    pub multiline_nodes: HashSet<NodeId>,
78}
79
80impl Layout {
81    /// Create an empty layout.
82    pub fn new() -> Self {
83        Self {
84            items: Vec::new(),
85            multiline_nodes: HashSet::new(),
86        }
87    }
88
89    /// Add an item to the layout.
90    pub fn push(&mut self, item: LayoutItem) {
91        self.items.push(item);
92    }
93}
94
95/// An item in the layout.
96#[derive(Debug, Clone, PartialEq)]
97pub enum LayoutItem {
98    /// A comment (line or block)
99    Comment(Comment),
100
101    /// A blank line for visual separation
102    BlankLine,
103
104    /// A key-value binding: `path.to.key = <value from NodeId>`
105    Binding {
106        /// Path to the binding target
107        path: SourcePath,
108        /// Reference to the value node in EureDocument
109        node: NodeId,
110        /// Optional trailing comment: `key = value // comment`
111        trailing_comment: Option<String>,
112    },
113
114    /// A section header: `@ path.to.section`
115    Section {
116        /// Path to the section
117        path: SourcePath,
118        /// Optional trailing comment: `@ section // comment`
119        trailing_comment: Option<String>,
120        /// Section body
121        body: SectionBody,
122    },
123
124    /// An array binding with per-element layout information.
125    ///
126    /// Used when an array has comments between elements that need to be preserved.
127    /// ```eure
128    /// items = [
129    ///   // First item
130    ///   "one",
131    ///   // Second item
132    ///   "two",
133    /// ]
134    /// ```
135    ArrayBinding {
136        /// Path to the binding target
137        path: SourcePath,
138        /// Reference to the array node in EureDocument
139        node: NodeId,
140        /// Per-element layout information (comments before each element)
141        elements: Vec<ArrayElementLayout>,
142        /// Optional trailing comment
143        trailing_comment: Option<String>,
144    },
145}
146
147/// The body of a section.
148#[derive(Debug, Clone, PartialEq)]
149pub enum SectionBody {
150    /// Items following the section header (newline-separated)
151    /// ```eure
152    /// @ section
153    /// key1 = value1
154    /// key2 = value2
155    /// ```
156    Items(Vec<LayoutItem>),
157
158    /// Block syntax with braces
159    /// ```eure
160    /// @ section {
161    ///     key1 = value1
162    ///     key2 = value2
163    /// }
164    /// ```
165    Block(Vec<LayoutItem>),
166}
167
168/// A path in source representation.
169pub type SourcePath = Vec<SourcePathSegment>;
170
171/// A segment in a source path.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct SourcePathSegment {
174    /// The key part of the segment
175    pub key: SourceKey,
176    /// Optional array marker:
177    /// - `None` = no marker
178    /// - `Some(None)` = `[]` (push to array)
179    /// - `Some(Some(n))` = `[n]` (index into array)
180    pub array: Option<Option<usize>>,
181}
182
183impl SourcePathSegment {
184    /// Create a simple identifier segment without array marker.
185    pub fn ident(name: Identifier) -> Self {
186        Self {
187            key: SourceKey::Ident(name),
188            array: None,
189        }
190    }
191
192    /// Create an extension segment without array marker.
193    pub fn extension(name: Identifier) -> Self {
194        Self {
195            key: SourceKey::Extension(name),
196            array: None,
197        }
198    }
199
200    /// Create a segment with array push marker (`[]`).
201    pub fn with_array_push(mut self) -> Self {
202        self.array = Some(None);
203        self
204    }
205
206    /// Create a segment with array index marker (`[n]`).
207    pub fn with_array_index(mut self, index: usize) -> Self {
208        self.array = Some(Some(index));
209        self
210    }
211}
212
213/// A key in source representation.
214///
215/// This determines how the key should be rendered in the output.
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub enum SourceKey {
218    /// Bare identifier: `foo`, `bar_baz`
219    Ident(Identifier),
220
221    /// Extension namespace: `$variant`, `$eure`
222    Extension(Identifier),
223
224    /// Quoted string key: `"hello world"`
225    String(String),
226
227    /// Integer key: `123`
228    Integer(i64),
229
230    /// Tuple key: `(1, "a")`
231    Tuple(Vec<SourceKey>),
232
233    /// Tuple index: `#0`, `#1`
234    TupleIndex(u8),
235}
236
237impl From<Identifier> for SourceKey {
238    fn from(id: Identifier) -> Self {
239        SourceKey::Ident(id)
240    }
241}
242
243impl From<i64> for SourceKey {
244    fn from(n: i64) -> Self {
245        SourceKey::Integer(n)
246    }
247}
248
249/// A comment in the source.
250#[derive(Debug, Clone, PartialEq, Eq)]
251pub enum Comment {
252    /// Line comment: `// comment`
253    Line(String),
254    /// Block comment: `/* comment */`
255    Block(String),
256}
257
258impl Comment {
259    /// Create a line comment.
260    pub fn line(s: impl Into<String>) -> Self {
261        Comment::Line(s.into())
262    }
263
264    /// Create a block comment.
265    pub fn block(s: impl Into<String>) -> Self {
266        Comment::Block(s.into())
267    }
268
269    /// Get the comment text content.
270    pub fn text(&self) -> &str {
271        match self {
272            Comment::Line(s) | Comment::Block(s) => s,
273        }
274    }
275}
276
277/// Layout information for an array element.
278///
279/// Used to preserve comments that appear before array elements when converting
280/// from formats like TOML.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct ArrayElementLayout {
283    /// Comments that appear before this element in the source
284    pub comments_before: Vec<Comment>,
285    /// Trailing comment on the same line as this element
286    pub trailing_comment: Option<String>,
287    /// The index of this element in the array (corresponds to NodeArray)
288    pub index: usize,
289}
290
291// ============================================================================
292// Builder helpers
293// ============================================================================
294
295impl LayoutItem {
296    /// Create a line comment item.
297    pub fn line_comment(s: impl Into<String>) -> Self {
298        LayoutItem::Comment(Comment::Line(s.into()))
299    }
300
301    /// Create a block comment item.
302    pub fn block_comment(s: impl Into<String>) -> Self {
303        LayoutItem::Comment(Comment::Block(s.into()))
304    }
305
306    /// Create a binding item.
307    pub fn binding(path: SourcePath, node: NodeId) -> Self {
308        LayoutItem::Binding {
309            path,
310            node,
311            trailing_comment: None,
312        }
313    }
314
315    /// Create a binding item with trailing comment.
316    pub fn binding_with_comment(
317        path: SourcePath,
318        node: NodeId,
319        comment: impl Into<String>,
320    ) -> Self {
321        LayoutItem::Binding {
322            path,
323            node,
324            trailing_comment: Some(comment.into()),
325        }
326    }
327
328    /// Create a section item with items body.
329    pub fn section(path: SourcePath, items: Vec<LayoutItem>) -> Self {
330        LayoutItem::Section {
331            path,
332            trailing_comment: None,
333            body: SectionBody::Items(items),
334        }
335    }
336
337    /// Create a section item with block body.
338    pub fn section_block(path: SourcePath, items: Vec<LayoutItem>) -> Self {
339        LayoutItem::Section {
340            path,
341            trailing_comment: None,
342            body: SectionBody::Block(items),
343        }
344    }
345
346    /// Create a section item with trailing comment.
347    pub fn section_with_comment(
348        path: SourcePath,
349        comment: impl Into<String>,
350        items: Vec<LayoutItem>,
351    ) -> Self {
352        LayoutItem::Section {
353            path,
354            trailing_comment: Some(comment.into()),
355            body: SectionBody::Items(items),
356        }
357    }
358
359    /// Create an array binding item with per-element layout.
360    pub fn array_binding(
361        path: SourcePath,
362        node: NodeId,
363        elements: Vec<ArrayElementLayout>,
364    ) -> Self {
365        LayoutItem::ArrayBinding {
366            path,
367            node,
368            elements,
369            trailing_comment: None,
370        }
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_source_path_segment_ident() {
380        let actual = SourcePathSegment::ident(Identifier::new_unchecked("foo"));
381        let expected = SourcePathSegment {
382            key: SourceKey::Ident(Identifier::new_unchecked("foo")),
383            array: None,
384        };
385        assert_eq!(actual, expected);
386    }
387
388    #[test]
389    fn test_source_path_segment_with_array_push() {
390        let actual = SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_push();
391        let expected = SourcePathSegment {
392            key: SourceKey::Ident(Identifier::new_unchecked("items")),
393            array: Some(None),
394        };
395        assert_eq!(actual, expected);
396    }
397
398    #[test]
399    fn test_source_path_segment_with_array_index() {
400        let actual =
401            SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_index(0);
402        let expected = SourcePathSegment {
403            key: SourceKey::Ident(Identifier::new_unchecked("items")),
404            array: Some(Some(0)),
405        };
406        assert_eq!(actual, expected);
407    }
408
409    #[test]
410    fn test_layout_item_binding() {
411        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked("foo"))];
412        let actual = LayoutItem::binding(path.clone(), NodeId(0));
413        let expected = LayoutItem::Binding {
414            path,
415            node: NodeId(0),
416            trailing_comment: None,
417        };
418        assert_eq!(actual, expected);
419    }
420
421    #[test]
422    fn test_layout_item_section_with_comment() {
423        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked(
424            "config",
425        ))];
426        let actual = LayoutItem::section_with_comment(path.clone(), "this is config", vec![]);
427        let expected = LayoutItem::Section {
428            path,
429            trailing_comment: Some("this is config".into()),
430            body: SectionBody::Items(vec![]),
431        };
432        assert_eq!(actual, expected);
433    }
434
435    #[test]
436    fn test_source_document_empty() {
437        let doc = SourceDocument::empty();
438        assert!(doc.layout.items.is_empty());
439        assert!(doc.layout.multiline_nodes.is_empty());
440    }
441}