1use 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#[derive(Debug, Clone, PartialEq)]
11pub enum BuildError {
12 UnexpectedEvent(String),
14 UnclosedStructure,
16 EmptyDocument,
18 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 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
49pub 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 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 pub fn finish(self) -> Result<Value, BuildError> {
91 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 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 fn push_value(&mut self, value: Value) {
114 if let Some(BuilderFrame::Tag { .. }) = self.stack.last() {
116 if let Some(BuilderFrame::Tag { name, span }) = self.stack.pop() {
118 let tagged = Value {
120 tag: Some(Tag {
121 name,
122 span: Some(span),
123 }),
124 payload: value.payload,
125 span: value.span,
126 };
127 self.push_value(tagged);
129 }
130 return;
131 }
132
133 if let Some(BuilderFrame::Entry { key: Some(_), .. }) = self.stack.last() {
135 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 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 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 unreachable!()
182 }
183 Some(BuilderFrame::Entry { key, .. }) => {
184 if key.is_none() {
185 *key = Some(value);
187 }
188 }
189 None => {
190 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 }
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 match self.stack.last_mut() {
301 Some(BuilderFrame::Object { entries, .. }) => {
302 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 entries.push(Entry {
313 key,
314 value: Value::unit(),
315 doc_comment,
316 });
317 }
318 _ => {
319 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 if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
376 && key.is_some()
377 {
378 let key_val = key.take().unwrap();
380 let doc = doc_comment.take();
381
382 self.stack.pop();
384
385 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 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 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 if !matches!(self.stack.last(), Some(BuilderFrame::Tag { .. })) {
465 return true;
466 }
467 if let Some(BuilderFrame::Tag { name, span }) = self.stack.pop() {
468 let tagged = Value {
470 tag: Some(Tag {
471 name,
472 span: Some(span),
473 }),
474 payload: None,
475 span: Some(span),
476 };
477
478 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 }
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 text.strip_prefix("///")
548 .map(|s| s.strip_prefix(' ').unwrap_or(s))
549 .unwrap_or(text)
550 .to_string()
551}
552
553fn 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 let value = parse("@ server.schema.styx");
625 let obj = value.as_object().unwrap();
626 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 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 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 let source = r#"schema {
681 @ @object {
682 name @string
683 }
684}"#;
685
686 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 let source = r#"schema {
703 @ @object{
704 name @string
705 }
706}"#;
707
708 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 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}