1use crate::v1::{Client, error, BASE_URL};
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Deserialize, Debug)]
8pub struct ListCategoriesResponse {
9 pub data: Vec<CategoryResource>,
11}
12
13#[derive(Deserialize, Debug)]
14pub struct GetCategoryResponse {
15 pub data: CategoryResource,
17
18}
19
20#[derive(Deserialize, Debug)]
21pub struct CategoryResource {
22 pub r#type: String,
24 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 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 pub r#type: String,
54 pub id: String,
56}
57
58#[derive(Deserialize, Debug)]
59pub struct ParentLinks {
60 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 pub r#type: String,
74 pub id: String,
76}
77
78#[derive(Deserialize, Debug)]
79pub struct ChildrenLinks {
80 pub related: String,
82}
83
84#[derive(Deserialize, Debug)]
85pub struct CategoryResourceLinks {
86 #[serde(rename = "self")]
88 pub this: String,
89}
90
91#[derive(Default)]
94pub struct ListCategoriesOptions {
95 filter_parent: Option<String>,
98}
99
100impl ListCategoriesOptions {
101 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#[derive(Serialize)]
127struct CategoriseTransactionRequest {
128 data: Option<CategoryInputResourceIdentifier>,
131}
132
133#[derive(Serialize)]
134struct CategoryInputResourceIdentifier {
135 r#type: String,
137 id: String,
140}
141
142impl Client {
143 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 pub async fn get_category(
181 &self, id: &str,
182 ) -> Result<GetCategoryResponse, error::Error> {
183 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 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}