aa_sms/
client.rs

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/// An asynchronous `Client` to send a [`Message`] with.
13///
14/// To configure a `Client`, use [`Client::builder()`].
15#[derive(serde::Serialize)]
16pub struct Client {
17    username: String,
18    password: String,
19    #[serde(skip_serializing)]
20    reqwest_client: reqwest::Client,
21}
22
23/// Our user agent to use instead of Reqwest's default one.
24static AA_UA: &str = concat!(
25    env!("CARGO_PKG_NAME"),
26    "/",
27    env!("CARGO_PKG_VERSION"),
28);
29
30impl Client {
31    /// Creates a `ClientBuilder` to configure a `Client`.
32    pub fn builder() -> ClientBuilder {
33        ClientBuilder {
34            ..ClientBuilder::default()
35        }
36    }
37
38    /// Sends the `Message` using the configured `Client`.
39    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/// A builder to construct the properties of a [`Client`].
83#[derive(Default)]
84pub struct ClientBuilder {
85    username: Option<String>,
86    password: Option<String>,
87}
88
89impl ClientBuilder {
90    /// Returns a `Client` that uses this `ClientBuilder` configuration.
91    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    /// Sets the `username` to be used to authenticate the `Client`.
100    pub fn username(mut self, username: impl Into<String>) -> Self {
101        self.username = Some(username.into());
102        self
103    }
104
105    /// Sets the `password` to be used to authenticate the `Client`.
106    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}