Skip to main content

sim_codec/implementation/
domain_form.rs

1//! A generic `#(...)` domain-form parser and formatter.
2//!
3//! Several domain codecs (`music-shapes`, `sound-shapes`, ...) hand-rolled the
4//! same `#(Name key=value value [list,...])` reader. This is that grammar, with
5//! no domain knowledge:
6//!
7//! ```text
8//! form  = "#(" name item* ")"
9//! item  = key "=" value | value
10//! value = form | "[" value ("," value)* "]" | string | atom
11//! ```
12//!
13//! Output is ASCII. A domain crate parses with [`parse_domain_form`], reads
14//! fields with [`DomainForm::atom`]/[`string`](DomainForm::string)/
15//! [`list`](DomainForm::list), and (optionally) renders with
16//! [`format_domain_form`].
17
18/// A parsed domain-form value.
19#[derive(Clone, Debug, PartialEq)]
20pub enum DomainValue {
21    /// A nested `#(...)` form.
22    Form(DomainForm),
23    /// A `[...]` list.
24    List(Vec<DomainValue>),
25    /// A `"..."` string.
26    String(String),
27    /// A bare atom (number, identifier, `4/4`, ...).
28    Atom(String),
29}
30
31/// A parsed `#(name ...)` form: a name, keyed fields, and positional values.
32#[derive(Clone, Debug, PartialEq)]
33pub struct DomainForm {
34    /// The form name.
35    pub name: String,
36    /// Keyed `key=value` fields, in order.
37    pub fields: Vec<(String, DomainValue)>,
38    /// Positional (un-keyed) values, in order.
39    pub positional: Vec<DomainValue>,
40}
41
42impl DomainForm {
43    /// The value of keyed field `key`, if present.
44    pub fn field(&self, key: &str) -> Option<&DomainValue> {
45        self.fields
46            .iter()
47            .find_map(|(name, value)| (name == key).then_some(value))
48    }
49
50    /// The atom string of keyed field `key`.
51    pub fn atom(&self, key: &str) -> Result<&str, DomainFormError> {
52        match self.field(key) {
53            Some(DomainValue::Atom(value)) => Ok(value),
54            Some(_) => Err(DomainFormError::WrongFieldKind(key.to_owned())),
55            None => Err(DomainFormError::MissingField(key.to_owned())),
56        }
57    }
58
59    /// The string of keyed field `key`.
60    pub fn string(&self, key: &str) -> Result<&str, DomainFormError> {
61        match self.field(key) {
62            Some(DomainValue::String(value)) => Ok(value),
63            Some(_) => Err(DomainFormError::WrongFieldKind(key.to_owned())),
64            None => Err(DomainFormError::MissingField(key.to_owned())),
65        }
66    }
67
68    /// The list items of keyed field `key`.
69    pub fn list(&self, key: &str) -> Result<&[DomainValue], DomainFormError> {
70        match self.field(key) {
71            Some(DomainValue::List(items)) => Ok(items),
72            Some(_) => Err(DomainFormError::WrongFieldKind(key.to_owned())),
73            None => Err(DomainFormError::MissingField(key.to_owned())),
74        }
75    }
76
77    /// The nested form of keyed field `key`.
78    pub fn form(&self, key: &str) -> Result<&DomainForm, DomainFormError> {
79        match self.field(key) {
80            Some(DomainValue::Form(value)) => Ok(value),
81            Some(_) => Err(DomainFormError::WrongFieldKind(key.to_owned())),
82            None => Err(DomainFormError::MissingField(key.to_owned())),
83        }
84    }
85
86    /// The atom or string text of keyed field `key`.
87    ///
88    /// Accepts either a bare atom or a quoted string, returning the underlying
89    /// text in both cases (the reverse of the `#(...)` writer, which renders a
90    /// symbol as an atom and a string with quotes).
91    pub fn field_atom_or_string(&self, name: &str) -> Result<&str, DomainFormError> {
92        match self.field(name) {
93            Some(DomainValue::Atom(value) | DomainValue::String(value)) => Ok(value),
94            Some(_) => Err(DomainFormError::WrongFieldKind(name.to_owned())),
95            None => Err(DomainFormError::MissingField(name.to_owned())),
96        }
97    }
98
99    /// The rendered [`render_text`](DomainValue::render_text) form of keyed
100    /// field `name`.
101    ///
102    /// Errors with [`MissingField`](DomainFormError::MissingField) when the
103    /// field is absent; any present value renders (an atom to its text, a
104    /// string to its quoted form, a list or nested form to its `#(...)`/`[...]`
105    /// text).
106    pub fn field_text(&self, name: &str) -> Result<String, DomainFormError> {
107        match self.field(name) {
108            Some(value) => Ok(value.render_text()),
109            None => Err(DomainFormError::MissingField(name.to_owned())),
110        }
111    }
112}
113
114impl DomainValue {
115    /// The nested form, if this value is a `#(...)` form.
116    ///
117    /// Errors with [`WrongValueKind`](DomainFormError::WrongValueKind) for a
118    /// list, string, or atom.
119    pub fn as_form(&self) -> Result<&DomainForm, DomainFormError> {
120        match self {
121            DomainValue::Form(form) => Ok(form),
122            _ => Err(DomainFormError::WrongValueKind),
123        }
124    }
125
126    /// The text of this value when it is a bare atom or a quoted string.
127    ///
128    /// Errors with [`WrongValueKind`](DomainFormError::WrongValueKind) for a
129    /// list or nested form.
130    pub fn atom_or_string(&self) -> Result<&str, DomainFormError> {
131        match self {
132            DomainValue::Atom(value) | DomainValue::String(value) => Ok(value),
133            _ => Err(DomainFormError::WrongValueKind),
134        }
135    }
136
137    /// Renders this value back to its ASCII `#(...)` source text: an atom to
138    /// its literal text, a string to its escaped quoted form, a list to
139    /// `[a,b,...]`, and a nested form via [`format_domain_form`].
140    ///
141    /// Round-trips through [`parse_domain_form`] for a top-level form value.
142    pub fn render_text(&self) -> String {
143        match self {
144            DomainValue::Form(form) => format_domain_form(form),
145            DomainValue::List(items) => format!(
146                "[{}]",
147                items
148                    .iter()
149                    .map(DomainValue::render_text)
150                    .collect::<Vec<_>>()
151                    .join(",")
152            ),
153            DomainValue::String(value) => {
154                format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
155            }
156            DomainValue::Atom(value) => value.clone(),
157        }
158    }
159}
160
161/// A domain-form parse or access error.
162#[derive(Clone, Debug, PartialEq, Eq)]
163pub enum DomainFormError {
164    /// Input did not start with `#(`.
165    ExpectedForm,
166    /// Input ended mid-form.
167    UnexpectedEof,
168    /// An invalid character was found where a token was expected.
169    InvalidToken,
170    /// A form repeated a field key.
171    DuplicateField(String),
172    /// Extra input followed the top-level form.
173    TrailingInput,
174    /// A required field was missing.
175    MissingField(String),
176    /// A field had the wrong value kind.
177    WrongFieldKind(String),
178    /// A value had the wrong kind for a keyless accessor (not the atom,
179    /// string, or form that was expected).
180    WrongValueKind,
181}
182
183/// Parse a top-level `#(...)` domain form.
184///
185/// # Examples
186///
187/// ```
188/// use sim_codec::{parse_domain_form, DomainValue};
189///
190/// let form = parse_domain_form("#(Note dur=4/4 60 64)").unwrap();
191/// assert_eq!(form.name, "Note");
192/// assert_eq!(form.atom("dur").unwrap(), "4/4");
193/// assert_eq!(
194///     form.positional,
195///     vec![DomainValue::Atom("60".into()), DomainValue::Atom("64".into())],
196/// );
197/// ```
198pub fn parse_domain_form(input: &str) -> Result<DomainForm, DomainFormError> {
199    let mut parser = Parser { input, index: 0 };
200    parser.skip_ws();
201    if !parser.consume_str("#(") {
202        return Err(DomainFormError::ExpectedForm);
203    }
204    let form = parser.parse_form_body()?;
205    parser.skip_ws();
206    if parser.index != parser.input.len() {
207        return Err(DomainFormError::TrailingInput);
208    }
209    Ok(form)
210}
211
212/// Render a domain form as an ASCII `#(...)` string. Round-trips through
213/// [`parse_domain_form`].
214///
215/// # Examples
216///
217/// ```
218/// use sim_codec::{format_domain_form, parse_domain_form};
219///
220/// let source = "#(Note dur=4/4 pitches=[60,64])";
221/// let form = parse_domain_form(source).unwrap();
222/// let rendered = format_domain_form(&form);
223/// assert_eq!(parse_domain_form(&rendered).unwrap(), form);
224/// ```
225pub fn format_domain_form(form: &DomainForm) -> String {
226    let mut out = String::from("#(");
227    out.push_str(&form.name);
228    for value in &form.positional {
229        out.push(' ');
230        format_value(value, &mut out);
231    }
232    for (key, value) in &form.fields {
233        out.push(' ');
234        out.push_str(key);
235        out.push('=');
236        format_value(value, &mut out);
237    }
238    out.push(')');
239    out
240}
241
242fn format_value(value: &DomainValue, out: &mut String) {
243    match value {
244        DomainValue::Form(form) => out.push_str(&format_domain_form(form)),
245        DomainValue::List(items) => {
246            out.push('[');
247            for (index, item) in items.iter().enumerate() {
248                if index > 0 {
249                    out.push(',');
250                }
251                format_value(item, out);
252            }
253            out.push(']');
254        }
255        DomainValue::String(text) => {
256            out.push('"');
257            for ch in text.chars() {
258                if ch == '\\' || ch == '"' {
259                    out.push('\\');
260                }
261                out.push(ch);
262            }
263            out.push('"');
264        }
265        DomainValue::Atom(text) => out.push_str(text),
266    }
267}
268
269struct Parser<'a> {
270    input: &'a str,
271    index: usize,
272}
273
274impl Parser<'_> {
275    fn parse_form_body(&mut self) -> Result<DomainForm, DomainFormError> {
276        let name = self.parse_ident()?;
277        let mut fields: Vec<(String, DomainValue)> = Vec::new();
278        let mut positional = Vec::new();
279        loop {
280            self.skip_ws();
281            if self.consume_char(')') {
282                break;
283            }
284            match self.peek_char() {
285                Some('#') | Some('[') | Some('"') => positional.push(self.parse_value()?),
286                _ => {
287                    let atom = self.parse_atom()?;
288                    if self.consume_char('=') {
289                        if fields.iter().any(|(key, _)| key == &atom) {
290                            return Err(DomainFormError::DuplicateField(atom));
291                        }
292                        fields.push((atom, self.parse_value()?));
293                    } else {
294                        positional.push(DomainValue::Atom(atom));
295                    }
296                }
297            }
298        }
299        Ok(DomainForm {
300            name,
301            fields,
302            positional,
303        })
304    }
305
306    fn parse_value(&mut self) -> Result<DomainValue, DomainFormError> {
307        self.skip_ws();
308        if self.consume_str("#(") {
309            return self.parse_form_body().map(DomainValue::Form);
310        }
311        if self.consume_char('[') {
312            return self.parse_list();
313        }
314        if self.peek_char() == Some('"') {
315            return self.parse_string().map(DomainValue::String);
316        }
317        self.parse_atom().map(DomainValue::Atom)
318    }
319
320    fn parse_list(&mut self) -> Result<DomainValue, DomainFormError> {
321        let mut items = Vec::new();
322        loop {
323            self.skip_ws();
324            if self.consume_char(']') {
325                break;
326            }
327            items.push(self.parse_value()?);
328            self.skip_ws();
329            if self.consume_char(',') {
330                continue;
331            }
332            self.expect_char(']')?;
333            break;
334        }
335        Ok(DomainValue::List(items))
336    }
337
338    fn parse_string(&mut self) -> Result<String, DomainFormError> {
339        self.expect_char('"')?;
340        let mut out = String::new();
341        while let Some(ch) = self.next_char() {
342            match ch {
343                '"' => return Ok(out),
344                '\\' => out.push(self.next_char().ok_or(DomainFormError::UnexpectedEof)?),
345                other => out.push(other),
346            }
347        }
348        Err(DomainFormError::UnexpectedEof)
349    }
350
351    fn parse_atom(&mut self) -> Result<String, DomainFormError> {
352        let start = self.index;
353        while let Some(ch) = self.peek_char() {
354            if ch.is_whitespace() || [',', ')', ']', '='].contains(&ch) {
355                break;
356            }
357            self.index += ch.len_utf8();
358        }
359        if self.index == start {
360            return Err(DomainFormError::UnexpectedEof);
361        }
362        Ok(self.input[start..self.index].to_owned())
363    }
364
365    fn parse_ident(&mut self) -> Result<String, DomainFormError> {
366        let atom = self.parse_atom()?;
367        if atom
368            .chars()
369            .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
370        {
371            Ok(atom)
372        } else {
373            Err(DomainFormError::InvalidToken)
374        }
375    }
376
377    fn expect_char(&mut self, expected: char) -> Result<(), DomainFormError> {
378        match self.next_char() {
379            Some(ch) if ch == expected => Ok(()),
380            Some(_) => Err(DomainFormError::InvalidToken),
381            None => Err(DomainFormError::UnexpectedEof),
382        }
383    }
384
385    fn consume_char(&mut self, expected: char) -> bool {
386        if self.peek_char() == Some(expected) {
387            self.index += expected.len_utf8();
388            true
389        } else {
390            false
391        }
392    }
393
394    fn consume_str(&mut self, expected: &str) -> bool {
395        if self.input[self.index..].starts_with(expected) {
396            self.index += expected.len();
397            true
398        } else {
399            false
400        }
401    }
402
403    fn skip_ws(&mut self) {
404        while let Some(ch) = self.peek_char() {
405            if ch.is_whitespace() {
406                self.index += ch.len_utf8();
407            } else {
408                break;
409            }
410        }
411    }
412
413    fn peek_char(&self) -> Option<char> {
414        self.input[self.index..].chars().next()
415    }
416
417    fn next_char(&mut self) -> Option<char> {
418        let ch = self.peek_char()?;
419        self.index += ch.len_utf8();
420        Some(ch)
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn parses_keyed_form_with_list_and_nested_form() {
430        let form = parse_domain_form("#(Note dur=4/4 pitch=C4 tags=[a,b] inner=#(Rest dur=1/4))")
431            .expect("parse");
432        assert_eq!(form.name, "Note");
433        assert_eq!(form.atom("dur").unwrap(), "4/4");
434        assert_eq!(form.atom("pitch").unwrap(), "C4");
435        assert_eq!(
436            form.list("tags").unwrap(),
437            &[
438                DomainValue::Atom("a".to_owned()),
439                DomainValue::Atom("b".to_owned())
440            ]
441        );
442        assert_eq!(form.form("inner").unwrap().name, "Rest");
443    }
444
445    #[test]
446    fn parses_positional_values() {
447        let form = parse_domain_form("#(Chord 60 64 67)").expect("parse");
448        assert_eq!(form.positional.len(), 3);
449        assert!(form.fields.is_empty());
450    }
451
452    #[test]
453    fn round_trips_through_format() {
454        let source = "#(Note dur=4/4 sym=\"a\\\"b\" pitches=[60,64])";
455        let form = parse_domain_form(source).expect("parse");
456        let rendered = format_domain_form(&form);
457        assert_eq!(parse_domain_form(&rendered).unwrap(), form);
458    }
459
460    #[test]
461    fn missing_close_paren_is_unexpected_eof() {
462        assert_eq!(
463            parse_domain_form("#(Note dur=4/4"),
464            Err(DomainFormError::UnexpectedEof)
465        );
466    }
467
468    #[test]
469    fn duplicate_field_is_rejected() {
470        assert_eq!(
471            parse_domain_form("#(Note dur=4/4 dur=1/2)"),
472            Err(DomainFormError::DuplicateField("dur".to_owned()))
473        );
474    }
475
476    #[test]
477    fn trailing_input_is_rejected() {
478        assert_eq!(
479            parse_domain_form("#(Note dur=4/4) extra"),
480            Err(DomainFormError::TrailingInput)
481        );
482    }
483
484    #[test]
485    fn non_form_input_is_rejected() {
486        assert_eq!(
487            parse_domain_form("Note"),
488            Err(DomainFormError::ExpectedForm)
489        );
490    }
491
492    #[test]
493    fn escaped_quotes_round_trip() {
494        let form = parse_domain_form("#(S v=\"he said \\\"hi\\\"\")").expect("parse");
495        assert_eq!(form.string("v").unwrap(), "he said \"hi\"");
496    }
497
498    #[test]
499    fn field_atom_or_string_accepts_both_kinds() {
500        let form = parse_domain_form("#(N a=bare s=\"quoted\")").expect("parse");
501        assert_eq!(form.field_atom_or_string("a").unwrap(), "bare");
502        assert_eq!(form.field_atom_or_string("s").unwrap(), "quoted");
503    }
504
505    #[test]
506    fn field_atom_or_string_rejects_list_and_missing() {
507        let form = parse_domain_form("#(N tags=[a,b])").expect("parse");
508        assert_eq!(
509            form.field_atom_or_string("tags"),
510            Err(DomainFormError::WrongFieldKind("tags".to_owned()))
511        );
512        assert_eq!(
513            form.field_atom_or_string("nope"),
514            Err(DomainFormError::MissingField("nope".to_owned()))
515        );
516    }
517
518    #[test]
519    fn field_text_renders_every_value_kind() {
520        let form = parse_domain_form("#(N a=bare s=\"q\\\"x\" tags=[a,b] inner=#(Rest dur=1/4))")
521            .expect("parse");
522        assert_eq!(form.field_text("a").unwrap(), "bare");
523        assert_eq!(form.field_text("s").unwrap(), "\"q\\\"x\"");
524        assert_eq!(form.field_text("tags").unwrap(), "[a,b]");
525        assert_eq!(form.field_text("inner").unwrap(), "#(Rest dur=1/4)");
526        assert_eq!(
527            form.field_text("nope"),
528            Err(DomainFormError::MissingField("nope".to_owned()))
529        );
530    }
531
532    #[test]
533    fn value_as_form_and_atom_or_string() {
534        let form = parse_domain_form("#(N inner=#(Rest dur=1/4) a=bare s=\"q\")").expect("parse");
535        assert_eq!(form.field("inner").unwrap().as_form().unwrap().name, "Rest");
536        assert_eq!(
537            form.field("a").unwrap().as_form(),
538            Err(DomainFormError::WrongValueKind)
539        );
540        assert_eq!(form.field("a").unwrap().atom_or_string().unwrap(), "bare");
541        assert_eq!(form.field("s").unwrap().atom_or_string().unwrap(), "q");
542        assert_eq!(
543            form.field("inner").unwrap().atom_or_string(),
544            Err(DomainFormError::WrongValueKind)
545        );
546    }
547
548    #[test]
549    fn render_text_round_trips_a_form_value() {
550        let source = "#(Note dur=4/4 sym=\"a\\\"b\" pitches=[60,64])";
551        let form = parse_domain_form(source).expect("parse");
552        let value = DomainValue::Form(form.clone());
553        let rendered = value.render_text();
554        assert_eq!(parse_domain_form(&rendered).unwrap(), form);
555    }
556
557    #[test]
558    fn render_text_formats_list_and_string_and_atom() {
559        assert_eq!(DomainValue::Atom("60".to_owned()).render_text(), "60");
560        assert_eq!(
561            DomainValue::String("a\"b\\c".to_owned()).render_text(),
562            "\"a\\\"b\\\\c\""
563        );
564        let list = DomainValue::List(vec![
565            DomainValue::Atom("a".to_owned()),
566            DomainValue::String("b".to_owned()),
567        ]);
568        assert_eq!(list.render_text(), "[a,\"b\"]");
569    }
570}