Skip to main content

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