up_api/v1/
categories.rs

1use crate::v1::{Client, error, BASE_URL};
2
3use serde::{Deserialize, Serialize};
4
5// ----------------- Response Objects -----------------
6
7#[derive(Deserialize, Debug)]
8pub struct ListCategoriesResponse {
9    /// The list of categories returned in this response.
10    pub data: Vec<CategoryResource>,
11}
12
13#[derive(Deserialize, Debug)]
14pub struct GetCategoryResponse {
15    /// The category returned in this response.
16    pub data: CategoryResource,
17
18}
19
20#[derive(Deserialize, Debug)]
21pub struct CategoryResource {
22    /// The type of this resource: categories
23    pub r#type: String,
24    /// The unique identifier for this category. This is a human-readable but
25    /// URL-safe value.
26    pub id: String,
27    pub attributes: Attributes,
28    pub relationships: Relationships,
29    pub links: Option<CategoryResourceLinks>,
30}
31
32#[derive(Deserialize, Debug)]
33pub struct Attributes {
34    /// The name of this category as seen in the Up application.
35    pub name: String,
36}
37
38#[derive(Deserialize, Debug)]
39pub struct Relationships {
40    pub parent: Parent,
41    pub children: Children,
42}
43
44#[derive(Deserialize, Debug)]
45pub struct Parent {
46    pub data: Option<ParentData>,
47    pub links: Option<ParentLinks>,
48}
49
50#[derive(Deserialize, Debug)]
51pub struct ParentData {
52    /// The type of this resource: `categories`
53    pub r#type: String,
54    /// The unique identifier of the resource within its type.
55    pub id: String,
56}
57
58#[derive(Deserialize, Debug)]
59pub struct ParentLinks {
60    /// The link to retrieve the related resource(s) in this relationship.
61    pub related: String,
62}
63
64#[derive(Deserialize, Debug)]
65pub struct Children {
66    pub data: Vec<ChildrenData>,
67    pub links: Option<ChildrenLinks>,
68}
69
70#[derive(Deserialize, Debug)]
71pub struct ChildrenData {
72    /// The type of this resource: `categories`
73    pub r#type: String,
74    /// The unique identifier of the resource within its type.
75    pub id: String,
76}
77
78#[derive(Deserialize, Debug)]
79pub struct ChildrenLinks {
80    /// The link to retrieve the related resource(s) in this relationship.
81    pub related: String,
82}
83
84#[derive(Deserialize, Debug)]
85pub struct CategoryResourceLinks {
86    /// The canonical link to this resource within the API.
87    #[serde(rename = "self")]
88    pub this: String,
89}
90
91// ----------------- Input Objects -----------------
92
93#[derive(Default)]
94pub struct ListCategoriesOptions {
95    /// The unique identifier of a parent category for which to return only its
96    ///  children.
97    filter_parent: Option<String>,
98}
99
100impl ListCategoriesOptions {
101    /// Sets the parent filter value.
102    pub fn filter_parent(&mut self, value: String) {
103        self.filter_parent = Some(value);
104    }
105
106    fn add_params(&self, url: &mut reqwest::Url) {
107        let mut query = String::new();
108
109        if let Some(value) = &self.filter_parent {
110            if !query.is_empty() {
111                query.push('&');
112            }
113            query.push_str(
114                &format!("filter[parent]={}", urlencoding::encode(value))
115            );
116        }
117
118        if !query.is_empty() {
119            url.set_query(Some(&query));
120        }
121    }
122}
123
124// ----------------- Request Objects -----------------
125
126#[derive(Serialize)]
127struct CategoriseTransactionRequest {
128    /// The category to set on the transaction. Set this entire key to `null`
129    /// de-categorize a transaction.
130    data: Option<CategoryInputResourceIdentifier>,
131}
132
133#[derive(Serialize)]
134struct CategoryInputResourceIdentifier {
135    /// The type of this resource: `categories`
136    r#type: String,
137    /// The unique identifier of the category, as returned by the
138    /// `list_categories` method.
139    id: String,
140}
141
142impl Client {
143    /// Retrieve a list of all categories and their ancestry. The returned list
144    /// is not paginated.
145    pub async fn list_categories(
146        &self,
147        options: &ListCategoriesOptions,
148    ) -> Result<ListCategoriesResponse, error::Error> {
149        let mut url = reqwest::Url::parse(
150            &format!("{}/categories", BASE_URL)
151        ).map_err(error::Error::UrlParse)?;
152        options.add_params(&mut url);
153
154        let res = reqwest::Client::new()
155            .get(url)
156            .header("Authorization", self.auth_header())
157            .send()
158            .await
159            .map_err(error::Error::Request)?;
160
161        match res.status() {
162            reqwest::StatusCode::OK => {
163                let body = res.text().await.map_err(error::Error::BodyRead)?;
164                let category_response: ListCategoriesResponse =
165                    serde_json::from_str(&body).map_err(error::Error::Json)?;
166
167                Ok(category_response)
168            },
169            _ => {
170                let body = res.text().await.map_err(error::Error::BodyRead)?;
171                let error: error::ErrorResponse =
172                    serde_json::from_str(&body).map_err(error::Error::Json)?;
173
174                Err(error::Error::Api(error))
175            }
176        }
177    }
178
179    /// Retrieve a specific category by providing its unique identifier.
180    pub async fn get_category(
181        &self, id: &str,
182    ) -> Result<GetCategoryResponse, error::Error> {
183        // This assertion is because without an ID the request is thought to be
184        // a request for many categories, and therefore the error messages are
185        // very unclear.
186        if id.is_empty() {
187            panic!("The provided category ID must not be empty.");
188        }
189
190        let url = reqwest::Url::parse(
191            &format!("{}/categories/{}", BASE_URL, id)
192        ).map_err(error::Error::UrlParse)?;
193
194        let res = reqwest::Client::new()
195            .get(url)
196            .header("Authorization", self.auth_header())
197            .send()
198            .await
199            .map_err(error::Error::Request)?;
200
201        match res.status() {
202            reqwest::StatusCode::OK => {
203                let body = res.text().await.map_err(error::Error::BodyRead)?;
204                let category_response: GetCategoryResponse =
205                    serde_json::from_str(&body).map_err(error::Error::Json)?;
206
207                Ok(category_response )
208            },
209            _ => {
210                let body = res.text().await.map_err(error::Error::BodyRead)?;
211                let error: error::ErrorResponse =
212                    serde_json::from_str(&body).map_err(error::Error::Json)?;
213
214                Err(error::Error::Api(error))
215            }
216        }
217    }
218
219    /// Updates the category associated with a transaction. Only transactions
220    /// for which `is_categorizable` is set to true support this operation. The
221    /// `id` is taken from the list exposed on `list_categories` and cannot be
222    /// one of the top-level (parent) categories. To de-categorize a
223    /// transaction, set the entire `data` key to `null`. The associated
224    /// category, along with its request URL is also exposed via the category
225    /// relationship on the transaction resource returned from `get_transaction`.
226    pub async fn categorise_transaction(
227        &self,
228        transaction_id: &str,
229        category: Option<&str>,
230    ) -> Result<(), error::Error> {
231        let url = reqwest::Url::parse(
232            &format!("{}/transactions/{}/relationships/category",
233                BASE_URL,
234                transaction_id,
235            )
236        ).map_err(error::Error::UrlParse)?;
237
238        let category = category.map(|id| {
239            CategoryInputResourceIdentifier {
240                r#type: String::from("categories"),
241                id: String::from(id),
242            }
243        });
244
245        let body = CategoriseTransactionRequest  { data: category };
246        let body =
247            serde_json::to_string(&body)
248            .map_err(error::Error::Serialize)?;
249
250        println!("{}", body);
251
252        let res = reqwest::Client::new()
253            .patch(url)
254            .header("Authorization", self.auth_header())
255            .header("Content-Type", "application/json")
256            .body(body)
257            .send()
258            .await
259            .map_err(error::Error::Request)?;
260
261        match res.status() {
262            reqwest::StatusCode::NO_CONTENT => {
263                Ok(())
264            },
265            _ => {
266                let body = res.text().await.map_err(error::Error::BodyRead)?;
267                let error: error::ErrorResponse =
268                    serde_json::from_str(&body).map_err(error::Error::Json)?;
269
270                Err(error::Error::Api(error))
271            }
272        }
273    }
274}