Skip to main content

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}