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}