krabby_cli/
script.rs

1use serde::{Deserialize, Serialize};
2use std::fmt::{self, Display};
3
4#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
5#[serde(transparent)]
6pub struct Script {
7    pub command: Command,
8    #[serde(skip)]
9    pub name: ScriptName,
10}
11
12impl Script {
13    pub fn new(name: ScriptName, command: Command) -> Self {
14        Self { name, command }
15    }
16    pub fn echo(&self) -> String {
17        println!("{}", self.command);
18        format!("{}", self.command)
19    }
20    pub fn set_name(&mut self, name: ScriptName) {
21        self.name = name;
22    }
23}
24
25#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
26#[serde(transparent)]
27pub struct ScriptName(String);
28impl ScriptName {
29    pub fn parse(s: String) -> Self {
30        let is_empty = s.trim().is_empty();
31
32        let is_too_long = s.len() > 20;
33
34        let forbidden_characters = ['/', '(', ')', '"', '\'', '<', '>', '\\', '{', '}'];
35        let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));
36
37        if is_empty || is_too_long || contains_forbidden_characters {
38            panic!("{} is not a valid script name", s);
39        }
40        Self(s)
41    }
42}
43impl Display for ScriptName {
44    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45        write!(f, "{}", self.0)
46    }
47}
48
49impl Display for Script {
50    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51        write!(f, "{}", self.command)
52    }
53}
54
55#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
56pub struct Command(String);
57impl Command {
58    pub fn parse(s: String) -> Self {
59        let is_empty = s.trim().is_empty();
60
61        if is_empty {
62            return Self(r#"echo 'Krabby says hi!'"#.into());
63        }
64        Self(s)
65    }
66}
67
68impl Display for Command {
69    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70        write!(f, "{}", self.0)
71    }
72}
73
74impl Default for ScriptName {
75    fn default() -> Self {
76        Self::parse("hello".into())
77    }
78}
79
80#[cfg(test)]
81mod test {
82    use std::panic;
83
84    use super::*;
85
86    #[test]
87    #[should_panic]
88    fn script_name_cannot_be_empty() {
89        ScriptName::parse("".into());
90    }
91
92    #[test]
93    #[should_panic]
94    fn script_name_cannot_be_blank() {
95        ScriptName::parse("  ".into());
96    }
97
98    #[test]
99    #[should_panic]
100    fn script_name_cannot_be_over_20_characters() {
101        ScriptName::parse("iamlongerthan20characters".into());
102    }
103
104    #[test]
105    fn script_name_cannot_contain_forbidden_characters() {
106        let cases = [
107            ("i/am/not/allowed".to_string(), "cannot contain slashes"),
108            ("i(cantbeallowed".to_string(), "cannot contain parenthesis"),
109            ("neither)shouldi".to_string(), "cannot contain parenthesis"),
110            ("i\\shouldpanic".to_string(), "cannot contain backslashes"),
111            (
112                "why\"notallowed".to_string(),
113                "cannot contain double quotes",
114            ),
115            ("shouldnot'be".to_string(), "cannot contain single quotes"),
116            ("<antthisbegood".to_string(), "cannot contain lt sign"),
117            ("cantthisbegoo>".to_string(), "cannot contain gt sign"),
118            ("cantcauseof{".to_string(), "cannot contain bracket"),
119            ("cantcauseof}".to_string(), "cannot contain bracket"),
120        ];
121        for (case, msg) in cases {
122            let result = panic::catch_unwind(|| ScriptName::parse(case.clone()));
123            assert!(
124                result.is_err(),
125                "{} should be a valid script name: {}",
126                case,
127                msg,
128            );
129        }
130    }
131
132    #[test]
133    fn script_command_defaults_to_hi() {
134        let script_cmd = Command::parse("".into());
135        assert_eq!(script_cmd, Command(r#"echo 'Krabby says hi!'"#.into()))
136    }
137
138    #[test]
139    fn valid_inputs_creates_valid_script() {
140        let script = Script::new(
141            ScriptName::parse("hello".into()),
142            Command::parse("valid".into()),
143        );
144
145        assert_eq!(
146            script,
147            Script {
148                name: ScriptName("hello".into()),
149                command: Command("valid".into())
150            }
151        )
152    }
153}