styx_tree/
value.rs

1//! Value types for Styx documents.
2//!
3//! In Styx, every value has the same structure:
4//! - An optional tag (`@name`)
5//! - An optional payload (scalar, sequence, or object)
6//!
7//! This means:
8//! - `@` is `Value { tag: None, payload: None }` (unit)
9//! - `foo` is `Value { tag: None, payload: Some(Payload::Scalar(...)) }`
10//! - `@string` is `Value { tag: Some("string"), payload: None }`
11//! - `@seq(a b)` is `Value { tag: Some("seq"), payload: Some(Payload::Sequence(...)) }`
12//! - `@object{...}` is `Value { tag: Some("object"), payload: Some(Payload::Object(...)) }`
13
14use styx_parse::{ScalarKind, Separator, Span};
15
16/// A Styx value: optional tag + optional payload.
17#[derive(Debug, Clone, PartialEq)]
18#[cfg_attr(feature = "facet", derive(facet::Facet))]
19#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
20pub struct Value {
21    /// Optional tag (e.g., `string` for `@string`).
22    pub tag: Option<Tag>,
23    /// Optional payload.
24    pub payload: Option<Payload>,
25    /// Source span (None if programmatically constructed).
26    pub span: Option<Span>,
27}
28
29/// A tag on a value.
30#[derive(Debug, Clone, PartialEq)]
31#[cfg_attr(feature = "facet", derive(facet::Facet))]
32#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
33pub struct Tag {
34    /// Tag name (without `@`).
35    pub name: String,
36    /// Source span.
37    pub span: Option<Span>,
38}
39
40/// The payload of a value.
41#[derive(Debug, Clone, PartialEq)]
42#[cfg_attr(feature = "facet", derive(facet::Facet))]
43#[repr(u8)]
44pub enum Payload {
45    /// Scalar text.
46    Scalar(Scalar),
47    /// Sequence `(a b c)`.
48    Sequence(Sequence),
49    /// Object `{key value, ...}`.
50    Object(Object),
51}
52
53/// A scalar value.
54#[derive(Debug, Clone, PartialEq)]
55#[cfg_attr(feature = "facet", derive(facet::Facet))]
56#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
57pub struct Scalar {
58    /// The text content.
59    pub text: String,
60    /// What kind of scalar syntax was used.
61    pub kind: ScalarKind,
62    /// Source span (None if programmatically constructed).
63    pub span: Option<Span>,
64}
65
66/// A sequence of values.
67#[derive(Debug, Clone, PartialEq)]
68#[cfg_attr(feature = "facet", derive(facet::Facet))]
69#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
70pub struct Sequence {
71    /// Items in the sequence.
72    pub items: Vec<Value>,
73    /// Source span.
74    pub span: Option<Span>,
75}
76
77/// An object (mapping of keys to values).
78#[derive(Debug, Clone, PartialEq)]
79#[cfg_attr(feature = "facet", derive(facet::Facet))]
80#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
81pub struct Object {
82    /// Entries in the object.
83    pub entries: Vec<Entry>,
84    /// Separator style used.
85    pub separator: Separator,
86    /// Source span.
87    pub span: Option<Span>,
88}
89
90/// An entry in an object.
91#[derive(Debug, Clone, PartialEq)]
92#[cfg_attr(feature = "facet", derive(facet::Facet))]
93#[cfg_attr(feature = "facet", facet(skip_all_unless_truthy))]
94pub struct Entry {
95    /// The key.
96    pub key: Value,
97    /// The value.
98    pub value: Value,
99    /// Doc comment attached to this entry.
100    pub doc_comment: Option<String>,
101}
102
103impl Value {
104    /// Create a unit value (`@`).
105    pub fn unit() -> Self {
106        Value {
107            tag: None,
108            payload: None,
109            span: None,
110        }
111    }
112
113    /// Create a scalar value (no tag).
114    pub fn scalar(text: impl Into<String>) -> Self {
115        Value {
116            tag: None,
117            payload: Some(Payload::Scalar(Scalar {
118                text: text.into(),
119                kind: ScalarKind::Bare,
120                span: None,
121            })),
122            span: None,
123        }
124    }
125
126    /// Create a tagged value with no payload (e.g., `@string`).
127    pub fn tag(name: impl Into<String>) -> Self {
128        Value {
129            tag: Some(Tag {
130                name: name.into(),
131                span: None,
132            }),
133            payload: None,
134            span: None,
135        }
136    }
137
138    /// Create a tagged value with a payload.
139    pub fn tagged(name: impl Into<String>, payload: Value) -> Self {
140        Value {
141            tag: Some(Tag {
142                name: name.into(),
143                span: None,
144            }),
145            payload: payload.payload,
146            span: None,
147        }
148    }
149
150    /// Create an empty sequence (no tag).
151    pub fn sequence() -> Self {
152        Value {
153            tag: None,
154            payload: Some(Payload::Sequence(Sequence {
155                items: Vec::new(),
156                span: None,
157            })),
158            span: None,
159        }
160    }
161
162    /// Create a sequence with items (no tag).
163    pub fn seq(items: Vec<Value>) -> Self {
164        Value {
165            tag: None,
166            payload: Some(Payload::Sequence(Sequence { items, span: None })),
167            span: None,
168        }
169    }
170
171    /// Create an empty object (no tag).
172    pub fn object() -> Self {
173        Value {
174            tag: None,
175            payload: Some(Payload::Object(Object {
176                entries: Vec::new(),
177                separator: Separator::Newline,
178                span: None,
179            })),
180            span: None,
181        }
182    }
183
184    /// Check if this is unit (`@` - no tag, no payload).
185    pub fn is_unit(&self) -> bool {
186        self.tag.is_none() && self.payload.is_none()
187    }
188
189    /// Check if this is a `@schema` tag (used for schema declarations).
190    pub fn is_schema_tag(&self) -> bool {
191        self.tag_name() == Some("schema")
192    }
193
194    /// Get the tag name if present.
195    pub fn tag_name(&self) -> Option<&str> {
196        self.tag.as_ref().map(|t| t.name.as_str())
197    }
198
199    /// Get as string (for untagged scalars).
200    pub fn as_str(&self) -> Option<&str> {
201        if self.tag.is_some() {
202            return None;
203        }
204        match &self.payload {
205            Some(Payload::Scalar(s)) => Some(&s.text),
206            _ => None,
207        }
208    }
209
210    /// Get the scalar text regardless of tag.
211    pub fn scalar_text(&self) -> Option<&str> {
212        match &self.payload {
213            Some(Payload::Scalar(s)) => Some(&s.text),
214            _ => None,
215        }
216    }
217
218    /// Get as object (payload only).
219    pub fn as_object(&self) -> Option<&Object> {
220        match &self.payload {
221            Some(Payload::Object(o)) => Some(o),
222            _ => None,
223        }
224    }
225
226    /// Get as mutable object (payload only).
227    pub fn as_object_mut(&mut self) -> Option<&mut Object> {
228        match &mut self.payload {
229            Some(Payload::Object(o)) => Some(o),
230            _ => None,
231        }
232    }
233
234    /// Get as sequence (payload only).
235    pub fn as_sequence(&self) -> Option<&Sequence> {
236        match &self.payload {
237            Some(Payload::Sequence(s)) => Some(s),
238            _ => None,
239        }
240    }
241
242    /// Get as mutable sequence (payload only).
243    pub fn as_sequence_mut(&mut self) -> Option<&mut Sequence> {
244        match &mut self.payload {
245            Some(Payload::Sequence(s)) => Some(s),
246            _ => None,
247        }
248    }
249
250    /// Add a tag to this value.
251    pub fn with_tag(mut self, name: impl Into<String>) -> Self {
252        self.tag = Some(Tag {
253            name: name.into(),
254            span: None,
255        });
256        self
257    }
258
259    /// Get a value by path.
260    ///
261    /// Path segments are separated by `.`.
262    /// Use `[n]` for sequence indexing.
263    pub fn get(&self, path: &str) -> Option<&Value> {
264        if path.is_empty() {
265            return Some(self);
266        }
267
268        let (segment, rest) = split_path(path);
269
270        match &self.payload {
271            Some(Payload::Object(obj)) => {
272                let value = obj.get(segment)?;
273                if rest.is_empty() {
274                    Some(value)
275                } else {
276                    value.get(rest)
277                }
278            }
279            Some(Payload::Sequence(seq)) => {
280                // Handle [n] indexing
281                if segment.starts_with('[') && segment.ends_with(']') {
282                    let idx: usize = segment[1..segment.len() - 1].parse().ok()?;
283                    let value = seq.get(idx)?;
284                    if rest.is_empty() {
285                        Some(value)
286                    } else {
287                        value.get(rest)
288                    }
289                } else {
290                    None
291                }
292            }
293            _ => None,
294        }
295    }
296
297    /// Get a mutable value by path.
298    pub fn get_mut(&mut self, path: &str) -> Option<&mut Value> {
299        if path.is_empty() {
300            return Some(self);
301        }
302
303        let (segment, rest) = split_path(path);
304
305        match &mut self.payload {
306            Some(Payload::Object(obj)) => {
307                let value = obj.get_mut(segment)?;
308                if rest.is_empty() {
309                    Some(value)
310                } else {
311                    value.get_mut(rest)
312                }
313            }
314            Some(Payload::Sequence(seq)) => {
315                if segment.starts_with('[') && segment.ends_with(']') {
316                    let idx: usize = segment[1..segment.len() - 1].parse().ok()?;
317                    let value = seq.get_mut(idx)?;
318                    if rest.is_empty() {
319                        Some(value)
320                    } else {
321                        value.get_mut(rest)
322                    }
323                } else {
324                    None
325                }
326            }
327            _ => None,
328        }
329    }
330}
331
332impl Object {
333    /// Get entry value by key (for untagged scalar keys).
334    pub fn get(&self, key: &str) -> Option<&Value> {
335        self.entries
336            .iter()
337            .find(|e| e.key.as_str() == Some(key))
338            .map(|e| &e.value)
339    }
340
341    /// Get mutable entry value by key.
342    pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
343        self.entries
344            .iter_mut()
345            .find(|e| e.key.as_str() == Some(key))
346            .map(|e| &mut e.value)
347    }
348
349    /// Get entry by unit key (`@`).
350    pub fn get_unit(&self) -> Option<&Value> {
351        self.entries
352            .iter()
353            .find(|e| e.key.is_unit())
354            .map(|e| &e.value)
355    }
356
357    /// Get mutable entry by unit key.
358    pub fn get_unit_mut(&mut self) -> Option<&mut Value> {
359        self.entries
360            .iter_mut()
361            .find(|e| e.key.is_unit())
362            .map(|e| &mut e.value)
363    }
364
365    /// Iterate over entries as (key, value) pairs.
366    pub fn iter(&self) -> impl Iterator<Item = (&Value, &Value)> {
367        self.entries.iter().map(|e| (&e.key, &e.value))
368    }
369
370    /// Check if key exists.
371    pub fn contains_key(&self, key: &str) -> bool {
372        self.entries.iter().any(|e| e.key.as_str() == Some(key))
373    }
374
375    /// Check if unit key exists.
376    pub fn contains_unit_key(&self) -> bool {
377        self.entries.iter().any(|e| e.key.is_unit())
378    }
379
380    /// Number of entries.
381    pub fn len(&self) -> usize {
382        self.entries.len()
383    }
384
385    /// Check if empty.
386    pub fn is_empty(&self) -> bool {
387        self.entries.is_empty()
388    }
389
390    /// Insert or update an entry with a string key.
391    pub fn insert(&mut self, key: impl Into<String>, value: Value) {
392        let key_str = key.into();
393        if let Some(entry) = self
394            .entries
395            .iter_mut()
396            .find(|e| e.key.as_str() == Some(&key_str))
397        {
398            entry.value = value;
399        } else {
400            self.entries.push(Entry {
401                key: Value::scalar(key_str),
402                value,
403                doc_comment: None,
404            });
405        }
406    }
407
408    /// Insert or update an entry with a unit key.
409    pub fn insert_unit(&mut self, value: Value) {
410        if let Some(entry) = self.entries.iter_mut().find(|e| e.key.is_unit()) {
411            entry.value = value;
412        } else {
413            self.entries.push(Entry {
414                key: Value::unit(),
415                value,
416                doc_comment: None,
417            });
418        }
419    }
420}
421
422impl Sequence {
423    /// Get item by index.
424    pub fn get(&self, index: usize) -> Option<&Value> {
425        self.items.get(index)
426    }
427
428    /// Get mutable item by index.
429    pub fn get_mut(&mut self, index: usize) -> Option<&mut Value> {
430        self.items.get_mut(index)
431    }
432
433    /// Number of items.
434    pub fn len(&self) -> usize {
435        self.items.len()
436    }
437
438    /// Check if empty.
439    pub fn is_empty(&self) -> bool {
440        self.items.is_empty()
441    }
442
443    /// Iterate over items.
444    pub fn iter(&self) -> impl Iterator<Item = &Value> {
445        self.items.iter()
446    }
447
448    /// Push an item.
449    pub fn push(&mut self, value: Value) {
450        self.items.push(value);
451    }
452}
453
454/// Split path at first `.` or `[`.
455fn split_path(path: &str) -> (&str, &str) {
456    // Handle [n] at start
457    if path.starts_with('[')
458        && let Some(end) = path.find(']')
459    {
460        let segment = &path[..=end];
461        let rest = &path[end + 1..];
462        // Skip leading `.` in rest
463        let rest = rest.strip_prefix('.').unwrap_or(rest);
464        return (segment, rest);
465    }
466
467    // Find first `.` or `[`
468    let dot_pos = path.find('.');
469    let bracket_pos = path.find('[');
470
471    match (dot_pos, bracket_pos) {
472        (Some(d), Some(b)) if b < d => (&path[..b], &path[b..]),
473        (Some(d), _) => (&path[..d], &path[d + 1..]),
474        (None, Some(b)) => (&path[..b], &path[b..]),
475        (None, None) => (path, ""),
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    #[test]
484    fn test_split_path() {
485        assert_eq!(split_path("foo"), ("foo", ""));
486        assert_eq!(split_path("foo.bar"), ("foo", "bar"));
487        assert_eq!(split_path("foo.bar.baz"), ("foo", "bar.baz"));
488        assert_eq!(split_path("[0]"), ("[0]", ""));
489        assert_eq!(split_path("[0].foo"), ("[0]", "foo"));
490        assert_eq!(split_path("foo[0]"), ("foo", "[0]"));
491        assert_eq!(split_path("foo[0].bar"), ("foo", "[0].bar"));
492    }
493
494    #[test]
495    fn test_unit_value() {
496        let v = Value::unit();
497        assert!(v.is_unit());
498        assert!(v.tag.is_none());
499        assert!(v.payload.is_none());
500    }
501
502    #[test]
503    fn test_scalar_value() {
504        let v = Value::scalar("hello");
505        assert!(!v.is_unit());
506        assert!(v.tag.is_none());
507        assert_eq!(v.as_str(), Some("hello"));
508    }
509
510    #[test]
511    fn test_tagged_value() {
512        let v = Value::tag("string");
513        assert!(!v.is_unit());
514        assert_eq!(v.tag_name(), Some("string"));
515        assert!(v.payload.is_none());
516    }
517
518    #[test]
519    fn test_object_get() {
520        let mut obj = Object {
521            entries: vec![Entry {
522                key: Value::scalar("name"),
523                value: Value::scalar("Alice"),
524                doc_comment: None,
525            }],
526            separator: Separator::Newline,
527            span: None,
528        };
529
530        assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Alice"));
531        assert_eq!(obj.get("missing"), None);
532
533        obj.insert("age", Value::scalar("30"));
534        assert_eq!(obj.get("age").and_then(|v| v.as_str()), Some("30"));
535    }
536
537    #[test]
538    fn test_object_unit_key() {
539        let mut obj = Object {
540            entries: vec![],
541            separator: Separator::Newline,
542            span: None,
543        };
544
545        obj.insert_unit(Value::scalar("root"));
546        assert!(obj.contains_unit_key());
547        assert_eq!(obj.get_unit().and_then(|v| v.as_str()), Some("root"));
548    }
549
550    #[test]
551    fn test_value_path_access() {
552        let value = Value {
553            tag: None,
554            payload: Some(Payload::Object(Object {
555                entries: vec![
556                    Entry {
557                        key: Value::scalar("user"),
558                        value: Value {
559                            tag: None,
560                            payload: Some(Payload::Object(Object {
561                                entries: vec![Entry {
562                                    key: Value::scalar("name"),
563                                    value: Value::scalar("Alice"),
564                                    doc_comment: None,
565                                }],
566                                separator: Separator::Newline,
567                                span: None,
568                            })),
569                            span: None,
570                        },
571                        doc_comment: None,
572                    },
573                    Entry {
574                        key: Value::scalar("items"),
575                        value: Value {
576                            tag: None,
577                            payload: Some(Payload::Sequence(Sequence {
578                                items: vec![
579                                    Value::scalar("a"),
580                                    Value::scalar("b"),
581                                    Value::scalar("c"),
582                                ],
583                                span: None,
584                            })),
585                            span: None,
586                        },
587                        doc_comment: None,
588                    },
589                ],
590                separator: Separator::Newline,
591                span: None,
592            })),
593            span: None,
594        };
595
596        assert_eq!(
597            value.get("user.name").and_then(|v| v.as_str()),
598            Some("Alice")
599        );
600        assert_eq!(value.get("items[0]").and_then(|v| v.as_str()), Some("a"));
601        assert_eq!(value.get("items[2]").and_then(|v| v.as_str()), Some("c"));
602        assert_eq!(value.get("missing"), None);
603    }
604
605    /// Test that Value can roundtrip through JSON via Facet.
606    #[test]
607    fn test_value_json_roundtrip() {
608        // Build a complicated Value
609        let value = Value {
610            tag: None,
611            payload: Some(Payload::Object(Object {
612                entries: vec![
613                    // Schema declaration
614                    Entry {
615                        key: Value::tag("schema"),
616                        value: Value::scalar("my-schema.styx"),
617                        doc_comment: Some("Schema for this config".to_string()),
618                    },
619                    // Simple scalar
620                    Entry {
621                        key: Value::scalar("name"),
622                        value: Value::scalar("my-app"),
623                        doc_comment: None,
624                    },
625                    // Tagged value
626                    Entry {
627                        key: Value::scalar("port"),
628                        value: Value::tagged("int", Value::scalar("8080")),
629                        doc_comment: None,
630                    },
631                    // Nested object
632                    Entry {
633                        key: Value::scalar("server"),
634                        value: Value {
635                            tag: None,
636                            payload: Some(Payload::Object(Object {
637                                entries: vec![
638                                    Entry {
639                                        key: Value::scalar("host"),
640                                        value: Value::scalar("localhost"),
641                                        doc_comment: None,
642                                    },
643                                    Entry {
644                                        key: Value::scalar("tls"),
645                                        value: Value {
646                                            tag: Some(Tag {
647                                                name: "object".to_string(),
648                                                span: None,
649                                            }),
650                                            payload: Some(Payload::Object(Object {
651                                                entries: vec![
652                                                    Entry {
653                                                        key: Value::scalar("cert"),
654                                                        value: Value::scalar("/path/to/cert.pem"),
655                                                        doc_comment: None,
656                                                    },
657                                                    Entry {
658                                                        key: Value::scalar("key"),
659                                                        value: Value::scalar("/path/to/key.pem"),
660                                                        doc_comment: None,
661                                                    },
662                                                ],
663                                                separator: Separator::Comma,
664                                                span: None,
665                                            })),
666                                            span: None,
667                                        },
668                                        doc_comment: Some("TLS configuration".to_string()),
669                                    },
670                                ],
671                                separator: Separator::Newline,
672                                span: None,
673                            })),
674                            span: None,
675                        },
676                        doc_comment: Some("Server settings".to_string()),
677                    },
678                    // Sequence
679                    Entry {
680                        key: Value::scalar("tags"),
681                        value: Value {
682                            tag: None,
683                            payload: Some(Payload::Sequence(Sequence {
684                                items: vec![
685                                    Value::scalar("production"),
686                                    Value::scalar("web"),
687                                    Value::tagged("important", Value::unit()),
688                                ],
689                                span: None,
690                            })),
691                            span: None,
692                        },
693                        doc_comment: None,
694                    },
695                    // Unit value
696                    Entry {
697                        key: Value::scalar("debug"),
698                        value: Value::unit(),
699                        doc_comment: None,
700                    },
701                ],
702                separator: Separator::Newline,
703                span: Some(Span::new(0, 100)),
704            })),
705            span: Some(Span::new(0, 100)),
706        };
707
708        // Serialize to JSON
709        let json = facet_json::to_string(&value).expect("should serialize");
710        eprintln!("JSON representation:\n{json}");
711
712        // Deserialize back
713        let roundtripped: Value = facet_json::from_str(&json).expect("should deserialize");
714
715        // Verify equality
716        assert_eq!(value, roundtripped, "Value should survive JSON roundtrip");
717    }
718
719    #[test]
720    fn test_value_postcard_roundtrip() {
721        // Simple scalar
722        let v = Value::scalar("hello");
723        let bytes = facet_postcard::to_vec(&v).expect("serialize scalar");
724        let v2: Value = facet_postcard::from_slice(&bytes).expect("deserialize scalar");
725        assert_eq!(v, v2);
726
727        // Tagged value
728        let v = Value::tag("string");
729        let bytes = facet_postcard::to_vec(&v).expect("serialize tagged");
730        let v2: Value = facet_postcard::from_slice(&bytes).expect("deserialize tagged");
731        assert_eq!(v, v2);
732
733        // Nested object (recursive structure)
734        let v = Value {
735            tag: None,
736            payload: Some(Payload::Object(Object {
737                entries: vec![
738                    Entry {
739                        key: Value::scalar("name"),
740                        value: Value::scalar("Alice"),
741                        doc_comment: None,
742                    },
743                    Entry {
744                        key: Value::scalar("nested"),
745                        value: Value {
746                            tag: None,
747                            payload: Some(Payload::Object(Object {
748                                entries: vec![Entry {
749                                    key: Value::scalar("inner"),
750                                    value: Value::scalar("value"),
751                                    doc_comment: None,
752                                }],
753                                separator: Separator::Newline,
754                                span: None,
755                            })),
756                            span: None,
757                        },
758                        doc_comment: Some("A nested object".to_string()),
759                    },
760                ],
761                separator: Separator::Newline,
762                span: None,
763            })),
764            span: None,
765        };
766        let bytes = facet_postcard::to_vec(&v).expect("serialize nested");
767        let v2: Value = facet_postcard::from_slice(&bytes).expect("deserialize nested");
768        assert_eq!(v, v2);
769
770        // Sequence with values
771        let v = Value::seq(vec![
772            Value::scalar("a"),
773            Value::scalar("b"),
774            Value::tagged("important", Value::unit()),
775        ]);
776        let bytes = facet_postcard::to_vec(&v).expect("serialize sequence");
777        let v2: Value = facet_postcard::from_slice(&bytes).expect("deserialize sequence");
778        assert_eq!(v, v2);
779    }
780}