1use std::borrow::Cow;
4
5use styx_parse::{Event, ParseErrorKind, 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 span: Span,
61 pending_doc_comment: Option<String>,
62 },
63 Sequence {
64 items: Vec<Value>,
65 span: Span,
66 },
67 Tag {
68 name: String,
69 span: Span,
70 },
71 Entry {
72 key: Option<Value>,
73 doc_comment: Option<String>,
74 },
75}
76
77impl TreeBuilder {
78 pub fn new() -> Self {
80 Self {
81 stack: Vec::new(),
82 root_entries: Vec::new(),
83 pending_doc_comment: None,
84 errors: Vec::new(),
85 }
86 }
87
88 pub fn finish(self) -> Result<Value, BuildError> {
90 if let Some((kind, span)) = self.errors.into_iter().next() {
92 return Err(BuildError::Parse(kind, span));
93 }
94
95 if !self.stack.is_empty() {
96 return Err(BuildError::UnclosedStructure);
97 }
98
99 Ok(Value {
101 tag: None,
102 payload: Some(Payload::Object(Object {
103 entries: self.root_entries,
104 span: None,
105 })),
106 span: None,
107 })
108 }
109
110 fn push_value(&mut self, value: Value) {
112 if let Some(BuilderFrame::Tag { .. }) = self.stack.last() {
114 if let Some(BuilderFrame::Tag { name, span }) = self.stack.pop() {
116 let tagged = Value {
118 tag: Some(Tag {
119 name,
120 span: Some(span),
121 }),
122 payload: value.payload,
123 span: value.span,
124 };
125 self.push_value(tagged);
127 }
128 return;
129 }
130
131 if let Some(BuilderFrame::Entry { key: Some(_), .. }) = self.stack.last() {
133 if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.pop() {
135 let key_val = key.unwrap();
136 match self.stack.last_mut() {
137 Some(BuilderFrame::Object { entries, .. }) => {
138 entries.push(Entry {
139 key: key_val,
140 value,
141 doc_comment,
142 });
143 }
144 _ => {
145 self.root_entries.push(Entry {
146 key: key_val,
147 value,
148 doc_comment,
149 });
150 }
151 }
152 self.stack.push(BuilderFrame::Entry {
154 key: None,
155 doc_comment: None,
156 });
157 }
158 return;
159 }
160
161 match self.stack.last_mut() {
162 Some(BuilderFrame::Object {
163 entries,
164 pending_doc_comment,
165 ..
166 }) => {
167 entries.push(Entry {
169 key: Value::unit(),
170 value,
171 doc_comment: pending_doc_comment.take(),
172 });
173 }
174 Some(BuilderFrame::Sequence { items, .. }) => {
175 items.push(value);
176 }
177 Some(BuilderFrame::Tag { .. }) => {
178 unreachable!()
180 }
181 Some(BuilderFrame::Entry { key, .. }) => {
182 if key.is_none() {
183 *key = Some(value);
185 }
186 }
187 None => {
188 self.root_entries.push(Entry {
190 key: Value::unit(),
191 value,
192 doc_comment: self.pending_doc_comment.take(),
193 });
194 }
195 }
196 }
197}
198
199impl Default for TreeBuilder {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205impl TreeBuilder {
206 pub fn event(&mut self, event: Event<'_>) {
208 let span = event.span;
209 match event.kind {
210 styx_parse::EventKind::DocumentStart | styx_parse::EventKind::DocumentEnd => {
211 }
213
214 styx_parse::EventKind::ObjectStart => {
215 self.stack.push(BuilderFrame::Object {
216 entries: Vec::new(),
217 span,
218 pending_doc_comment: None,
219 });
220 }
221
222 styx_parse::EventKind::ObjectEnd => {
223 if let Some(BuilderFrame::Object {
224 entries,
225 span: start_span,
226 ..
227 }) = self.stack.pop()
228 {
229 if self.stack.is_empty() {
231 self.root_entries = entries;
232 } else {
233 let obj = Value {
234 tag: None,
235 payload: Some(Payload::Object(Object {
236 entries,
237 span: Some(Span {
238 start: start_span.start,
239 end: span.end,
240 }),
241 })),
242 span: Some(Span {
243 start: start_span.start,
244 end: span.end,
245 }),
246 };
247 self.push_value(obj);
248 }
249 }
250 }
251
252 styx_parse::EventKind::SequenceStart => {
253 self.stack.push(BuilderFrame::Sequence {
254 items: Vec::new(),
255 span,
256 });
257 }
258
259 styx_parse::EventKind::SequenceEnd => {
260 if let Some(BuilderFrame::Sequence {
261 items,
262 span: start_span,
263 }) = self.stack.pop()
264 {
265 let seq = Value {
266 tag: None,
267 payload: Some(Payload::Sequence(Sequence {
268 items,
269 span: Some(Span {
270 start: start_span.start,
271 end: span.end,
272 }),
273 })),
274 span: Some(Span {
275 start: start_span.start,
276 end: span.end,
277 }),
278 };
279 self.push_value(seq);
280 }
281 }
282
283 styx_parse::EventKind::EntryStart => {
284 let doc_comment = match self.stack.last_mut() {
285 Some(BuilderFrame::Object {
286 pending_doc_comment,
287 ..
288 }) => pending_doc_comment.take(),
289 _ => self.pending_doc_comment.take(),
290 };
291 self.stack.push(BuilderFrame::Entry {
292 key: None,
293 doc_comment,
294 });
295 }
296
297 styx_parse::EventKind::EntryEnd => {
298 if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.pop()
299 && let Some(key) = key
300 {
301 match self.stack.last_mut() {
303 Some(BuilderFrame::Object { entries, .. }) => {
304 if let Some(last) = entries.last_mut()
306 && last.key.is_unit()
307 && last.doc_comment.is_none()
308 {
309 last.key = key;
310 last.doc_comment = doc_comment;
311 return;
312 }
313 entries.push(Entry {
315 key,
316 value: Value::unit(),
317 doc_comment,
318 });
319 }
320 _ => {
321 if let Some(last) = self.root_entries.last_mut()
323 && last.key.is_unit()
324 && last.doc_comment.is_none()
325 {
326 last.key = key;
327 last.doc_comment = doc_comment;
328 return;
329 }
330 self.root_entries.push(Entry {
331 key,
332 value: Value::unit(),
333 doc_comment,
334 });
335 }
336 }
337 }
338 }
339
340 styx_parse::EventKind::Key { tag, payload, kind } => {
341 let key_value = Value {
342 tag: tag.map(|name| Tag {
343 name: name.to_string(),
344 span: Some(span),
345 }),
346 payload: payload.map(|text| {
347 Payload::Scalar(Scalar {
348 text: cow_to_string(text),
349 kind,
350 span: Some(span),
351 })
352 }),
353 span: Some(span),
354 };
355 if let Some(BuilderFrame::Entry { key, .. }) = self.stack.last_mut() {
356 *key = Some(key_value);
357 }
358 }
359
360 styx_parse::EventKind::Scalar { value, kind } => {
361 let scalar = Value {
362 tag: None,
363 payload: Some(Payload::Scalar(Scalar {
364 text: cow_to_string(value),
365 kind,
366 span: Some(span),
367 })),
368 span: Some(span),
369 };
370
371 if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
373 && key.is_some()
374 {
375 let key_val = key.take().unwrap();
377 let doc = doc_comment.take();
378
379 self.stack.pop();
381
382 match self.stack.last_mut() {
384 Some(BuilderFrame::Object { entries, .. }) => {
385 entries.push(Entry {
386 key: key_val,
387 value: scalar,
388 doc_comment: doc,
389 });
390 }
391 _ => {
392 self.root_entries.push(Entry {
393 key: key_val,
394 value: scalar,
395 doc_comment: doc,
396 });
397 }
398 }
399 self.stack.push(BuilderFrame::Entry {
401 key: None,
402 doc_comment: None,
403 });
404 return;
405 }
406
407 self.push_value(scalar);
408 }
409
410 styx_parse::EventKind::Unit => {
411 let unit = Value {
412 tag: None,
413 payload: None,
414 span: Some(span),
415 };
416
417 if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
419 && key.is_some()
420 {
421 let key_val = key.take().unwrap();
422 let doc = doc_comment.take();
423 self.stack.pop();
424
425 match self.stack.last_mut() {
426 Some(BuilderFrame::Object { entries, .. }) => {
427 entries.push(Entry {
428 key: key_val,
429 value: unit,
430 doc_comment: doc,
431 });
432 }
433 _ => {
434 self.root_entries.push(Entry {
435 key: key_val,
436 value: unit,
437 doc_comment: doc,
438 });
439 }
440 }
441 self.stack.push(BuilderFrame::Entry {
442 key: None,
443 doc_comment: None,
444 });
445 return;
446 }
447
448 self.push_value(unit);
449 }
450
451 styx_parse::EventKind::TagStart { name } => {
452 self.stack.push(BuilderFrame::Tag {
453 name: name.to_string(),
454 span,
455 });
456 }
457
458 styx_parse::EventKind::TagEnd => {
459 if !matches!(self.stack.last(), Some(BuilderFrame::Tag { .. })) {
462 return;
463 }
464 if let Some(BuilderFrame::Tag { name, span }) = self.stack.pop() {
465 let tagged = Value {
467 tag: Some(Tag {
468 name,
469 span: Some(span),
470 }),
471 payload: None,
472 span: Some(span),
473 };
474
475 if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
477 && key.is_some()
478 {
479 let key_val = key.take().unwrap();
480 let doc = doc_comment.take();
481 self.stack.pop();
482
483 match self.stack.last_mut() {
484 Some(BuilderFrame::Object { entries, .. }) => {
485 entries.push(Entry {
486 key: key_val,
487 value: tagged,
488 doc_comment: doc,
489 });
490 }
491 _ => {
492 self.root_entries.push(Entry {
493 key: key_val,
494 value: tagged,
495 doc_comment: doc,
496 });
497 }
498 }
499 self.stack.push(BuilderFrame::Entry {
500 key: None,
501 doc_comment: None,
502 });
503 return;
504 }
505
506 self.push_value(tagged);
507 }
508 }
509
510 styx_parse::EventKind::DocComment { lines } => {
511 let comment = lines.join("\n");
513 match self.stack.last_mut() {
514 Some(BuilderFrame::Object {
515 pending_doc_comment,
516 ..
517 }) => {
518 append_doc_comment(pending_doc_comment, comment);
519 }
520 _ => {
521 append_doc_comment(&mut self.pending_doc_comment, comment);
522 }
523 }
524 }
525
526 styx_parse::EventKind::Comment { .. } => {
527 }
529
530 styx_parse::EventKind::Error { kind } => {
531 self.errors.push((kind, span));
532 }
533 }
534 }
535}
536
537fn cow_to_string(cow: Cow<'_, str>) -> String {
538 cow.into_owned()
539}
540
541fn append_doc_comment(target: &mut Option<String>, line: String) {
543 match target {
544 Some(existing) => {
545 existing.push('\n');
546 existing.push_str(&line);
547 }
548 None => {
549 *target = Some(line);
550 }
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use styx_parse::Parser;
557
558 use super::*;
559
560 fn parse(source: &str) -> Value {
561 let mut parser = Parser::new(source);
562 let mut builder = TreeBuilder::new();
563 while let Some(event) = parser.next_event() {
564 eprintln!("Event: {:?}", event);
565 builder.event(event);
566 }
567 builder.finish().unwrap()
568 }
569
570 fn try_parse(source: &str) -> Result<Value, BuildError> {
571 let mut parser = Parser::new(source);
572 let mut builder = TreeBuilder::new();
573 while let Some(event) = parser.next_event() {
574 builder.event(event);
575 }
576 builder.finish()
577 }
578
579 #[test]
580 fn test_empty_document() {
581 let value = parse("");
582 assert!(value.as_object().unwrap().is_empty());
583 }
584
585 #[test]
586 fn test_simple_entry() {
587 let value = parse("name Alice");
588 let obj = value.as_object().unwrap();
589 assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Alice"));
590 }
591
592 #[test]
593 fn test_multiple_entries() {
594 let value = parse("name Alice\nage 30");
595 let obj = value.as_object().unwrap();
596 assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Alice"));
597 assert_eq!(obj.get("age").and_then(|v| v.as_str()), Some("30"));
598 }
599
600 #[test]
601 fn test_path_access() {
602 let value = parse("name Alice\nage 30");
603 eprintln!("Built value: {value:#?}");
604 assert_eq!(value.get("name").and_then(|v| v.as_str()), Some("Alice"));
605 assert_eq!(value.get("age").and_then(|v| v.as_str()), Some("30"));
606 }
607
608 #[test]
609 fn test_unit_value() {
610 let value = parse("enabled @");
611 let obj = value.as_object().unwrap();
612 assert!(obj.get("enabled").unwrap().is_unit());
613 }
614
615 #[test]
616 fn test_unit_key() {
617 let value = parse("@ server.schema.styx");
619 let obj = value.as_object().unwrap();
620 let unit_entry = obj.entries.iter().find(|e| e.key.is_unit());
622 assert!(
623 unit_entry.is_some(),
624 "should have unit key entry, got: {:?}",
625 obj.entries
626 .iter()
627 .map(|e| format!("key={:?}", e.key))
628 .collect::<Vec<_>>()
629 );
630 assert_eq!(
631 unit_entry.unwrap().value.as_str(),
632 Some("server.schema.styx")
633 );
634 }
635
636 #[test]
637 fn test_tag() {
638 let value = parse("type @user");
639 let obj = value.as_object().unwrap();
640 assert_eq!(obj.get("type").and_then(|v| v.tag_name()), Some("user"));
641 }
642
643 #[test]
644 fn test_tag_with_object_payload() {
645 let value = parse("result @err{message \"failed\"}");
646 let obj = value.as_object().unwrap();
647 let result = obj.get("result").unwrap();
648 assert_eq!(result.tag_name(), Some("err"));
649 let payload_obj = result.as_object().expect("payload should be object");
651 assert_eq!(
652 payload_obj.get("message").and_then(|v| v.as_str()),
653 Some("failed")
654 );
655 }
656
657 #[test]
658 fn test_tag_with_sequence_payload() {
659 let value = parse("color @rgb(255 128 0)");
660 let obj = value.as_object().unwrap();
661 let color = obj.get("color").unwrap();
662 assert_eq!(color.tag_name(), Some("rgb"));
663 let payload_seq = color.as_sequence().expect("payload should be sequence");
665 assert_eq!(payload_seq.len(), 3);
666 assert_eq!(payload_seq.get(0).and_then(|v| v.as_str()), Some("255"));
667 assert_eq!(payload_seq.get(1).and_then(|v| v.as_str()), Some("128"));
668 assert_eq!(payload_seq.get(2).and_then(|v| v.as_str()), Some("0"));
669 }
670
671 #[test]
672 fn test_schema_structure_with_space_is_error() {
673 let source = r#"schema {
675 @ @object {
676 name @string
677 }
678}"#;
679
680 let result = try_parse(source);
682 assert!(
683 result.is_err(),
684 "@ @object {{ }} with space should be a parse error"
685 );
686 match result {
687 Err(BuildError::Parse(styx_parse::ParseErrorKind::TooManyAtoms, _)) => {}
688 Err(e) => panic!("expected TooManyAtoms error, got {:?}", e),
689 Ok(_) => panic!("expected error, got Ok"),
690 }
691 }
692
693 #[test]
694 fn test_schema_structure_no_space() {
695 let source = r#"schema {
697 @ @object{
698 name @string
699 }
700}"#;
701
702 eprintln!("=== Events for no-space version ===");
704 let mut debug_parser = Parser::new(source);
705 while let Some(event) = debug_parser.next_event() {
706 eprintln!("Event: {:?}", event);
707 }
708
709 let value = parse(source);
710 let obj = value.as_object().unwrap();
711 eprintln!(
712 "Root entries: {:?}",
713 obj.entries
714 .iter()
715 .map(|e| e.key.as_str())
716 .collect::<Vec<_>>()
717 );
718 assert!(obj.get("schema").is_some(), "should have schema entry");
719 let schema = obj.get("schema").unwrap();
720 assert!(
721 schema.as_object().is_some(),
722 "schema should be an object, got tag={:?} payload={:?}",
723 schema.tag,
724 schema.payload.is_some()
725 );
726 }
727
728 #[test]
729 fn test_multiline_doc_comment() {
730 let source = r#"/// First line of doc
731/// Second line of doc
732name @string"#;
733 let value = parse(source);
734 let obj = value.as_object().unwrap();
735 let entry = obj.entries.iter().find(|e| e.key.as_str() == Some("name"));
736 assert!(entry.is_some(), "should have 'name' entry");
737 let entry = entry.unwrap();
738 assert_eq!(
739 entry.doc_comment,
740 Some("First line of doc\nSecond line of doc".to_string()),
741 "doc comment should contain both lines joined by newline"
742 );
743 }
744
745 #[test]
746 fn test_single_line_doc_comment() {
747 let source = r#"/// Just one line
748value 42"#;
749 let value = parse(source);
750 let obj = value.as_object().unwrap();
751 let entry = obj.entries.iter().find(|e| e.key.as_str() == Some("value"));
752 assert!(entry.is_some(), "should have 'value' entry");
753 let entry = entry.unwrap();
754 assert_eq!(entry.doc_comment, Some("Just one line".to_string()),);
755 }
756
757 #[test]
758 fn test_multiline_doc_comment_in_object() {
759 let source = r#"schema {
761 /// First line
762 /// Second line
763 /// Third line
764 field @string
765}"#;
766 let value = parse(source);
767 let obj = value.as_object().unwrap();
768 let schema = obj.get("schema").expect("should have schema");
769 let schema_obj = schema.as_object().expect("schema should be an object");
770 let entry = schema_obj
771 .entries
772 .iter()
773 .find(|e| e.key.as_str() == Some("field"));
774 assert!(entry.is_some(), "should have 'field' entry");
775 let entry = entry.unwrap();
776 assert_eq!(
777 entry.doc_comment,
778 Some("First line\nSecond line\nThird line".to_string()),
779 "doc comment should contain all lines joined by newline"
780 );
781 }
782}