styx_tree/
lib.rs

1#![doc = include_str!("../README.md")]
2//! Document tree representation for Styx configuration files.
3//!
4//! This crate provides a high-level API for working with Styx documents,
5//! including parsing, accessing values by path, and serialization.
6
7mod builder;
8mod diagnostic;
9mod value;
10
11pub use builder::{BuildError, TreeBuilder};
12pub use diagnostic::ParseError;
13pub use styx_parse::{ParseErrorKind, ScalarKind, Separator, Span};
14pub use value::{Entry, Object, Payload, Scalar, Sequence, Tag, Value};
15
16/// Parse a Styx document into a tree.
17pub fn parse(source: &str) -> Result<Value, BuildError> {
18    let parser = styx_parse::Parser::new(source);
19    let mut builder = TreeBuilder::new();
20    parser.parse(&mut builder);
21    builder.finish()
22}
23
24/// A Styx document (root is always an implicit object).
25#[derive(Debug, Clone, PartialEq)]
26pub struct Document {
27    /// The root object.
28    pub root: Object,
29    /// Leading doc comments (before first entry).
30    pub leading_comments: Vec<String>,
31}
32
33impl Document {
34    /// Parse a Styx document.
35    pub fn parse(source: &str) -> Result<Self, BuildError> {
36        let value = parse(source)?;
37        match value.payload {
38            Some(Payload::Object(root)) => Ok(Document {
39                root,
40                leading_comments: Vec::new(),
41            }),
42            _ => Err(BuildError::UnexpectedEvent(
43                "expected object at root".to_string(),
44            )),
45        }
46    }
47
48    /// Get a value by path.
49    pub fn get(&self, path: &str) -> Option<&Value> {
50        if path.is_empty() {
51            return None;
52        }
53
54        let (segment, rest) = split_path(path);
55        let value = self.root.get(segment)?;
56        if rest.is_empty() {
57            Some(value)
58        } else {
59            value.get(rest)
60        }
61    }
62}
63
64fn split_path(path: &str) -> (&str, &str) {
65    if path.starts_with('[')
66        && let Some(end) = path.find(']')
67    {
68        let segment = &path[..=end];
69        let rest = &path[end + 1..];
70        let rest = rest.strip_prefix('.').unwrap_or(rest);
71        return (segment, rest);
72    }
73
74    let dot_pos = path.find('.');
75    let bracket_pos = path.find('[');
76
77    match (dot_pos, bracket_pos) {
78        (Some(d), Some(b)) if b < d => (&path[..b], &path[b..]),
79        (Some(d), _) => (&path[..d], &path[d + 1..]),
80        (None, Some(b)) => (&path[..b], &path[b..]),
81        (None, None) => (path, ""),
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_parse_simple() {
91        let doc = Document::parse("name Alice\nage 30").unwrap();
92        assert_eq!(doc.get("name").and_then(|v| v.as_str()), Some("Alice"));
93        assert_eq!(doc.get("age").and_then(|v| v.as_str()), Some("30"));
94    }
95
96    #[test]
97    fn test_parse_empty() {
98        let doc = Document::parse("").unwrap();
99        assert!(doc.root.is_empty());
100    }
101
102    #[test]
103    fn test_convenience_parse() {
104        let value = parse("greeting hello").unwrap();
105        assert_eq!(
106            value.get("greeting").and_then(|v| v.as_str()),
107            Some("hello")
108        );
109    }
110
111    #[test]
112    fn test_schema_tree_structure() {
113        // Parse a schema-like document to understand the tree structure
114        // Structure:
115        //   schema {
116        //     @ @object{         // @ is unit key, @object{...} is the value (tag with object payload)
117        //       name @string
118        //     }
119        //   }
120        let source = r#"schema {
121  @ @object{
122    name @string
123  }
124}"#;
125        let value = parse(source).unwrap();
126
127        // Root is an object with one entry: "schema"
128        let obj = value.as_object().expect("root should be object");
129        assert_eq!(obj.len(), 1);
130
131        // "schema" value is an object
132        let schema = obj.get("schema").expect("should have schema key");
133        let schema_obj = schema.as_object().expect("schema should be object");
134
135        // schema has one entry with a unit key
136        assert_eq!(schema_obj.len(), 1);
137        let entry = &schema_obj.entries[0];
138
139        // Key is unit (@ as a key means unit key)
140        assert!(
141            entry.key.is_unit(),
142            "key should be unit, got {:?}",
143            entry.key
144        );
145
146        // Value is @object{...} - a tagged value with tag "object" and object payload
147        assert_eq!(
148            entry.value.tag_name(),
149            Some("object"),
150            "value should have tag 'object'"
151        );
152
153        // The payload of @object{...} is the inner object { name @string }
154        let payload = entry
155            .value
156            .payload
157            .as_ref()
158            .expect("@object should have payload");
159        let payload_obj = match payload {
160            value::Payload::Object(obj) => obj,
161            _ => panic!("payload should be object, got {:?}", payload),
162        };
163        assert_eq!(payload_obj.len(), 1);
164
165        // "name" entry
166        let name_entry = &payload_obj.entries[0];
167        assert_eq!(name_entry.key.as_str(), Some("name"));
168
169        // Value is tagged with "string", no payload
170        assert_eq!(
171            name_entry.value.tag_name(),
172            Some("string"),
173            "@string should have tag 'string'"
174        );
175        assert!(
176            name_entry.value.payload.is_none(),
177            "@string should have no payload"
178        );
179    }
180}