shared/domain/
category.rs

1//! Types for categories.
2
3use chrono::{DateTime, Utc};
4use macros::make_path_parts;
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8use crate::{api::endpoints::PathPart, domain::user::UserScope};
9
10wrap_uuid! {
11    /// Wrapper type around [`Uuid`], represents the ID of a category.
12    ///
13    /// [`Uuid`]: ../../uuid/struct.Uuid.html
14    pub struct CategoryId
15}
16
17#[derive(Serialize, Deserialize, Debug)]
18/// The response returned when a request for categories is successful.
19pub struct CategoryResponse {
20    /// The categories returned.
21    pub categories: Vec<Category>,
22}
23
24#[derive(Serialize, Deserialize, Debug, Clone)]
25/// The over-the-wire representation of a category.
26pub struct Category {
27    /// The category's id.
28    pub id: CategoryId,
29
30    /// The category's user_scope.
31    pub user_scopes: Vec<UserScope>,
32
33    /// The category's name.
34    pub name: String,
35
36    /// The category's children, if any.
37    #[serde(default)]
38    #[serde(skip_serializing_if = "Vec::is_empty")]
39    pub children: Vec<Category>,
40
41    /// When the category was initially created.
42    pub created_at: DateTime<Utc>,
43
44    /// When the category was last updated.
45    pub updated_at: Option<DateTime<Utc>>,
46}
47
48#[derive(Serialize, Deserialize, Debug)]
49/// When getting a tree of categories, which direction should the categories be followed?
50pub enum CategoryTreeScope {
51    /// Follow the parents up to the root.
52    Ancestors,
53
54    /// Follow the children down.
55    Descendants,
56}
57
58#[derive(Serialize, Deserialize)]
59/// Request to create a category.
60pub struct CreateCategoryRequest {
61    /// The name of the new category.
62    pub name: String,
63
64    /// The [`id`](Category::id) of the parent [`Category`](Category) to attatch it to.
65    pub parent_id: Option<CategoryId>,
66}
67
68make_path_parts!(GetCategoryPath => "/v1/category");
69
70/// Request to get a tree of categories.
71///
72/// # Examples
73///
74/// There are a few different use cases.
75///
76/// ### get root categories.
77/// ```ignore
78/// GetCategoryRequest { ids: vec![], scope: None }
79/// ```
80///
81/// Additionally, you can do the same with `scope: Some(CategoryTreeScope::Ancestors)` but it is not considered the cannonical form.
82///
83/// ### get all categories
84/// ```ignore
85/// GetCategoryRequest { ids: vec![], scope: Some(CategoryTreeScope::Descendants) }
86/// ```
87///
88/// ### get exact categories
89/// ```ignore
90/// GetCategoryRequest { ids: vec![id1, id2, ...], scope: None }
91/// ```
92///
93/// ### get exact categories and their ancestors
94/// ```ignore
95/// GetCategoryRequest { ids: vec![id1, id2, ...], scope: Some(CategoryTreeScope::Ancestors) }
96/// ```
97///
98/// ### get exact categories and their decendants.
99/// ```ignore
100/// GetCategoryRequest { ids: vec![id1, id2, ...], scope: Some(CategoryTreeScope::Descendants) }
101/// ```
102#[derive(Serialize, Deserialize, Debug, Default)]
103pub struct GetCategoryRequest {
104    // fixme: Use CategoryId, unfortunately, sqlx doesn't currently allow for passing of T
105    // the backend _could_ transmute the `CategoryId`s into `Uuid`s, but that's `unsafe`.
106    /// The exact ids to be included in the response.
107    #[serde(default)]
108    #[serde(serialize_with = "super::csv_encode_uuids")]
109    #[serde(deserialize_with = "super::from_csv")]
110    pub ids: Vec<Uuid>,
111
112    /// Which direction to follow the tree.
113    #[serde(default)]
114    pub scope: Option<CategoryTreeScope>,
115}
116
117make_path_parts!(CreateCategoryPath => "/v1/category");
118
119#[derive(Serialize, Deserialize)]
120/// Response returned when a new category is created.
121pub struct NewCategoryResponse {
122    /// The offset visual offset into the parent category.
123    pub index: u16,
124
125    /// The id of the new category.
126    pub id: CategoryId,
127}
128
129make_path_parts!(UpdateCategoryPath => "/v1/category/{}" => CategoryId);
130
131#[derive(Serialize, Deserialize, Default, Eq, PartialEq)]
132/// Request to update a category.
133///
134/// All fields are optional, any field that is [`None`] will not be updated.
135///
136/// # Errors
137///
138/// * [`UpdateError::OutOfRange`](crate::error::category::UpdateError::OutOfRange) if the given index is past the end of the parent.
139pub struct UpdateCategoryRequest {
140    /// If [`Some`] change the category's name to this name
141    pub name: Option<String>,
142
143    /// If [`Some`], change the parent to the given `Option<CategoryId>`.
144    ///
145    /// Specifically, if [`None`], don't update.
146    /// If `Some(None)`, set the parent to [`None`].
147    /// Otherwise set it to the given [`CategoryId`].
148    #[serde(deserialize_with = "super::deserialize_optional_field")]
149    #[serde(skip_serializing_if = "Option::is_none")]
150    #[serde(default)]
151    pub parent_id: Option<Option<CategoryId>>,
152
153    /// If [`Some`] move to _before_ the category with the given index (ie, 0 moves to the start).
154    ///
155    /// # interactions
156    /// If `index` is [`None`], and [`parent_id`](UpdateCategoryRequest::parent_id) is [`Some`] it will append to the end of the new parent.
157    pub index: Option<u16>,
158
159    /// If [`Some`] add a user scope
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub user_scopes: Option<Vec<UserScope>>,
162}
163
164make_path_parts!(DeleteCategoryPath => "/v1/category/{}" => CategoryId);