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