dialogue_rs/script/
command.rs

1//! # Commands
2//!
3//! Commands are written in ALL-CAPS-KEBAB-CASE and delimited by pipes. Commands can have 'prefixes' and
4//! 'suffixes'. Several built-in commands are supported, and _(in most cases)_ it's easy to extend the language with custom
5//! commands.
6
7use crate::script::parser::{Parser, Rule};
8use anyhow::bail;
9use pest::iterators::Pair;
10use pest::Parser as PestParser;
11use std::borrow::Cow;
12use std::fmt;
13
14/// A command in a script.
15#[derive(Debug, PartialEq, Eq, Clone)]
16pub struct Command {
17    name: Cow<'static, str>,
18    prefix: Option<Cow<'static, str>>,
19    suffix: Option<Cow<'static, str>>,
20}
21
22impl fmt::Display for Command {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        if let Some(prefix) = &self.prefix {
25            write!(f, "{prefix} ")?;
26        }
27
28        write!(f, "|{}|", self.name)?;
29
30        if let Some(suffix) = &self.suffix {
31            write!(f, " {suffix}")?;
32        }
33
34        Ok(())
35    }
36}
37
38impl Command {
39    /// Create a new [Command].
40    pub fn new<T: Into<Cow<'static, str>>>(name: T, prefix: Option<T>, suffix: Option<T>) -> Self {
41        Self {
42            name: name.into(),
43            prefix: prefix.map(Into::into),
44            suffix: suffix.map(Into::into),
45        }
46    }
47
48    /// Create a new [Command] from a string.
49    pub fn parse(command_str: &str) -> Result<Self, anyhow::Error> {
50        let mut pairs = Parser::parse(Rule::Command, command_str)?;
51        let pair = pairs.next().expect("a pair exists");
52        assert_eq!(pairs.next(), None);
53
54        pair.try_into()
55    }
56}
57
58impl TryFrom<Pair<'_, Rule>> for Command {
59    type Error = anyhow::Error;
60
61    fn try_from(pair: Pair<'_, Rule>) -> Result<Self, Self::Error> {
62        match pair.as_rule() {
63            Rule::Command => {
64                let inner_pairs = pair.into_inner();
65                let mut prefix = None;
66                let mut command_name = None;
67                let mut suffix = None;
68
69                for pair in inner_pairs {
70                    match pair.as_rule() {
71                        Rule::CommandName => {
72                            command_name = Some(
73                                pair.as_str()
74                                    .trim_start_matches('|')
75                                    .trim_end_matches('|')
76                                    .to_owned(),
77                            );
78                        }
79                        Rule::Prefix => {
80                            if pair.as_str().trim().is_empty() {
81                                continue;
82                            }
83
84                            prefix = Some(pair.as_str().trim().to_owned());
85                        }
86                        Rule::Text => {
87                            suffix = Some(pair.as_str().trim().to_owned());
88                        }
89                        _ => unreachable!("hit unexpected pair: {pair}"),
90                    }
91                }
92
93                let command_name = command_name.expect("all commands have a name");
94
95                Ok(Self::new(command_name, prefix, suffix))
96            }
97            _ => bail!("Pair is not a command: {:#?}", pair),
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::Command;
105    use pretty_assertions::assert_eq;
106
107    #[test]
108    fn test_infix_command_parse() {
109        let command_str = "ZELDA |SAY| \"Hello, world!\"";
110        let actual = Command::parse(command_str).expect("command is valid");
111        let expected = Command::new("SAY", Some("ZELDA"), Some("\"Hello, world!\""));
112        assert_eq!(expected, actual);
113    }
114
115    #[test]
116    fn test_postfix_command_parse() {
117        let command_str = "|CHOICE| Do the thing";
118        let expected = Command::new("CHOICE".to_owned(), None, Some("Do the thing".to_owned()));
119        let actual = Command::parse(command_str).expect("command is valid");
120        assert_eq!(expected, actual);
121    }
122
123    #[test]
124    fn test_round_trip() {
125        let input = "ZELDA |SAY| \"Hello, world!\"";
126        let command = Command::parse(input).expect("command is valid");
127        let output = command.to_string();
128        assert_eq!(input, output);
129    }
130}