1use crate::client::metadata::*;
2use reqwest::header::ACCEPT;
3use secrecy::{ExposeSecret, SecretString};
4use serde::{Deserialize, Serialize};
5use serde_json::Number;
6use time::{OffsetDateTime, format_description::well_known::Rfc3339};
7
8use super::{ClientError, WekanClient, transport::decode_success};
9
10pub mod attachments;
11pub mod card_settings;
12mod copy;
13pub mod domains;
14mod export;
15pub mod import;
16pub mod presentation;
17pub mod trello_import;
18pub use copy::CopyBoardRequest;
19pub mod labels;
20pub mod members;
21
22#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
23#[serde(rename_all = "lowercase")]
24pub enum BoardPermission {
25 #[default]
26 Private,
27 Public,
28 Instance,
29}
30
31impl BoardPermission {
32 pub(crate) const fn as_str(self) -> &'static str {
33 match self {
34 Self::Private => "private",
35 Self::Public => "public",
36 Self::Instance => "instance",
37 }
38 }
39}
40
41#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
42#[serde(rename_all = "lowercase")]
43pub enum BoardColor {
44 #[default]
45 Belize,
46 Nephritis,
47 Pomegranate,
48 Pumpkin,
49 Wisteria,
50 Moderatepink,
51 Strongcyan,
52 Limegreen,
53 Midnight,
54 Dark,
55 Relax,
56 Corteza,
57 Appleglasspastel,
58 Clearblue,
59 Cleargreen,
60 Clearorange,
61 Clearpink,
62 Clearpurple,
63 Clearred,
64 Natural,
65 Modern,
66 Moderndark,
67 Exodark,
68 Cleandark,
69 Cleanlight,
70}
71
72impl BoardColor {
73 pub(crate) const fn as_str(self) -> &'static str {
74 match self {
75 Self::Belize => "belize",
76 Self::Nephritis => "nephritis",
77 Self::Pomegranate => "pomegranate",
78 Self::Pumpkin => "pumpkin",
79 Self::Wisteria => "wisteria",
80 Self::Moderatepink => "moderatepink",
81 Self::Strongcyan => "strongcyan",
82 Self::Limegreen => "limegreen",
83 Self::Midnight => "midnight",
84 Self::Dark => "dark",
85 Self::Relax => "relax",
86 Self::Corteza => "corteza",
87 Self::Appleglasspastel => "appleglasspastel",
88 Self::Clearblue => "clearblue",
89 Self::Cleargreen => "cleargreen",
90 Self::Clearorange => "clearorange",
91 Self::Clearpink => "clearpink",
92 Self::Clearpurple => "clearpurple",
93 Self::Clearred => "clearred",
94 Self::Natural => "natural",
95 Self::Modern => "modern",
96 Self::Moderndark => "moderndark",
97 Self::Exodark => "exodark",
98 Self::Cleandark => "cleandark",
99 Self::Cleanlight => "cleanlight",
100 }
101 }
102}
103
104#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
105#[serde(rename_all = "kebab-case")]
106pub enum PresentParentTask {
107 PrefixWithFullPath,
108 PrefixWithParent,
109 SubtextWithFullPath,
110 SubtextWithParent,
111 NoParent,
112}
113
114impl PresentParentTask {
115 pub(crate) const fn as_str(self) -> &'static str {
116 match self {
117 Self::PrefixWithFullPath => "prefix-with-full-path",
118 Self::PrefixWithParent => "prefix-with-parent",
119 Self::SubtextWithFullPath => "subtext-with-full-path",
120 Self::SubtextWithParent => "subtext-with-parent",
121 Self::NoParent => "no-parent",
122 }
123 }
124}
125
126#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
127#[serde(rename_all = "kebab-case")]
128pub enum BoardType {
129 Board,
130 TemplateBoard,
131 TemplateContainer,
132}
133
134impl BoardType {
135 pub(crate) const fn as_str(self) -> &'static str {
136 match self {
137 Self::Board => "board",
138 Self::TemplateBoard => "template-board",
139 Self::TemplateContainer => "template-container",
140 }
141 }
142}
143
144#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
145#[serde(rename_all = "lowercase")]
146pub enum BoardWatchLevel {
147 Watching,
148 Tracking,
149 Muted,
150}
151
152impl BoardWatchLevel {
153 pub(crate) const fn as_str(self) -> &'static str {
154 match self {
155 Self::Watching => "watching",
156 Self::Tracking => "tracking",
157 Self::Muted => "muted",
158 }
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
163#[serde(rename_all = "camelCase")]
164pub struct CreateBoardRequest {
165 pub title: String,
166 pub permission: BoardPermission,
167 pub color: BoardColor,
168}
169
170#[derive(Clone, Debug, Eq, PartialEq)]
171pub struct CreateBoardResult {
172 pub board_id: String,
173 pub default_swimlane_id: String,
174}
175
176#[derive(Clone, Debug, Eq, PartialEq)]
177pub struct RenameBoardResult {
178 pub board_id: String,
179 pub title: String,
180}
181
182#[derive(Clone, Debug, Eq, PartialEq)]
183pub struct DeleteBoardResult {
184 pub board_id: String,
185}
186
187#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
188#[serde(deny_unknown_fields)]
189pub struct BoardCounts {
190 pub instance: u64,
191 pub private: u64,
192 pub public: u64,
193}
194
195#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
196#[serde(deny_unknown_fields, rename_all = "camelCase")]
197pub struct BoardSummary {
198 #[serde(rename = "_id")]
199 pub board_id: String,
200 pub title: String,
201}
202
203#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
204#[serde(
205 deny_unknown_fields,
206 rename_all(deserialize = "camelCase", serialize = "snake_case")
207)]
208pub struct BoardMember {
209 #[serde(default, deserialize_with = "super::decoding::optional_non_null")]
210 pub granted_by: Option<Vec<String>>,
211 #[serde(default, deserialize_with = "super::decoding::optional_non_null")]
212 pub custom_role_id: Option<String>,
213 pub user_id: String,
214 pub is_admin: bool,
215 pub is_active: bool,
216 pub is_no_comments: Option<bool>,
217 pub is_comment_only: Option<bool>,
218 pub is_worker: Option<bool>,
219 pub is_normal_assigned_only: Option<bool>,
220 pub is_comment_assigned_only: Option<bool>,
221 pub is_read_only: Option<bool>,
222 pub is_read_assigned_only: Option<bool>,
223}
224
225#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
226#[serde(
227 deny_unknown_fields,
228 rename_all(deserialize = "camelCase", serialize = "snake_case")
229)]
230pub struct BoardWatcher {
231 pub user_id: String,
232 pub level: BoardWatchLevel,
233}
234
235#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
236#[serde(
237 deny_unknown_fields,
238 rename_all(deserialize = "camelCase", serialize = "snake_case")
239)]
240pub struct BoardLabel {
241 #[serde(default, deserialize_with = "super::decoding::optional_non_null")]
242 pub due_at: Option<String>,
243 #[serde(default, deserialize_with = "super::decoding::optional_non_null")]
244 pub shared_label_id: Option<String>,
245 #[serde(rename(deserialize = "_id", serialize = "label_id"))]
246 pub label_id: String,
247 pub name: Option<String>,
248 pub color: String,
249}
250
251#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
252#[serde(
253 deny_unknown_fields,
254 rename_all(deserialize = "camelCase", serialize = "snake_case")
255)]
256pub struct BoardOrganization {
257 pub org_id: String,
258 pub org_display_name: String,
259 pub is_active: bool,
260}
261
262#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
263#[serde(
264 deny_unknown_fields,
265 rename_all(deserialize = "camelCase", serialize = "snake_case")
266)]
267pub struct BoardTeam {
268 pub team_id: String,
269 pub team_display_name: String,
270 pub is_active: bool,
271}
272
273#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
274#[serde(
275 deny_unknown_fields,
276 rename_all(deserialize = "camelCase", serialize = "snake_case")
277)]
278pub struct BoardDomain {
279 pub domain: String,
280 pub is_active: bool,
281}
282
283#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
284#[serde(
285 deny_unknown_fields,
286 rename_all(deserialize = "camelCase", serialize = "snake_case")
287)]
288pub struct BoardDocument {
289 pub scrum: Option<Box<ScrumSettings>>,
290 pub scrum_import_losses: Option<Vec<ScrumImportLoss>>,
291 pub announcement: Option<BoardAnnouncement>,
292 pub wip_limit_groups: Option<Vec<BoardWipGroup>>,
293 pub email_to_card: Option<Box<BoardEmailToCard>>,
294 pub notification_delivery: Option<Box<NotificationDelivery>>,
295 pub column_sets: Option<Vec<BoardColumnSet>>,
296
297 pub scrum_revision: Option<serde_json::Number>,
298 pub allows_instance_editing: Option<bool>,
299 pub import_run_id: Option<String>,
300 pub sync_effects_enabled: Option<bool>,
301 pub map_image_attachment_id: Option<String>,
302 pub auto_archive_inactive_days: Option<Number>,
303 pub rules_visible_to_members: Option<bool>,
304 pub card_field_order: Option<Vec<String>>,
305 pub minicard_field_order: Option<Vec<String>>,
306 pub notify_override_tray: Option<bool>,
307 pub notify_override_email: Option<bool>,
308 pub due_reminder_days: Option<Vec<Number>>,
309 pub due_reminder_webhook: Option<bool>,
310 pub labels_above_title_on_card: Option<bool>,
311 pub allows_checklist_due_date_on_minicard: Option<bool>,
312 pub allows_location_on_minicard: Option<bool>,
313 pub allows_swimlane_name_on_card: Option<bool>,
314 pub allows_flowtime_on_minicard: Option<bool>,
315 pub allows_pomodoro_on_minicard: Option<bool>,
316 pub allows_attachment_list_on_minicard: Option<bool>,
317 pub allows_text_notes_on_minicard: Option<bool>,
318 pub allows_comment_count_on_card: Option<bool>,
319 pub allows_activities_on_minicard: Option<bool>,
320 pub allows_card_buttons_on_minicard: Option<bool>,
321 pub allows_description_badge_on_card: Option<bool>,
322 pub allows_unread_comments_on_card: Option<bool>,
323 pub allows_due_from_title_on_card: Option<bool>,
324 pub allows_due_from_title_on_minicard: Option<bool>,
325 pub allows_similar_cards_on_card: Option<bool>,
326 pub allows_similar_cards_on_minicard: Option<bool>,
327 pub allows_at_risk_on_card: Option<bool>,
328 pub allows_at_risk_on_minicard: Option<bool>,
329 pub allows_suggested_members_on_card: Option<bool>,
330 pub allows_forecast_on_card: Option<bool>,
331 pub allows_forecast_on_minicard: Option<bool>,
332 pub allows_checklist_from_description_on_card: Option<bool>,
333 pub allows_action_items_on_card: Option<bool>,
334 pub allows_whiteboard_on_card: Option<bool>,
335 pub shared_list_headers: Option<bool>,
336 pub board_view_settings: Option<
337 std::collections::BTreeMap<
338 String,
339 crate::client::boards::presentation::StoredBoardViewVisibility,
340 >,
341 >,
342 pub board_view_order: Option<Vec<String>>,
343 pub date_format_override: Option<bool>,
344 pub date_format: Option<String>,
345 pub ask_for_move_reason: Option<bool>,
346 pub subtasks_default_swimlane_id: Option<String>,
347 pub allows_swimlane_dragging: Option<bool>,
348 pub allows_list_dragging: Option<bool>,
349 pub allows_card_dragging: Option<bool>,
350 pub allows_checklist_dragging: Option<bool>,
351 pub allows_checklist_item_dragging: Option<bool>,
352 pub allows_subtask_dragging: Option<bool>,
353 pub allows_minicard_collapse: Option<bool>,
354 pub labels_above_title_on_minicard: Option<bool>,
355 pub allows_checklist_due_date: Option<bool>,
356 pub allows_checklist_title: Option<bool>,
357 pub allows_comments_on_minicard: Option<bool>,
358 pub show_label_text: Option<bool>,
359 pub allows_swimlane_name_on_minicard: Option<bool>,
360 pub allows_spent_time: Option<bool>,
361 pub allows_spent_time_on_minicard: Option<bool>,
362 pub allows_stickers: Option<bool>,
363 pub allows_stickers_on_minicard: Option<bool>,
364 pub allows_location: Option<bool>,
365 pub allows_dependencies: Option<bool>,
366 pub allows_dependencies_on_minicard: Option<bool>,
367 pub allows_flowtime: Option<bool>,
368 pub allows_pomodoro: Option<bool>,
369 pub allows_vote: Option<bool>,
370 pub allows_vote_on_minicard: Option<bool>,
371 pub allows_poker: Option<bool>,
372 pub allows_poker_on_minicard: Option<bool>,
373 pub allows_text_notes: Option<bool>,
374 pub allows_comment_count_on_minicard: Option<bool>,
375 pub allows_card_collapse: Option<bool>,
376 pub allows_checklist_title_on_minicard: Option<bool>,
377 pub allows_label_text_on_card: Option<bool>,
378 pub allows_card_color_on_card: Option<bool>,
379 pub allows_card_color_on_minicard: Option<bool>,
380 pub allows_parent_card_on_card: Option<bool>,
381 pub allows_parent_card_on_minicard: Option<bool>,
382 pub allows_linked_card_on_card: Option<bool>,
383 pub allows_linked_card_on_minicard: Option<bool>,
384 pub allows_card_buttons_on_card: Option<bool>,
385 pub allows_description_badge_on_minicard: Option<bool>,
386 pub allows_unread_comments_on_minicard: Option<bool>,
387 pub allows_issue_type_on_minicard: Option<bool>,
388 pub sticky_list_headers: Option<bool>,
389 pub list_width_resize_locked: Option<bool>,
390 pub swimlane_height_resize_locked: Option<bool>,
391 pub default_public_board_view: Option<String>,
392 pub default_private_board_view: Option<String>,
393 pub same_width_for_all_lists: Option<bool>,
394 pub same_width_for_all_lists_value: Option<Number>,
395 pub key: Option<String>,
396 #[serde(rename(deserialize = "_id", serialize = "board_id"))]
397 pub board_id: String,
398 pub title: String,
399 pub slug: Option<String>,
400 pub archived: Option<bool>,
401 pub archived_at: Option<String>,
402 pub created_at: Option<String>,
403 pub modified_at: Option<String>,
404 pub stars: Option<Number>,
405 #[serde(default)]
406 pub labels: Vec<BoardLabel>,
407 pub members: Vec<BoardMember>,
408 #[serde(default)]
409 pub watchers: Vec<BoardWatcher>,
410 pub permission: Option<BoardPermission>,
411 #[serde(default)]
412 pub orgs: Vec<BoardOrganization>,
413 #[serde(default)]
414 pub teams: Vec<BoardTeam>,
415 #[serde(default)]
416 pub domains: Vec<BoardDomain>,
417 #[serde(default)]
418 pub import_usernames: Vec<String>,
419 pub color: Option<BoardColor>,
420 #[serde(default)]
421 pub custom_theme_colors: Vec<String>,
422 #[serde(rename(deserialize = "backgroundImageURL", serialize = "background_image_url"))]
423 pub background_image_url: Option<String>,
424 pub background_image_id: Option<String>,
425 pub allows_card_counter_list: Option<bool>,
426 pub card_aging: Option<bool>,
427 pub show_dependencies: Option<bool>,
428 pub card_aging_days1: Option<Number>,
429 pub card_aging_days2: Option<Number>,
430 pub card_aging_days3: Option<Number>,
431 pub allows_board_member_list: Option<bool>,
432 pub description: Option<String>,
433 pub subtasks_default_board_id: Option<String>,
434 pub migration_version: Option<Number>,
435 pub subtasks_default_list_id: Option<String>,
436 pub date_settings_default_board_id: Option<String>,
437 pub date_settings_default_list_id: Option<String>,
438 pub allows_subtasks: Option<bool>,
439 pub allows_subtasks_on_minicard: Option<bool>,
440 pub allows_attachments: Option<bool>,
441 pub allows_attachments_on_minicard: Option<bool>,
442 pub allows_checklists: Option<bool>,
443 pub allows_checklists_on_minicard: Option<bool>,
444 pub allows_custom_fields: Option<bool>,
445 pub allows_custom_fields_on_minicard: Option<bool>,
446 pub allows_checklist_count_badge_on_minicard: Option<bool>,
447 pub allows_comments: Option<bool>,
448 pub allows_description_title: Option<bool>,
449 pub allows_description_title_on_minicard: Option<bool>,
450 pub allows_description_text: Option<bool>,
451 pub allows_description_text_on_minicard: Option<bool>,
452 pub allows_cover_attachment_on_minicard: Option<bool>,
453 pub allows_cover_attachment_on_card: Option<bool>,
454 pub allows_badge_attachment_on_minicard: Option<bool>,
455 pub allows_attachment_count_on_card: Option<bool>,
456 pub allows_checklist_count_badge_on_card: Option<bool>,
457 pub allows_card_sorting_by_number_on_minicard: Option<bool>,
458 pub allows_card_number: Option<bool>,
459 pub allows_card_number_on_minicard: Option<bool>,
460 pub allows_activities: Option<bool>,
461 pub allows_labels: Option<bool>,
462 pub allows_labels_on_minicard: Option<bool>,
463 pub allows_creator: Option<bool>,
464 pub allows_creator_on_minicard: Option<bool>,
465 pub allows_assignee: Option<bool>,
466 pub allows_assignee_on_minicard: Option<bool>,
467 pub allows_members: Option<bool>,
468 pub allows_members_on_minicard: Option<bool>,
469 pub allows_requested_by: Option<bool>,
470 pub allows_requested_by_on_minicard: Option<bool>,
471 pub allows_card_sorting_by_number: Option<bool>,
472 pub allows_show_lists: Option<bool>,
473 pub allows_assigned_by: Option<bool>,
474 pub allows_assigned_by_on_minicard: Option<bool>,
475 pub allows_show_lists_on_minicard: Option<bool>,
476 pub allows_checklist_at_minicard: Option<bool>,
477 pub allows_received_date: Option<bool>,
478 pub restrict_comment_editing: Option<bool>,
479 pub allows_personal_list_width: Option<bool>,
480 pub auto_width: Option<bool>,
481 pub allows_received_date_on_minicard: Option<bool>,
482 pub allows_start_date: Option<bool>,
483 pub allows_start_date_on_minicard: Option<bool>,
484 pub allows_end_date: Option<bool>,
485 pub allows_end_date_on_minicard: Option<bool>,
486 pub allows_due_date: Option<bool>,
487 pub allows_due_date_on_minicard: Option<bool>,
488 pub allows_due_complete: Option<bool>,
489 pub allows_due_complete_on_minicard: Option<bool>,
490 pub present_parent_task: Option<PresentParentTask>,
491 pub received_at: Option<String>,
492 pub start_at: Option<String>,
493 pub due_at: Option<String>,
494 pub end_at: Option<String>,
495 pub spent_time: Option<Number>,
496 pub is_overtime: Option<bool>,
497 #[serde(rename(deserialize = "type", serialize = "board_type"))]
498 pub board_type: Option<BoardType>,
499 pub sort: Option<Number>,
500 pub show_activities: Option<bool>,
501}
502
503#[derive(Deserialize)]
504#[serde(deny_unknown_fields, rename_all = "camelCase")]
505struct CreateBoardResponse {
506 #[serde(rename = "_id")]
507 board_id: String,
508 default_swimlane_id: String,
509}
510
511#[derive(Serialize)]
512struct RenameBoardRequest<'a> {
513 title: &'a str,
514}
515
516#[derive(Deserialize)]
517#[serde(deny_unknown_fields)]
518struct RenameBoardResponse {
519 #[serde(rename = "_id")]
520 board_id: String,
521 title: String,
522}
523
524#[derive(Deserialize)]
525#[serde(deny_unknown_fields)]
526struct IdResponse {
527 #[serde(rename = "_id")]
528 id: String,
529}
530
531impl WekanClient {
532 pub async fn public_boards(
533 &self,
534 token: &SecretString,
535 ) -> Result<Vec<BoardSummary>, ClientError> {
536 let body = self
537 .execute_success_request(
538 self.http
539 .get(self.boards_api_endpoint("api/boards", "public boards")?)
540 .header(ACCEPT, "application/json")
541 .bearer_auth(token.expose_secret()),
542 )
543 .await?;
544 let boards = decode_success(&body, "public boards", true)?;
545 validate_board_summaries(boards, "public boards")
546 }
547
548 pub async fn board_counts(&self, token: &SecretString) -> Result<BoardCounts, ClientError> {
549 let body = self
550 .execute_success_request(
551 self.http
552 .get(self.boards_api_endpoint("api/boards_count", "board counts")?)
553 .header(ACCEPT, "application/json")
554 .bearer_auth(token.expose_secret()),
555 )
556 .await?;
557 decode_success(&body, "board counts", true)
558 }
559
560 pub async fn board(
561 &self,
562 board_id: &str,
563 token: &SecretString,
564 ) -> Result<BoardDocument, ClientError> {
565 let body = self
566 .execute_success_request(
567 self.http
568 .get(self.board_endpoint(board_id, None)?)
569 .header(ACCEPT, "application/json")
570 .bearer_auth(token.expose_secret()),
571 )
572 .await?;
573 let board = decode_success(&body, "board", true)?;
574 validate_board_document(board)
575 }
576
577 pub async fn create_board(
578 &self,
579 request: &CreateBoardRequest,
580 token: &SecretString,
581 ) -> Result<CreateBoardResult, ClientError> {
582 let body = self
583 .execute_success_request(
584 self.http
585 .post(self.boards_api_endpoint("api/boards", "board creation")?)
586 .header(ACCEPT, "application/json")
587 .bearer_auth(token.expose_secret())
588 .json(request),
589 )
590 .await?;
591 let response: CreateBoardResponse = decode_success(&body, "board creation", true)?;
592 require_non_empty(&response.board_id, "board creation", "board id")?;
593 require_non_empty(
594 &response.default_swimlane_id,
595 "board creation",
596 "default swimlane id",
597 )?;
598 Ok(CreateBoardResult {
599 board_id: response.board_id,
600 default_swimlane_id: response.default_swimlane_id,
601 })
602 }
603
604 pub async fn rename_board(
605 &self,
606 board_id: &str,
607 title: &str,
608 token: &SecretString,
609 ) -> Result<RenameBoardResult, ClientError> {
610 let body = self
611 .execute_success_request(
612 self.http
613 .put(self.board_endpoint(board_id, Some("title"))?)
614 .header(ACCEPT, "application/json")
615 .bearer_auth(token.expose_secret())
616 .json(&RenameBoardRequest { title }),
617 )
618 .await?;
619 let response: RenameBoardResponse = decode_success(&body, "board rename", true)?;
620 require_non_empty(&response.board_id, "board rename", "board id")?;
621 require_non_empty(&response.title, "board rename", "title")?;
622 Ok(RenameBoardResult {
623 board_id: response.board_id,
624 title: response.title,
625 })
626 }
627
628 pub async fn delete_board(
629 &self,
630 board_id: &str,
631 token: &SecretString,
632 ) -> Result<DeleteBoardResult, ClientError> {
633 let body = self
634 .execute_success_request(
635 self.http
636 .delete(self.board_endpoint(board_id, None)?)
637 .header(ACCEPT, "application/json")
638 .bearer_auth(token.expose_secret()),
639 )
640 .await?;
641 let response: IdResponse = decode_success(&body, "board deletion", true)?;
642 require_non_empty(&response.id, "board deletion", "board id")?;
643 Ok(DeleteBoardResult {
644 board_id: response.id,
645 })
646 }
647
648 fn boards_api_endpoint(
649 &self,
650 path: &str,
651 operation: &str,
652 ) -> Result<reqwest::Url, ClientError> {
653 self.server()
654 .join(path)
655 .map_err(|error| ClientError::Protocol {
656 diagnostic: None,
657 message: format!("could not build the {operation} endpoint: {error}"),
658 success_status_received: false,
659 })
660 }
661
662 pub(super) fn board_endpoint(
663 &self,
664 board_id: &str,
665 suffix: Option<&str>,
666 ) -> Result<reqwest::Url, ClientError> {
667 let mut endpoint = self.boards_api_endpoint("api/boards", "board")?;
668 let mut segments = endpoint
669 .path_segments_mut()
670 .map_err(|()| ClientError::Protocol {
671 diagnostic: None,
672 message: "could not add the board id to the endpoint".to_owned(),
673 success_status_received: false,
674 })?;
675 segments.push(board_id);
676 if let Some(suffix) = suffix {
677 segments.push(suffix);
678 }
679 drop(segments);
680 Ok(endpoint)
681 }
682}
683
684fn validate_board_summaries(
685 boards: Vec<BoardSummary>,
686 operation: &str,
687) -> Result<Vec<BoardSummary>, ClientError> {
688 for board in &boards {
689 require_non_empty(&board.board_id, operation, "board id")?;
690 require_non_empty(&board.title, operation, "title")?;
691 }
692 Ok(boards)
693}
694
695fn validate_board_document(board: BoardDocument) -> Result<BoardDocument, ClientError> {
696 require_non_empty(&board.board_id, "board", "board id")?;
697 require_non_empty(&board.title, "board", "title")?;
698 for label in &board.labels {
699 if let Some(value) = &label.due_at {
700 validate_date_time(value, "label dueAt")?;
701 }
702 }
703 for (field, value) in [
704 ("archivedAt", board.archived_at.as_deref()),
705 ("createdAt", board.created_at.as_deref()),
706 ("modifiedAt", board.modified_at.as_deref()),
707 ("receivedAt", board.received_at.as_deref()),
708 ("startAt", board.start_at.as_deref()),
709 ("dueAt", board.due_at.as_deref()),
710 ("endAt", board.end_at.as_deref()),
711 ] {
712 if let Some(value) = value {
713 validate_date_time(value, field)?;
714 }
715 }
716 Ok(board)
717}
718
719fn validate_date_time(value: &str, field: &str) -> Result<(), ClientError> {
720 OffsetDateTime::parse(value, &Rfc3339)
721 .map(|_| ())
722 .map_err(|_| ClientError::Protocol {
723 diagnostic: None,
724 message: format!("the board response contained an invalid {field} date-time"),
725 success_status_received: true,
726 })
727}
728
729fn require_non_empty(value: &str, operation: &str, field: &str) -> Result<(), ClientError> {
730 if value.is_empty() {
731 Err(ClientError::Protocol {
732 diagnostic: None,
733 message: format!("the {operation} response contained an empty {field}"),
734 success_status_received: true,
735 })
736 } else {
737 Ok(())
738 }
739}