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}