Skip to main content

sim_citizen/
read_construct.rs

1//! Builds the read-construct `Expr` for a versioned text citizen form.
2
3use sim_kernel::{Expr, Symbol};
4
5/// Builds the `citizen/read-construct` [`Expr`] for a `v1` text citizen form.
6///
7/// Text-form citizens (pitch, chord, MIDI, and sound shapes) read-construct
8/// from a single canonical string tagged with the `v1` form version. This is
9/// the shared spelling of that wire form: a `citizen/read-construct` extension
10/// whose payload is the vector `[class, v1, form]`.
11///
12/// The kernel owns `Expr` and `Symbol`; this helper only assembles them, so it
13/// adds no dependency beyond the kernel.
14///
15/// # Examples
16///
17/// ```
18/// # use sim_citizen::text_read_construct_expr;
19/// # use sim_kernel::{Expr, Symbol};
20/// let class = Symbol::qualified("pitch", "Pitch");
21/// let expr = text_read_construct_expr(class.clone(), "C4");
22/// assert_eq!(
23///     expr,
24///     Expr::Extension {
25///         tag: Symbol::qualified("citizen", "read-construct"),
26///         payload: Box::new(Expr::Vector(vec![
27///             Expr::Symbol(class),
28///             Expr::Symbol(Symbol::new("v1")),
29///             Expr::String("C4".to_owned()),
30///         ])),
31///     }
32/// );
33/// ```
34pub fn text_read_construct_expr(class: Symbol, form: impl Into<String>) -> Expr {
35    Expr::Extension {
36        tag: Symbol::qualified("citizen", "read-construct"),
37        payload: Box::new(Expr::Vector(vec![
38            Expr::Symbol(class),
39            Expr::Symbol(Symbol::new("v1")),
40            Expr::String(form.into()),
41        ])),
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::text_read_construct_expr;
48    use sim_kernel::{Expr, Symbol};
49
50    /// Reproduces the wire form the music shape crates hand-roll and asserts the
51    /// helper matches it: class symbol, `v1` tag, string payload, in order.
52    fn shape_crate_read_construct_expr(class: Symbol, form: String) -> Expr {
53        Expr::Extension {
54            tag: Symbol::qualified("citizen", "read-construct"),
55            payload: Box::new(Expr::Vector(vec![
56                Expr::Symbol(class),
57                Expr::Symbol(Symbol::new("v1")),
58                Expr::String(form),
59            ])),
60        }
61    }
62
63    #[test]
64    fn text_read_construct_expr_matches_shape_crate_form() {
65        let class = Symbol::qualified("pitch", "Chord");
66        let expr = text_read_construct_expr(class.clone(), "C4,E4,G4");
67        assert_eq!(
68            expr,
69            shape_crate_read_construct_expr(class, "C4,E4,G4".to_owned())
70        );
71    }
72
73    #[test]
74    fn text_read_construct_expr_has_class_v1_and_string_payload() {
75        let class = Symbol::qualified("pitch", "Pitch");
76        let expr = text_read_construct_expr(class.clone(), "C4");
77        match expr {
78            Expr::Extension { tag, payload } => {
79                assert_eq!(tag, Symbol::qualified("citizen", "read-construct"));
80                match *payload {
81                    Expr::Vector(items) => {
82                        assert_eq!(
83                            items,
84                            vec![
85                                Expr::Symbol(class),
86                                Expr::Symbol(Symbol::new("v1")),
87                                Expr::String("C4".to_owned()),
88                            ]
89                        );
90                    }
91                    other => panic!("payload must be a vector, found {other:?}"),
92                }
93            }
94            other => panic!("must be an extension, found {other:?}"),
95        }
96    }
97}