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