1use reqwest::header::ACCEPT;
2use secrecy::{ExposeSecret, SecretString};
3use serde::{Deserialize, Serialize};
4use serde_json::Number;
5
6use super::{
7 ClientError, WekanClient,
8 checklists::{protocol_error, require_date, require_id},
9 transport::decode_success,
10};
11
12#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13#[serde(
14 deny_unknown_fields,
15 rename_all(deserialize = "camelCase", serialize = "snake_case")
16)]
17pub struct ChecklistItemDocument {
18 #[serde(default, deserialize_with = "super::decoding::optional_non_null")]
19 pub assignee_id: Option<String>,
20 #[serde(default, deserialize_with = "super::decoding::optional_non_null")]
21 pub linked_card_id: Option<String>,
22 pub due_at: Option<String>,
23 #[serde(rename(deserialize = "_id", serialize = "item_id"))]
24 pub item_id: String,
25 pub title: String,
26 pub sort: Number,
27 pub is_finished: bool,
28 pub checklist_id: String,
29 pub card_id: String,
30 pub board_id: Option<String>,
32 pub user_id: Option<String>,
33 pub created_at: Option<String>,
34 pub modified_at: String,
35}
36
37#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
38#[serde(untagged)]
39pub enum ChecklistItemSortInput {
40 Number(Number),
41 String(String),
42 Null,
43}
44
45#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
46#[serde(untagged)]
47pub enum ChecklistItemFinishedInput {
48 Boolean(bool),
49 String(String),
50}
51
52#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
53pub struct CreateChecklistItemRequest {
54 #[serde(rename = "dueAt", skip_serializing_if = "Option::is_none")]
55 pub due_at: Option<String>,
56 pub title: String,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub sort: Option<ChecklistItemSortInput>,
59}
60
61#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
62#[serde(rename_all = "camelCase")]
63pub struct UpdateChecklistItemRequest {
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub due_at: Option<Option<String>>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub title: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub is_finished: Option<ChecklistItemFinishedInput>,
70}
71
72#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct CreateChecklistItemResult {
74 pub item_id: String,
75}
76
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct UpdateChecklistItemResult {
79 pub item_id: String,
80}
81
82#[derive(Clone, Debug, Eq, PartialEq)]
83pub struct DeleteChecklistItemResult {
84 pub item_id: String,
85}
86
87#[derive(Deserialize)]
88#[serde(deny_unknown_fields)]
89struct IdResponse {
90 #[serde(rename = "_id")]
91 id: String,
92}
93
94impl WekanClient {
95 pub async fn checklist_item(
96 &self,
97 board_id: &str,
98 card_id: &str,
99 checklist_id: &str,
100 item_id: &str,
101 token: &SecretString,
102 ) -> Result<ChecklistItemDocument, ClientError> {
103 let body = self
104 .execute_success_request(
105 self.http
106 .get(self.checklist_item_endpoint(
107 board_id,
108 card_id,
109 checklist_id,
110 Some(item_id),
111 )?)
112 .header(ACCEPT, "application/json")
113 .bearer_auth(token.expose_secret()),
114 )
115 .await?;
116 let item: ChecklistItemDocument = decode_success(&body, "checklist item", true)?;
117 for (value, field) in [
118 (&item.item_id, "item id"),
119 (&item.checklist_id, "checklist id"),
120 (&item.card_id, "card id"),
121 ] {
122 require_id(value, field)?;
123 }
124 if let Some(id) = &item.board_id {
125 require_id(id, "board id")?;
126 }
127 if let Some(id) = &item.user_id {
128 require_id(id, "creator user id")?;
129 }
130 if item.item_id != item_id || item.checklist_id != checklist_id || item.card_id != card_id {
131 return Err(protocol_error(
132 "the checklist item response identifiers did not match the requested scope",
133 ));
134 }
135 require_date(&item.modified_at)?;
136 if let Some(date) = &item.due_at {
137 require_date(date)?;
138 }
139 if let Some(date) = &item.created_at {
140 require_date(date)?;
141 }
142 Ok(item)
143 }
144
145 pub async fn create_checklist_item(
146 &self,
147 board_id: &str,
148 card_id: &str,
149 checklist_id: &str,
150 request: &CreateChecklistItemRequest,
151 token: &SecretString,
152 ) -> Result<CreateChecklistItemResult, ClientError> {
153 let body = self
154 .execute_success_request(
155 self.http
156 .post(self.checklist_item_endpoint(board_id, card_id, checklist_id, None)?)
157 .header(ACCEPT, "application/json")
158 .bearer_auth(token.expose_secret())
159 .json(request),
160 )
161 .await?;
162 let response: IdResponse = decode_success(&body, "checklist item creation", true)?;
163 require_id(&response.id, "created item id")?;
164 Ok(CreateChecklistItemResult {
165 item_id: response.id,
166 })
167 }
168
169 pub async fn update_checklist_item(
170 &self,
171 board_id: &str,
172 card_id: &str,
173 checklist_id: &str,
174 item_id: &str,
175 request: &UpdateChecklistItemRequest,
176 token: &SecretString,
177 ) -> Result<UpdateChecklistItemResult, ClientError> {
178 let body = self
179 .execute_success_request(
180 self.http
181 .put(self.checklist_item_endpoint(
182 board_id,
183 card_id,
184 checklist_id,
185 Some(item_id),
186 )?)
187 .header(ACCEPT, "application/json")
188 .bearer_auth(token.expose_secret())
189 .json(request),
190 )
191 .await?;
192 let response: IdResponse = decode_success(&body, "checklist item update", true)?;
193 require_id(&response.id, "updated item id")?;
194 if response.id != item_id {
195 return Err(protocol_error(
196 "Wekan reported a different updated checklist item ID than requested",
197 ));
198 }
199 Ok(UpdateChecklistItemResult {
200 item_id: response.id,
201 })
202 }
203
204 pub async fn delete_checklist_item(
205 &self,
206 board_id: &str,
207 card_id: &str,
208 checklist_id: &str,
209 item_id: &str,
210 token: &SecretString,
211 ) -> Result<DeleteChecklistItemResult, ClientError> {
212 let body = self
213 .execute_success_request(
214 self.http
215 .delete(self.checklist_item_endpoint(
216 board_id,
217 card_id,
218 checklist_id,
219 Some(item_id),
220 )?)
221 .header(ACCEPT, "application/json")
222 .bearer_auth(token.expose_secret()),
223 )
224 .await?;
225 let response: IdResponse = decode_success(&body, "checklist item deletion", true)?;
226 require_id(&response.id, "deleted item id")?;
227 if response.id != item_id {
228 return Err(protocol_error(
229 "Wekan reported a different deleted checklist item ID than requested",
230 ));
231 }
232 Ok(DeleteChecklistItemResult {
233 item_id: response.id,
234 })
235 }
236
237 fn checklist_item_endpoint(
238 &self,
239 board_id: &str,
240 card_id: &str,
241 checklist_id: &str,
242 item_id: Option<&str>,
243 ) -> Result<reqwest::Url, ClientError> {
244 let mut endpoint = self.checklist_endpoint(board_id, card_id, Some(checklist_id))?;
245 let mut segments = endpoint
246 .path_segments_mut()
247 .map_err(|()| ClientError::Protocol {
248 diagnostic: None,
249 message: "could not add identifiers to the checklist item endpoint".to_owned(),
250 success_status_received: false,
251 })?;
252 segments.push("items");
253 if let Some(id) = item_id {
254 segments.push(id);
255 }
256 drop(segments);
257 Ok(endpoint)
258 }
259}