1use crate::message::Message;
2use std::error::Error;
3
4#[derive(serde::Serialize)]
5struct ApiCall<'a> {
6 #[serde(flatten)]
7 client: &'a Client,
8 #[serde(flatten)]
9 message: &'a Message,
10}
11
12#[derive(serde::Serialize)]
16pub struct Client {
17 username: String,
18 password: String,
19 #[serde(skip_serializing)]
20 reqwest_client: reqwest::Client,
21}
22
23static AA_UA: &str = concat!(
25 env!("CARGO_PKG_NAME"),
26 "/",
27 env!("CARGO_PKG_VERSION"),
28);
29
30impl Client {
31 pub fn builder() -> ClientBuilder {
33 ClientBuilder {
34 ..ClientBuilder::default()
35 }
36 }
37
38 pub async fn send(self, message: Message) -> Result<(), Box<dyn Error>> {
40 let api_call = ApiCall {
41 client: &self,
42 message: &message,
43 };
44 let res = match self
45 .reqwest_client
46 .post("https://sms.aa.net.uk/sms.cgi")
47 .json(&api_call)
48 .send()
49 .await
50 {
51 Ok(res) => res,
52 Err(_) => { Err(Box::<dyn Error>::from(String::from("Invalid HTTP call."))) }?,
53 };
54 let body = match res.text().await {
55 Ok(body) => body,
56 Err(_) => {
57 Err(Box::<dyn Error>::from(String::from(
58 "Invalid HTTP response.",
59 )))
60 }?,
61 };
62
63 let status_and_message = body.split_once(':');
64
65 if status_and_message.is_none() {
66 return Err(Box::<dyn Error>::from(String::from(
67 "Invalid API response.",
68 )));
69 }
70
71 let status = status_and_message.unwrap().0;
72 let message = status_and_message.unwrap().1;
73
74 if status == "ERR" {
75 return Err(Box::<dyn Error>::from(message));
76 }
77
78 Ok(())
79 }
80}
81
82#[derive(Default)]
84pub struct ClientBuilder {
85 username: Option<String>,
86 password: Option<String>,
87}
88
89impl ClientBuilder {
90 pub fn build(self) -> Client {
92 Client {
93 username: self.username.expect("Cannot build sms without `username`."),
94 password: self.password.expect("Cannot build sms without `password`."),
95 reqwest_client: reqwest::Client::builder().user_agent(AA_UA).build().unwrap(),
96 }
97 }
98
99 pub fn username(mut self, username: impl Into<String>) -> Self {
101 self.username = Some(username.into());
102 self
103 }
104
105 pub fn password(mut self, password: impl Into<String>) -> Self {
107 self.password = Some(password.into());
108 self
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn builder_default_all_none() {
118 let cb = Client::builder();
119 assert_eq!(cb.username, None);
120 assert_eq!(cb.password, None);
121 }
122
123 #[test]
124 fn builder_setters_work() {
125 let cb = Client::builder().username("123");
126 assert_eq!(cb.username, Some(String::from("123")));
127 assert_eq!(cb.password, None);
128
129 let cb = Client::builder().password("123");
130 assert_eq!(cb.username, None);
131 assert_eq!(cb.password, Some(String::from("123")));
132 }
133
134 #[test]
135 fn builder_works() {
136 let client = Client::builder().username("123").password("456").build();
137 assert_eq!(client.username, "123");
138 assert_eq!(client.password, "456");
139 }
140
141 #[test]
142 fn builder_order_doesnt_matter() {
143 let client1 = Client::builder().username("123").password("456").build();
144 let client2 = Client::builder().password("456").username("123").build();
145 assert_eq!(client1.username, client2.username);
146 assert_eq!(client1.password, client2.password);
147 }
148
149 #[test]
150 #[should_panic]
151 fn build_without_username_fails() {
152 Client::builder().password("123").build();
153 }
154
155 #[test]
156 #[should_panic]
157 fn build_without_password_fails() {
158 Client::builder().username("123").build();
159 }
160}