1#[derive(Debug, PartialEq)]
12pub enum Cmd {
13 Join(String),
14 Part(Option<String>),
16 Quit,
17 Send(String),
19 Usage(&'static str),
21}
22
23pub fn parse(input: &str) -> Cmd {
26 let line = input.trim();
27 if let Some(rest) = line.strip_prefix("//") {
30 return Cmd::Send(format!("/{rest}"));
31 }
32 let Some(body) = line.strip_prefix('/') else {
33 return Cmd::Send(line.to_string());
34 };
35 let mut it = body.splitn(2, char::is_whitespace);
36 let verb = it.next().unwrap_or_default().to_lowercase();
37 let arg = it.next().map(str::trim).filter(|s| !s.is_empty());
38 match verb.as_str() {
39 "join" | "j" => match arg {
40 Some(a) => Cmd::Join(a.to_string()),
41 None => Cmd::Usage("usage: /join <channel> (or kick:<channel>)"),
42 },
43 "part" | "leave" | "close" => Cmd::Part(arg.map(str::to_string)),
44 "quit" | "q" | "exit" => Cmd::Quit,
45 _ => Cmd::Send(line.to_string()),
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 #[test]
55 fn join_takes_a_channel() {
56 assert_eq!(parse("/join hot"), Cmd::Join("hot".into()));
57 assert_eq!(parse("/j xqc"), Cmd::Join("xqc".into()));
58 assert_eq!(
59 parse("/join kick:trainwreckstv"),
60 Cmd::Join("kick:trainwreckstv".into())
61 );
62 assert_eq!(parse(" /join hot "), Cmd::Join("hot".into()));
63 }
64
65 #[test]
66 fn join_without_an_argument_explains_itself() {
67 assert!(matches!(parse("/join"), Cmd::Usage(_)));
68 assert!(matches!(parse("/join "), Cmd::Usage(_)));
69 }
70
71 #[test]
72 fn part_and_quit_have_the_usual_aliases() {
73 assert_eq!(parse("/part"), Cmd::Part(None));
74 assert_eq!(parse("/leave"), Cmd::Part(None));
75 assert_eq!(parse("/part xqc"), Cmd::Part(Some("xqc".into())));
76 assert_eq!(parse("/quit"), Cmd::Quit);
77 assert_eq!(parse("/q"), Cmd::Quit);
78 }
79
80 #[test]
81 fn verbs_are_case_insensitive() {
82 assert_eq!(parse("/JOIN hot"), Cmd::Join("hot".into()));
83 assert_eq!(parse("/Quit"), Cmd::Quit);
84 }
85
86 #[test]
89 fn platform_commands_pass_through_untouched() {
90 for line in [
91 "/ban someguy",
92 "/timeout someguy 600 spam",
93 "/me waves",
94 "/vip friend",
95 "/unban someguy",
96 "/mod someguy",
97 "/raid xqc",
98 "/clear",
99 "/slow 30",
100 "/emoteonly",
101 "/followers 10m",
102 "/w someguy hello",
103 "/announce hello chat",
104 ] {
105 assert_eq!(
106 parse(line),
107 Cmd::Send(line.into()),
108 "{line} must reach the platform"
109 );
110 }
111 }
112
113 #[test]
114 fn ordinary_messages_are_untouched() {
115 assert_eq!(parse("hello chat"), Cmd::Send("hello chat".into()));
116 assert_eq!(parse("PagMan"), Cmd::Send("PagMan".into()));
117 assert_eq!(parse("/"), Cmd::Send("/".into()));
119 }
120
121 #[test]
122 fn double_slash_sends_a_literal_slash() {
123 assert_eq!(parse("//join hot"), Cmd::Send("/join hot".into()));
124 assert_eq!(parse("//ban"), Cmd::Send("/ban".into()));
125 }
126}