styx_tree/
builder.rs

1//! Tree builder from parse events.
2
3use std::borrow::Cow;
4
5use styx_parse::{Event, ParseCallback, ParseErrorKind, Separator, Span};
6
7use crate::value::{Entry, Object, Payload, Scalar, Sequence, Tag, Value};
8
9/// Error during tree building.
10#[derive(Debug, Clone, PartialEq)]
11pub enum BuildError {
12    /// Unexpected event during building.
13    UnexpectedEvent(String),
14    /// Unclosed structure.
15    UnclosedStructure,
16    /// Empty document.
17    EmptyDocument,
18    /// Parse error from the lexer/parser.
19    Parse(ParseErrorKind, Span),
20}
21
22impl std::fmt::Display for BuildError {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            BuildError::UnexpectedEvent(msg) => write!(f, "unexpected event: {}", msg),
26            BuildError::UnclosedStructure => write!(f, "unclosed structure"),
27            BuildError::EmptyDocument => write!(f, "empty document"),
28            BuildError::Parse(kind, span) => {
29                write!(f, "parse error at {}-{}: {}", span.start, span.end, kind)
30            }
31        }
32    }
33}
34
35impl std::error::Error for BuildError {}
36
37impl BuildError {
38    /// If this is a parse error, return it as a `ParseError` for diagnostic rendering.
39    pub fn as_parse_error(&self) -> Option<crate::diagnostic::ParseError> {
40        match self {
41            BuildError::Parse(kind, span) => {
42                Some(crate::diagnostic::ParseError::new(kind.clone(), *span))
43            }
44            _ => None,
45        }
46    }
47}
48
49/// Builder that constructs a tree from parse events.
50pub struct TreeBuilder {
51    stack: Vec<BuilderFrame>,
52    root_entries: Vec<Entry>,
53    pending_doc_comment: Option<String>,
54    errors: Vec<(ParseErrorKind, Span)>,
55}
56
57enum BuilderFrame {
58    Object {
59        entries: Vec<Entry>,
60        separator: Separator,
61        span: Span,
62        pending_doc_comment: Option<String>,
63    },
64    Sequence {
65        items: Vec<Value>,
66        span: Span,
67    },
68    Tag {
69        name: String,
70        span: Span,
71    },
72    Entry {
73        key: Option<Value>,
74        doc_comment: Option<String>,
75    },
76}
77
78impl TreeBuilder {
79    /// Create a new tree builder.
80    pub fn new() -> Self {
81        Self {
82            stack: Vec::new(),
83            root_entries: Vec::new(),
84            pending_doc_comment: None,
85            errors: Vec::new(),
86        }
87    }
88
89    /// Finish building and return the root value.
90    pub fn finish(self) -> Result<Value, BuildError> {
91        // Return the first error if any occurred during parsing
92        if let Some((kind, span)) = self.errors.into_iter().next() {
93            return Err(BuildError::Parse(kind, span));
94        }
95
96        if !self.stack.is_empty() {
97            return Err(BuildError::UnclosedStructure);
98        }
99
100        // Root is always an implicit object (no tag)
101        Ok(Value {
102            tag: None,
103            payload: Some(Payload::Object(Object {
104                entries: self.root_entries,
105                separator: Separator::Newline,
106                span: None,
107            })),
108            span: None,
109        })
110    }
111
112    /// Push a value to the current context.
113    fn push_value(&mut self, value: Value) {
114        // First, check if we're in a Tag frame - if so, the value becomes the tag's payload
115        if let Some(BuilderFrame::Tag { .. }) = self.stack.last() {
116            // Pop the tag frame
117            if let Some(BuilderFrame::Tag { name, span }) = self.stack.pop() {
118                // Create tagged value: the tag wraps the value's payload
119                let tagged = Value {
120                    tag: Some(Tag {
121                        name,
122                        span: Some(span),
123                    }),
124                    payload: value.payload,
125                    span: value.span,
126                };
127                // Recursively push the tagged value
128                self.push_value(tagged);
129            }
130            return;
131        }
132
133        // Check if we're in an Entry frame with a key - if so, this value completes the entry
134        if let Some(BuilderFrame::Entry { key: Some(_), .. }) = self.stack.last() {
135            // Pop the entry frame and add the complete entry to parent
136            if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.pop() {
137                let key_val = key.unwrap();
138                match self.stack.last_mut() {
139                    Some(BuilderFrame::Object { entries, .. }) => {
140                        entries.push(Entry {
141                            key: key_val,
142                            value,
143                            doc_comment,
144                        });
145                    }
146                    _ => {
147                        self.root_entries.push(Entry {
148                            key: key_val,
149                            value,
150                            doc_comment,
151                        });
152                    }
153                }
154                // Re-push an empty entry frame for potential continuation
155                self.stack.push(BuilderFrame::Entry {
156                    key: None,
157                    doc_comment: None,
158                });
159            }
160            return;
161        }
162
163        match self.stack.last_mut() {
164            Some(BuilderFrame::Object {
165                entries,
166                pending_doc_comment,
167                ..
168            }) => {
169                // Value for an entry without explicit key - use unit key
170                entries.push(Entry {
171                    key: Value::unit(),
172                    value,
173                    doc_comment: pending_doc_comment.take(),
174                });
175            }
176            Some(BuilderFrame::Sequence { items, .. }) => {
177                items.push(value);
178            }
179            Some(BuilderFrame::Tag { .. }) => {
180                // Already handled above
181                unreachable!()
182            }
183            Some(BuilderFrame::Entry { key, .. }) => {
184                if key.is_none() {
185                    // This is the key
186                    *key = Some(value);
187                }
188            }
189            None => {
190                // Root level - treat as entry in implicit object
191                self.root_entries.push(Entry {
192                    key: Value::unit(),
193                    value,
194                    doc_comment: self.pending_doc_comment.take(),
195                });
196            }
197        }
198    }
199}
200
201impl Default for TreeBuilder {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207impl<'src> ParseCallback<'src> for TreeBuilder {
208    fn event(&mut self, event: Event<'src>) -> bool {
209        match event {
210            Event::DocumentStart | Event::DocumentEnd => {
211                // No-op for tree building
212            }
213
214            Event::ObjectStart { span, separator } => {
215                self.stack.push(BuilderFrame::Object {
216                    entries: Vec::new(),
217                    separator,
218                    span,
219                    pending_doc_comment: None,
220                });
221            }
222
223            Event::ObjectEnd { span } => {
224                if let Some(BuilderFrame::Object {
225                    entries,
226                    separator,
227                    span: start_span,
228                    ..
229                }) = self.stack.pop()
230                {
231                    let obj = Value {
232                        tag: None,
233                        payload: Some(Payload::Object(Object {
234                            entries,
235                            separator,
236                            span: Some(Span {
237                                start: start_span.start,
238                                end: span.end,
239                            }),
240                        })),
241                        span: Some(Span {
242                            start: start_span.start,
243                            end: span.end,
244                        }),
245                    };
246                    self.push_value(obj);
247                }
248            }
249
250            Event::SequenceStart { span } => {
251                self.stack.push(BuilderFrame::Sequence {
252                    items: Vec::new(),
253                    span,
254                });
255            }
256
257            Event::SequenceEnd { span } => {
258                if let Some(BuilderFrame::Sequence {
259                    items,
260                    span: start_span,
261                }) = self.stack.pop()
262                {
263                    let seq = Value {
264                        tag: None,
265                        payload: Some(Payload::Sequence(Sequence {
266                            items,
267                            span: Some(Span {
268                                start: start_span.start,
269                                end: span.end,
270                            }),
271                        })),
272                        span: Some(Span {
273                            start: start_span.start,
274                            end: span.end,
275                        }),
276                    };
277                    self.push_value(seq);
278                }
279            }
280
281            Event::EntryStart => {
282                let doc_comment = match self.stack.last_mut() {
283                    Some(BuilderFrame::Object {
284                        pending_doc_comment,
285                        ..
286                    }) => pending_doc_comment.take(),
287                    _ => self.pending_doc_comment.take(),
288                };
289                self.stack.push(BuilderFrame::Entry {
290                    key: None,
291                    doc_comment,
292                });
293            }
294
295            Event::EntryEnd => {
296                if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.pop()
297                    && let Some(key) = key
298                {
299                    // We have a key but might not have a value yet
300                    match self.stack.last_mut() {
301                        Some(BuilderFrame::Object { entries, .. }) => {
302                            // Check if last entry needs this key
303                            if let Some(last) = entries.last_mut()
304                                && last.key.is_unit()
305                                && last.doc_comment.is_none()
306                            {
307                                last.key = key;
308                                last.doc_comment = doc_comment;
309                                return true;
310                            }
311                            // Otherwise add as unit-valued entry
312                            entries.push(Entry {
313                                key,
314                                value: Value::unit(),
315                                doc_comment,
316                            });
317                        }
318                        _ => {
319                            // Root level
320                            if let Some(last) = self.root_entries.last_mut()
321                                && last.key.is_unit()
322                                && last.doc_comment.is_none()
323                            {
324                                last.key = key;
325                                last.doc_comment = doc_comment;
326                                return true;
327                            }
328                            self.root_entries.push(Entry {
329                                key,
330                                value: Value::unit(),
331                                doc_comment,
332                            });
333                        }
334                    }
335                }
336            }
337
338            Event::Key {
339                span,
340                tag,
341                payload,
342                kind,
343            } => {
344                let key_value = Value {
345                    tag: tag.map(|name| Tag {
346                        name: name.to_string(),
347                        span: Some(span),
348                    }),
349                    payload: payload.map(|text| {
350                        Payload::Scalar(Scalar {
351                            text: cow_to_string(text),
352                            kind,
353                            span: Some(span),
354                        })
355                    }),
356                    span: Some(span),
357                };
358                if let Some(BuilderFrame::Entry { key, .. }) = self.stack.last_mut() {
359                    *key = Some(key_value);
360                }
361            }
362
363            Event::Scalar { span, value, kind } => {
364                let scalar = Value {
365                    tag: None,
366                    payload: Some(Payload::Scalar(Scalar {
367                        text: cow_to_string(value),
368                        kind,
369                        span: Some(span),
370                    })),
371                    span: Some(span),
372                };
373
374                // Check if we're in an entry context with a key already set
375                if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
376                    && key.is_some()
377                {
378                    // We have a key, this is the value
379                    let key_val = key.take().unwrap();
380                    let doc = doc_comment.take();
381
382                    // Pop the entry frame
383                    self.stack.pop();
384
385                    // Add to parent
386                    match self.stack.last_mut() {
387                        Some(BuilderFrame::Object { entries, .. }) => {
388                            entries.push(Entry {
389                                key: key_val,
390                                value: scalar,
391                                doc_comment: doc,
392                            });
393                        }
394                        _ => {
395                            self.root_entries.push(Entry {
396                                key: key_val,
397                                value: scalar,
398                                doc_comment: doc,
399                            });
400                        }
401                    }
402                    // Re-push entry frame for potential more processing
403                    self.stack.push(BuilderFrame::Entry {
404                        key: None,
405                        doc_comment: None,
406                    });
407                    return true;
408                }
409
410                self.push_value(scalar);
411            }
412
413            Event::Unit { span } => {
414                let unit = Value {
415                    tag: None,
416                    payload: None,
417                    span: Some(span),
418                };
419
420                // Similar logic to Scalar for entry handling
421                if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
422                    && key.is_some()
423                {
424                    let key_val = key.take().unwrap();
425                    let doc = doc_comment.take();
426                    self.stack.pop();
427
428                    match self.stack.last_mut() {
429                        Some(BuilderFrame::Object { entries, .. }) => {
430                            entries.push(Entry {
431                                key: key_val,
432                                value: unit,
433                                doc_comment: doc,
434                            });
435                        }
436                        _ => {
437                            self.root_entries.push(Entry {
438                                key: key_val,
439                                value: unit,
440                                doc_comment: doc,
441                            });
442                        }
443                    }
444                    self.stack.push(BuilderFrame::Entry {
445                        key: None,
446                        doc_comment: None,
447                    });
448                    return true;
449                }
450
451                self.push_value(unit);
452            }
453
454            Event::TagStart { span, name } => {
455                self.stack.push(BuilderFrame::Tag {
456                    name: name.to_string(),
457                    span,
458                });
459            }
460
461            Event::TagEnd => {
462                // Only pop if the top frame is a Tag - otherwise the tag was already
463                // consumed when its payload was processed
464                if !matches!(self.stack.last(), Some(BuilderFrame::Tag { .. })) {
465                    return true;
466                }
467                if let Some(BuilderFrame::Tag { name, span }) = self.stack.pop() {
468                    // Tag with no payload - just the tag itself
469                    let tagged = Value {
470                        tag: Some(Tag {
471                            name,
472                            span: Some(span),
473                        }),
474                        payload: None,
475                        span: Some(span),
476                    };
477
478                    // Similar to scalar handling
479                    if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
480                        && key.is_some()
481                    {
482                        let key_val = key.take().unwrap();
483                        let doc = doc_comment.take();
484                        self.stack.pop();
485
486                        match self.stack.last_mut() {
487                            Some(BuilderFrame::Object { entries, .. }) => {
488                                entries.push(Entry {
489                                    key: key_val,
490                                    value: tagged,
491                                    doc_comment: doc,
492                                });
493                            }
494                            _ => {
495                                self.root_entries.push(Entry {
496                                    key: key_val,
497                                    value: tagged,
498                                    doc_comment: doc,
499                                });
500                            }
501                        }
502                        self.stack.push(BuilderFrame::Entry {
503                            key: None,
504                            doc_comment: None,
505                        });
506                        return true;
507                    }
508
509                    self.push_value(tagged);
510                }
511            }
512
513            Event::DocComment { text, .. } => {
514                let comment = extract_doc_comment(text);
515                match self.stack.last_mut() {
516                    Some(BuilderFrame::Object {
517                        pending_doc_comment,
518                        ..
519                    }) => {
520                        append_doc_comment(pending_doc_comment, comment);
521                    }
522                    _ => {
523                        append_doc_comment(&mut self.pending_doc_comment, comment);
524                    }
525                }
526            }
527
528            Event::Comment { .. } => {
529                // Ignore regular comments for tree building
530            }
531
532            Event::Error { span, kind } => {
533                self.errors.push((kind, span));
534            }
535        }
536
537        true
538    }
539}
540
541fn cow_to_string(cow: Cow<'_, str>) -> String {
542    cow.into_owned()
543}
544
545fn extract_doc_comment(text: &str) -> String {
546    // Strip leading `///` and optional space
547    text.strip_prefix("///")
548        .map(|s| s.strip_prefix(' ').unwrap_or(s))
549        .unwrap_or(text)
550        .to_string()
551}
552
553/// Append a doc comment line to an existing doc comment, joining with newline.
554fn append_doc_comment(target: &mut Option<String>, line: String) {
555    match target {
556        Some(existing) => {
557            existing.push('\n');
558            existing.push_str(&line);
559        }
560        None => {
561            *target = Some(line);
562        }
563    }
564}
565
566#[cfg(test)]
567mod tests {
568    use styx_parse::Parser;
569
570    use super::*;
571
572    fn parse(source: &str) -> Value {
573        let parser = Parser::new(source);
574        let mut builder = TreeBuilder::new();
575        parser.parse(&mut builder);
576        builder.finish().unwrap()
577    }
578
579    fn try_parse(source: &str) -> Result<Value, BuildError> {
580        let parser = Parser::new(source);
581        let mut builder = TreeBuilder::new();
582        parser.parse(&mut builder);
583        builder.finish()
584    }
585
586    #[test]
587    fn test_empty_document() {
588        let value = parse("");
589        assert!(value.as_object().unwrap().is_empty());
590    }
591
592    #[test]
593    fn test_simple_entry() {
594        let value = parse("name Alice");
595        let obj = value.as_object().unwrap();
596        assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Alice"));
597    }
598
599    #[test]
600    fn test_multiple_entries() {
601        let value = parse("name Alice\nage 30");
602        let obj = value.as_object().unwrap();
603        assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Alice"));
604        assert_eq!(obj.get("age").and_then(|v| v.as_str()), Some("30"));
605    }
606
607    #[test]
608    fn test_path_access() {
609        let value = parse("name Alice\nage 30");
610        assert_eq!(value.get("name").and_then(|v| v.as_str()), Some("Alice"));
611        assert_eq!(value.get("age").and_then(|v| v.as_str()), Some("30"));
612    }
613
614    #[test]
615    fn test_unit_value() {
616        let value = parse("enabled @");
617        let obj = value.as_object().unwrap();
618        assert!(obj.get("enabled").unwrap().is_unit());
619    }
620
621    #[test]
622    fn test_unit_key() {
623        // @ followed by a value should create a unit key
624        let value = parse("@ server.schema.styx");
625        let obj = value.as_object().unwrap();
626        // The unit key entry
627        let unit_entry = obj.entries.iter().find(|e| e.key.is_unit());
628        assert!(
629            unit_entry.is_some(),
630            "should have unit key entry, got: {:?}",
631            obj.entries
632                .iter()
633                .map(|e| format!("key={:?}", e.key))
634                .collect::<Vec<_>>()
635        );
636        assert_eq!(
637            unit_entry.unwrap().value.as_str(),
638            Some("server.schema.styx")
639        );
640    }
641
642    #[test]
643    fn test_tag() {
644        let value = parse("type @user");
645        let obj = value.as_object().unwrap();
646        assert_eq!(obj.get("type").and_then(|v| v.tag_name()), Some("user"));
647    }
648
649    #[test]
650    fn test_tag_with_object_payload() {
651        let value = parse("result @err{message \"failed\"}");
652        let obj = value.as_object().unwrap();
653        let result = obj.get("result").unwrap();
654        assert_eq!(result.tag_name(), Some("err"));
655        // Check payload is an object with message field
656        let payload_obj = result.as_object().expect("payload should be object");
657        assert_eq!(
658            payload_obj.get("message").and_then(|v| v.as_str()),
659            Some("failed")
660        );
661    }
662
663    #[test]
664    fn test_tag_with_sequence_payload() {
665        let value = parse("color @rgb(255 128 0)");
666        let obj = value.as_object().unwrap();
667        let color = obj.get("color").unwrap();
668        assert_eq!(color.tag_name(), Some("rgb"));
669        // Check payload is a sequence
670        let payload_seq = color.as_sequence().expect("payload should be sequence");
671        assert_eq!(payload_seq.len(), 3);
672        assert_eq!(payload_seq.get(0).and_then(|v| v.as_str()), Some("255"));
673        assert_eq!(payload_seq.get(1).and_then(|v| v.as_str()), Some("128"));
674        assert_eq!(payload_seq.get(2).and_then(|v| v.as_str()), Some("0"));
675    }
676
677    #[test]
678    fn test_schema_structure_with_space_is_error() {
679        // @ @object { ... } with space before brace is now an error (3 atoms)
680        let source = r#"schema {
681  @ @object {
682    name @string
683  }
684}"#;
685
686        // This should produce a parse error (TooManyAtoms)
687        let result = try_parse(source);
688        assert!(
689            result.is_err(),
690            "@ @object {{ }} with space should be a parse error"
691        );
692        match result {
693            Err(BuildError::Parse(styx_parse::ParseErrorKind::TooManyAtoms, _)) => {}
694            Err(e) => panic!("expected TooManyAtoms error, got {:?}", e),
695            Ok(_) => panic!("expected error, got Ok"),
696        }
697    }
698
699    #[test]
700    fn test_schema_structure_no_space() {
701        // @ @object{ ... } without space before brace
702        let source = r#"schema {
703  @ @object{
704    name @string
705  }
706}"#;
707
708        // Debug: print all events
709        struct EventPrinter;
710        impl<'src> styx_parse::ParseCallback<'src> for EventPrinter {
711            fn event(&mut self, event: styx_parse::Event<'src>) -> bool {
712                eprintln!("Event: {:?}", event);
713                true
714            }
715        }
716
717        eprintln!("=== Events for no-space version ===");
718        let parser = styx_parse::Parser::new(source);
719        parser.parse(&mut EventPrinter);
720
721        let value = parse(source);
722        let obj = value.as_object().unwrap();
723        eprintln!(
724            "Root entries: {:?}",
725            obj.entries
726                .iter()
727                .map(|e| e.key.as_str())
728                .collect::<Vec<_>>()
729        );
730        assert!(obj.get("schema").is_some(), "should have schema entry");
731        let schema = obj.get("schema").unwrap();
732        assert!(
733            schema.as_object().is_some(),
734            "schema should be an object, got tag={:?} payload={:?}",
735            schema.tag,
736            schema.payload.is_some()
737        );
738    }
739
740    #[test]
741    fn test_multiline_doc_comment() {
742        let source = r#"/// First line of doc
743/// Second line of doc
744name @string"#;
745        let value = parse(source);
746        let obj = value.as_object().unwrap();
747        let entry = obj.entries.iter().find(|e| e.key.as_str() == Some("name"));
748        assert!(entry.is_some(), "should have 'name' entry");
749        let entry = entry.unwrap();
750        assert_eq!(
751            entry.doc_comment,
752            Some("First line of doc\nSecond line of doc".to_string()),
753            "doc comment should contain both lines joined by newline"
754        );
755    }
756
757    #[test]
758    fn test_single_line_doc_comment() {
759        let source = r#"/// Just one line
760value 42"#;
761        let value = parse(source);
762        let obj = value.as_object().unwrap();
763        let entry = obj.entries.iter().find(|e| e.key.as_str() == Some("value"));
764        assert!(entry.is_some(), "should have 'value' entry");
765        let entry = entry.unwrap();
766        assert_eq!(entry.doc_comment, Some("Just one line".to_string()),);
767    }
768
769    #[test]
770    fn test_multiline_doc_comment_in_object() {
771        // Test doc comments inside a braced object
772        let source = r#"schema {
773    /// First line
774    /// Second line
775    /// Third line
776    field @string
777}"#;
778        let value = parse(source);
779        let obj = value.as_object().unwrap();
780        let schema = obj.get("schema").expect("should have schema");
781        let schema_obj = schema.as_object().expect("schema should be an object");
782        let entry = schema_obj
783            .entries
784            .iter()
785            .find(|e| e.key.as_str() == Some("field"));
786        assert!(entry.is_some(), "should have 'field' entry");
787        let entry = entry.unwrap();
788        assert_eq!(
789            entry.doc_comment,
790            Some("First line\nSecond line\nThird line".to_string()),
791            "doc comment should contain all lines joined by newline"
792        );
793    }
794}