monzo/client.rs
1//! Monzo API clients
2
3use std::future::Future;
4
5use serde::{de::DeserializeOwned, Deserialize};
6use tracing::instrument;
7
8use crate::{
9 endpoints::{accounts, balance, feed_items, pots, transactions, who_am_i, Endpoint},
10 Result,
11};
12
13pub mod inner;
14
15/// A generic trait of any HTTP client which also stores and manages an access
16/// token.
17pub trait Inner: Send + Sync + std::fmt::Debug {
18 /// Construct end send an HTTP request using the provided Endpoint with
19 /// bearer token authentication.
20 fn execute<E>(
21 &self,
22 endpoint: &E,
23 ) -> impl Future<Output = reqwest::Result<reqwest::Response>> + Send
24 where
25 E: Endpoint;
26
27 /// Return a reference to the current access token
28 fn access_token(&self) -> &String;
29
30 /// Manually set the access token
31 fn set_access_token(&mut self, access_token: String);
32
33 /// The base URL of the API
34 fn url(&self) -> &str;
35
36 #[instrument(skip(self, endpoint), fields(url = self.url(), endpoint = endpoint.endpoint()))]
37 fn handle_request<E, R>(&self, endpoint: &E) -> impl Future<Output = Result<R>> + Send
38 where
39 R: DeserializeOwned,
40 E: Endpoint,
41 {
42 async {
43 tracing::info!("sending request");
44 let response = self.execute(endpoint).await?;
45 tracing::info!("response received");
46
47 let result = handle_response(response).await;
48
49 match &result {
50 Ok(_) => {
51 tracing::info!("request successful");
52 }
53 Err(e) => {
54 tracing::info!("request failed: {}", e);
55 }
56 }
57 result
58 }
59 }
60}
61
62/// A Monzo API client
63#[derive(Debug)]
64pub struct Client<C>
65where
66 C: Inner,
67{
68 inner_client: C,
69}
70
71impl<C> Client<C>
72where
73 C: Inner,
74{
75 /// Return a reference to the current access token
76 #[must_use]
77 pub fn access_token(&self) -> &String {
78 self.inner_client.access_token()
79 }
80
81 /// Manually update the access token
82 pub fn set_access_token(&mut self, access_token: impl Into<String>) {
83 self.inner_client.set_access_token(access_token.into());
84 }
85
86 /// Return a list of accounts
87 ///
88 /// # Example
89 /// ```no_run
90 /// # use monzo::{Client};
91 /// #
92 /// # #[tokio::main]
93 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
94 /// #
95 /// # let ACCESS_TOKEN = "ACCESS TOKEN";
96 /// # let client = Client::new(ACCESS_TOKEN);
97 /// #
98 /// let accounts = client.accounts().await?;
99 /// #
100 /// # Ok(())
101 /// # }
102 pub async fn accounts(&self) -> Result<Vec<accounts::Account>> {
103 #[derive(Deserialize)]
104 pub struct Response {
105 accounts: Vec<accounts::Account>,
106 }
107 let response: Response = self.inner_client.handle_request(&accounts::List).await?;
108
109 Ok(response.accounts)
110 }
111
112 /// Return the balance of a given account
113 ///
114 /// # Example
115 /// ```no_run
116 /// # use monzo::{Client};
117 /// #
118 /// # #[tokio::main]
119 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
120 /// #
121 /// # let ACCESS_TOKEN = "ACCESS TOKEN";
122 /// # let ACCOUNT_ID = "ACCOUNT_ID";
123 /// # let client = Client::new(ACCESS_TOKEN);
124 /// #
125 /// let balance = client.balance(ACCOUNT_ID).await?;
126 /// #
127 /// # Ok(())
128 /// # }
129 /// ```
130 pub async fn balance(&self, account_id: &str) -> Result<balance::Balance> {
131 self.inner_client
132 .handle_request(&balance::Get::new(account_id))
133 .await
134 }
135
136 /// Return a list of Pots
137 ///
138 /// # Example
139 /// ```no_run
140 /// # use monzo::{Client};
141 /// #
142 /// # #[tokio::main]
143 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
144 /// #
145 /// # let ACCESS_TOKEN = "ACCESS TOKEN";
146 /// # let ACCOUNT_ID = "ACCOUNT_ID";
147 /// #
148 /// # let client = Client::new(ACCESS_TOKEN);
149 /// #
150 /// let pots = client.pots(ACCOUNT_ID).await?;
151 /// #
152 /// # Ok(())
153 /// # }
154 /// ```
155 pub async fn pots(&self, account_id: &str) -> Result<Vec<pots::Pot>> {
156 #[derive(Deserialize)]
157 struct Response {
158 pots: Vec<pots::Pot>,
159 }
160
161 let response: Response = self
162 .inner_client
163 .handle_request(&pots::List::new(account_id))
164 .await?;
165
166 Ok(response.pots)
167 }
168
169 /// Post a basic item on the account feed.
170 ///
171 /// # Example
172 /// ```no_run
173 /// # use monzo::Client;
174 /// # #[tokio::main]
175 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
176 /// # let access_token = "ACCESS_TOKEN";
177 /// # let client = Client::new(access_token);
178 /// #
179 /// let account_id = "ACCOUNT_ID";
180 /// let title = "Feed Item Title";
181 /// let image_url = "http://www.nyan.cat/cats/original.gif";
182 ///
183 /// client
184 /// .basic_feed_item(account_id, title, image_url)
185 /// .body("i figured out how to send messages to monzo from my computer...")
186 /// .send()
187 /// .await?;
188 /// #
189 /// # Ok(())
190 /// # }
191 /// ```
192 pub const fn basic_feed_item<'a>(
193 &'a self,
194 account_id: &'a str,
195 title: &'a str,
196 image_url: &'a str,
197 ) -> feed_items::basic::Request<'a, C> {
198 feed_items::basic::Request::new(&self.inner_client, account_id, title, image_url)
199 }
200
201 /// Deposit money into a pot
202 pub async fn deposit_into_pot(
203 &self,
204 pot_id: &str,
205 source_account_id: &str,
206 amount: u32,
207 ) -> Result<pots::Pot> {
208 self.inner_client
209 .handle_request(&pots::Deposit::new(pot_id, source_account_id, amount))
210 .await
211 }
212
213 /// Withdraw money from a pot
214 pub async fn withdraw_from_pot(
215 &self,
216 pot_id: &str,
217 destination_account_id: &str,
218 amount: u32,
219 ) -> Result<pots::Pot> {
220 self.inner_client
221 .handle_request(&pots::Withdraw::new(pot_id, destination_account_id, amount))
222 .await
223 }
224
225 /// Get a list of transactions
226 ///
227 /// The only required field is the account id, however optional pagination
228 /// parameters can be supplied.
229 ///
230 /// # Example
231 /// ```no_run
232 /// use chrono::{Duration, Utc};
233 /// use monzo::Client;
234 /// # #[tokio::main]
235 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
236 /// # let access_token = "ACCESS_TOKEN";
237 /// # let client = Client::new(access_token);
238 /// #
239 /// let account_id = "ACCOUNT_ID";
240 ///
241 /// let transactions = client
242 /// .transactions(account_id)
243 /// .since(Utc::now() - Duration::days(10))
244 /// .limit(10)
245 /// .send()
246 /// .await?;
247 /// #
248 /// # Ok(())
249 /// # }
250 /// ```
251 ///
252 /// # Note
253 /// *The Monzo API will only return transactions from more than 90 days ago
254 /// in the first 5 minutes after authorising the Client. You can avoid this
255 /// by using the 'since' method.*
256 pub fn transactions<'a>(&'a self, account_id: &'a str) -> transactions::List<'a, C> {
257 transactions::List::new(&self.inner_client, account_id)
258 }
259
260 /// Retrieve a transaction by transaction id
261 ///
262 /// # Example
263 /// ```no_run
264 /// use monzo::Client;
265 /// # #[tokio::main]
266 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
267 /// # let access_token = "ACCESS_TOKEN";
268 /// # let client = Client::new(access_token);
269 /// #
270 /// let transaction_id = "TRANSACTION_ID";
271 ///
272 /// let transactions = client.transaction(transaction_id).send().await?;
273 /// #
274 /// # Ok(())
275 /// # }
276 /// ```
277 ///
278 /// # Note
279 /// *The Monzo API will only return transactions from more than 90 days ago
280 /// in the first 5 minutes after authorising the Client.*
281 pub fn transaction<'a>(&'a self, transaction_id: &'a str) -> transactions::Get<'a, C> {
282 transactions::Get::new(&self.inner_client, transaction_id)
283 }
284
285 /// Return information about the current session
286 pub async fn who_am_i(&self) -> Result<who_am_i::Response> {
287 self.inner_client.handle_request(&who_am_i::Request).await
288 }
289}
290
291async fn handle_response<R>(response: reqwest::Response) -> Result<R>
292where
293 R: DeserializeOwned,
294{
295 let status = response.status();
296
297 if status.is_success() {
298 Ok(serde_json::from_slice(&response.bytes().await?)?)
299 } else {
300 Err(status.into())
301 }
302}