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}