sure_client_rs/client/categories.rs
1use bon::bon;
2use reqwest::Method;
3
4use crate::ApiError;
5use crate::error::ApiResult;
6use crate::models::category::{
7 CategoryCollection, CategoryDetail, CreateCategoryData, CreateCategoryRequest,
8 UpdateCategoryData, UpdateCategoryRequest,
9};
10use crate::models::{DeleteResponse, PaginatedResponse};
11use crate::types::CategoryId;
12use std::collections::HashMap;
13
14use super::SureClient;
15
16const MAX_PER_PAGE: u32 = 100;
17
18#[bon]
19impl SureClient {
20 /// List categories with optional filters
21 ///
22 /// Retrieves a paginated list of categories. Results can be filtered by parent
23 /// category, or limited to root categories only.
24 ///
25 /// # Arguments
26 /// * `page` - Page number (default: 1)
27 /// * `per_page` - Items per page (default: 25, max: 100)
28 /// * `roots_only` - Return only root categories (default: false)
29 /// * `parent_id` - Filter by parent category ID
30 ///
31 /// # Returns
32 /// A paginated response containing categories and pagination metadata.
33 ///
34 /// # Errors
35 /// Returns `ApiError::Unauthorized` if the bearer token is invalid or expired.
36 /// Returns `ApiError::Network` if the request fails due to network issues.
37 #[builder]
38 pub async fn get_categories(
39 &self,
40 #[builder(default = 1)] page: u32,
41 #[builder(default = 25)] per_page: u32,
42 #[builder(default = false)] roots_only: bool,
43 parent_id: Option<&CategoryId>,
44 ) -> ApiResult<PaginatedResponse<CategoryCollection>> {
45 let mut query_params = HashMap::new();
46
47 if per_page > MAX_PER_PAGE {
48 return Err(ApiError::InvalidParameter(format!(
49 "per_page cannot exceed {MAX_PER_PAGE}",
50 )));
51 }
52
53 query_params.insert("page", page.to_string());
54 query_params.insert("per_page", per_page.to_string());
55 query_params.insert("roots_only", roots_only.to_string());
56
57 if let Some(parent_id) = parent_id {
58 query_params.insert("parent_id", parent_id.to_string());
59 }
60
61 self.execute_request(Method::GET, "/api/v1/categories", Some(&query_params), None)
62 .await
63 }
64
65 /// Get a specific category by ID
66 ///
67 /// Retrieves detailed information about a single category, including parent
68 /// and subcategory information.
69 ///
70 /// # Arguments
71 /// * `id` - The category ID to retrieve
72 ///
73 /// # Returns
74 /// Detailed category information including parent and subcategory count.
75 ///
76 /// # Errors
77 /// Returns `ApiError::NotFound` if the category doesn't exist.
78 /// Returns `ApiError::Unauthorized` if the bearer token is invalid or expired.
79 /// Returns `ApiError::Network` if the request fails due to network issues.
80 pub async fn get_category(&self, id: &CategoryId) -> ApiResult<CategoryDetail> {
81 self.execute_request(
82 Method::GET,
83 &format!("/api/v1/categories/{}", id),
84 None,
85 None,
86 )
87 .await
88 }
89}
90
91#[bon]
92impl SureClient {
93 /// Create a new category
94 ///
95 /// Creates a new category with the specified details.
96 ///
97 /// # Arguments
98 /// * `request` - The category creation request containing all required fields
99 ///
100 /// # Returns
101 /// The newly created category with full details.
102 ///
103 /// # Errors
104 /// Returns `ApiError::ValidationError` if required fields are missing or invalid.
105 /// Returns `ApiError::Unauthorized` if the API key is invalid.
106 /// Returns `ApiError::Network` if the request fails due to network issues.
107 ///
108 /// # Example
109 /// ```no_run
110 /// use sure_client_rs::SureClient;
111 ///
112 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
113 /// let category = client.create_category()
114 /// .name("Groceries".to_string())
115 /// .color("#FF5733".to_string())
116 /// .lucide_icon("shopping-cart".to_string())
117 /// .call()
118 /// .await?;
119 ///
120 /// println!("Created category: {}", category.name);
121 /// # Ok(())
122 /// # }
123 /// ```
124 #[builder]
125 pub async fn create_category(
126 &self,
127 name: String,
128 color: String,
129 lucide_icon: Option<String>,
130 parent_id: Option<CategoryId>,
131 ) -> ApiResult<CategoryDetail> {
132 let request = CreateCategoryRequest {
133 category: CreateCategoryData {
134 name,
135 color,
136 lucide_icon,
137 parent_id,
138 },
139 };
140
141 self.execute_request(
142 Method::POST,
143 "/api/v1/categories",
144 None,
145 Some(serde_json::to_string(&request)?),
146 )
147 .await
148 }
149
150 /// Update a category
151 ///
152 /// Updates an existing category with new values. Only fields provided in the
153 /// request will be updated.
154 ///
155 /// # Arguments
156 /// * `id` - The category ID to update
157 /// * `request` - The category update request containing fields to update
158 ///
159 /// # Returns
160 /// The updated category.
161 ///
162 /// # Errors
163 /// Returns `ApiError::NotFound` if the category doesn't exist.
164 /// Returns `ApiError::ValidationError` if the provided values are invalid.
165 /// Returns `ApiError::Unauthorized` if the API key is invalid.
166 /// Returns `ApiError::Network` if the request fails due to network issues.
167 ///
168 /// # Example
169 /// ```no_run
170 /// use sure_client_rs::{SureClient, BearerToken, CategoryId};
171 /// use uuid::Uuid;
172 ///
173 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
174 /// let category_id = CategoryId::new(Uuid::new_v4());
175 ///
176 /// let category = client.update_category()
177 /// .id(&category_id)
178 /// .name("Updated Category Name".to_string())
179 /// .color("#00FF00".to_string())
180 /// .call()
181 /// .await?;
182 ///
183 /// println!("Updated category: {}", category.name);
184 /// # Ok(())
185 /// # }
186 /// ```
187 #[builder]
188 pub async fn update_category(
189 &self,
190 id: &CategoryId,
191 name: Option<String>,
192 color: Option<String>,
193 lucide_icon: Option<String>,
194 parent_id: Option<CategoryId>,
195 ) -> ApiResult<CategoryDetail> {
196 let request = UpdateCategoryRequest {
197 category: UpdateCategoryData {
198 name,
199 color,
200 lucide_icon,
201 parent_id,
202 },
203 };
204
205 self.execute_request(
206 Method::PATCH,
207 &format!("/api/v1/categories/{}", id),
208 None,
209 Some(serde_json::to_string(&request)?),
210 )
211 .await
212 }
213
214 /// Delete a category
215 ///
216 /// Permanently deletes a category.
217 ///
218 /// # Arguments
219 /// * `id` - The category ID to delete
220 ///
221 /// # Returns
222 /// A confirmation message.
223 ///
224 /// # Errors
225 /// Returns `ApiError::NotFound` if the category doesn't exist.
226 /// Returns `ApiError::Unauthorized` if the API key is invalid.
227 /// Returns `ApiError::Network` if the request fails due to network issues.
228 ///
229 /// # Example
230 /// ```no_run
231 /// use sure_client_rs::{SureClient, BearerToken, CategoryId};
232 /// use uuid::Uuid;
233 ///
234 /// # async fn example(client: SureClient) -> Result<(), Box<dyn std::error::Error>> {
235 /// let category_id = CategoryId::new(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
236 /// let response = client.delete_category(&category_id).await?;
237 ///
238 /// println!("Deleted: {}", response.message);
239 /// # Ok(())
240 /// # }
241 /// ```
242 pub async fn delete_category(&self, id: &CategoryId) -> ApiResult<DeleteResponse> {
243 self.execute_request(
244 Method::DELETE,
245 &format!("/api/v1/categories/{}", id),
246 None,
247 None,
248 )
249 .await
250 }
251}