Skip to main content

wekan_cli/
output.rs

1mod attachments;
2mod auth;
3mod boards;
4mod cards;
5mod checklist_items;
6mod checklists;
7mod comments;
8mod custom_field_items;
9mod custom_fields;
10mod dependencies;
11mod formatting;
12mod integrations;
13mod lists;
14mod profiles;
15mod raw;
16mod rules;
17mod swimlanes;
18mod users;
19
20use auth::{render_auth_status, render_auth_success, render_logout};
21use boards::{
22    render_board_count, render_board_created, render_board_deleted, render_board_detail,
23    render_board_list, render_board_renamed,
24};
25use cards::{
26    render_card_archived, render_card_collection, render_card_created, render_card_deleted,
27    render_card_detail, render_card_restored, render_card_updated,
28};
29use checklist_items::{
30    render_checklist_item_created, render_checklist_item_deleted, render_checklist_item_detail,
31    render_checklist_item_updated,
32};
33use checklists::{
34    render_checklist_collection, render_checklist_created, render_checklist_deleted,
35    render_checklist_detail,
36};
37use comments::{
38    render_comment_collection, render_comment_created, render_comment_deleted,
39    render_comment_detail,
40};
41use lists::{
42    render_list_collection, render_list_created, render_list_deleted, render_list_detail,
43    render_list_updated,
44};
45use profiles::{render_profile_item, render_profile_list, render_profile_removed};
46use swimlanes::{
47    render_swimlane_collection, render_swimlane_created, render_swimlane_deleted,
48    render_swimlane_detail, render_swimlane_updated,
49};
50use users::{
51    render_user_boards, render_user_cards, render_user_created, render_user_deleted,
52    render_user_detail, render_user_list, render_user_login_change, render_user_ownership,
53};
54
55use std::{ffi::OsString, io::Write, process::ExitCode};
56
57use clap::ValueEnum;
58use serde::Serialize;
59
60use crate::command_result::CommandSuccess;
61use crate::{
62    error::{AppError, ErrorCode},
63    exit_code::StableExitCode,
64};
65
66pub(crate) use raw::render_human_response as render_api_response_human;
67#[cfg(test)]
68pub(crate) use raw::structured_api_response;
69
70#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
71#[value(rename_all = "lower")]
72pub enum OutputFormat {
73    #[default]
74    Human,
75    Json,
76    Raw,
77}
78
79impl OutputFormat {
80    pub fn detect_from_args(args: &[OsString]) -> Self {
81        let mut iter = args.iter().filter_map(|arg| arg.to_str());
82        while let Some(arg) = iter.next() {
83            if arg == "--output" {
84                return match iter.next() {
85                    Some("json") => Self::Json,
86                    Some("raw") => Self::Raw,
87                    _ => Self::Human,
88                };
89            }
90            if arg == "--output=json" {
91                return Self::Json;
92            }
93            if arg == "--output=raw" {
94                return Self::Raw;
95            }
96        }
97        Self::Human
98    }
99}
100
101#[derive(Serialize)]
102struct SuccessEnvelope<'a, T> {
103    ok: bool,
104    data: &'a T,
105}
106
107#[derive(Serialize)]
108struct ErrorEnvelope<'a> {
109    ok: bool,
110    error: ErrorBody<'a>,
111}
112
113#[derive(Serialize)]
114struct ErrorBody<'a> {
115    code: &'static str,
116    message: &'a str,
117    details: &'a crate::error::ErrorDetails,
118}
119
120pub fn render_success(format: OutputFormat, success: &CommandSuccess) -> String {
121    match (format, success) {
122        (OutputFormat::Human, CommandSuccess::AdminDomains(data)) => {
123            formatting::render_object(data)
124        }
125        (OutputFormat::Json, CommandSuccess::AdminDomains(data)) => render_json_success(data),
126        (OutputFormat::Human, CommandSuccess::AdminOrganizations(data)) => {
127            formatting::render_object(data)
128        }
129        (OutputFormat::Json, CommandSuccess::AdminOrganizations(data)) => render_json_success(data),
130        (OutputFormat::Human, CommandSuccess::AdminTeams(data)) => formatting::render_object(data),
131        (OutputFormat::Json, CommandSuccess::AdminTeams(data)) => render_json_success(data),
132        (OutputFormat::Human, CommandSuccess::AdminOrganizationUpdated(data)) => {
133            formatting::render_object(data)
134        }
135        (OutputFormat::Json, CommandSuccess::AdminOrganizationUpdated(data)) => {
136            render_json_success(data)
137        }
138        (OutputFormat::Human, CommandSuccess::AdminTeamUpdated(data)) => {
139            formatting::render_object(data)
140        }
141        (OutputFormat::Json, CommandSuccess::AdminTeamUpdated(data)) => render_json_success(data),
142        (OutputFormat::Human, CommandSuccess::AdminOrganizationFeaturesUpdatedAll(data)) => {
143            formatting::render_object(data)
144        }
145        (OutputFormat::Json, CommandSuccess::AdminOrganizationFeaturesUpdatedAll(data)) => {
146            render_json_success(data)
147        }
148        (OutputFormat::Human, CommandSuccess::AdminTeamFeaturesUpdatedAll(data)) => {
149            formatting::render_object(data)
150        }
151        (OutputFormat::Json, CommandSuccess::AdminTeamFeaturesUpdatedAll(data)) => {
152            render_json_success(data)
153        }
154        (OutputFormat::Human, CommandSuccess::GlobalSettings(data)) => {
155            formatting::render_object(data)
156        }
157        (OutputFormat::Json, CommandSuccess::GlobalSettings(data)) => render_json_success(data),
158        (OutputFormat::Human, CommandSuccess::GlobalSettingsUpdated(data)) => {
159            formatting::render_object(data)
160        }
161        (OutputFormat::Json, CommandSuccess::GlobalSettingsUpdated(data)) => {
162            render_json_success(data)
163        }
164        (OutputFormat::Human, CommandSuccess::AttachmentSettings(data)) => {
165            formatting::render_object(data)
166        }
167        (OutputFormat::Json, CommandSuccess::AttachmentSettings(data)) => render_json_success(data),
168        (OutputFormat::Human, CommandSuccess::AttachmentSettingsUpdated(data)) => {
169            formatting::render_object(data)
170        }
171        (OutputFormat::Json, CommandSuccess::AttachmentSettingsUpdated(data)) => {
172            render_json_success(data)
173        }
174
175        (OutputFormat::Human, CommandSuccess::BoardCardSettings(data)) => {
176            formatting::render_object(data)
177        }
178        (OutputFormat::Json, CommandSuccess::BoardCardSettings(data)) => render_json_success(data),
179        (OutputFormat::Human, CommandSuccess::BoardCardSettingsUpdated(data)) => {
180            formatting::render_object(data)
181        }
182        (OutputFormat::Json, CommandSuccess::BoardCardSettingsUpdated(data)) => {
183            render_json_success(data)
184        }
185        (OutputFormat::Human, CommandSuccess::BoardDomains(data)) => {
186            formatting::render_object(data)
187        }
188        (OutputFormat::Json, CommandSuccess::BoardDomains(data)) => render_json_success(data),
189        (OutputFormat::Human, CommandSuccess::BoardDomainChanged(data)) => format!(
190            "Board domains returned for the requested sharing change:\n{}",
191            formatting::render_object(data)
192        ),
193        (OutputFormat::Json, CommandSuccess::BoardDomainChanged(data)) => render_json_success(data),
194        (OutputFormat::Human, CommandSuccess::RuleCollection(data)) => rules::collection(data),
195        (OutputFormat::Json, CommandSuccess::RuleCollection(data)) => render_json_success(data),
196        (OutputFormat::Human, CommandSuccess::RuleShown(data)) => rules::detail(data),
197        (OutputFormat::Json, CommandSuccess::RuleShown(data)) => render_json_success(data),
198        (OutputFormat::Human, CommandSuccess::RuleCreated(data)) => rules::created(data),
199        (OutputFormat::Json, CommandSuccess::RuleCreated(data)) => render_json_success(data),
200        (OutputFormat::Human, CommandSuccess::RuleUpdated(data)) => rules::updated(data),
201        (OutputFormat::Json, CommandSuccess::RuleUpdated(data)) => render_json_success(data),
202        (OutputFormat::Human, CommandSuccess::RuleDeleted(data)) => rules::deleted(data),
203        (OutputFormat::Json, CommandSuccess::RuleDeleted(data)) => render_json_success(data),
204        (OutputFormat::Human, CommandSuccess::IntegrationCollection(data)) => {
205            integrations::collection(data)
206        }
207        (OutputFormat::Json, CommandSuccess::IntegrationCollection(data)) => {
208            render_json_success(data)
209        }
210        (OutputFormat::Human, CommandSuccess::IntegrationShown(data)) => integrations::detail(data),
211        (OutputFormat::Json, CommandSuccess::IntegrationShown(data)) => render_json_success(data),
212        (OutputFormat::Human, CommandSuccess::IntegrationCreated(data)) => {
213            integrations::created(data)
214        }
215        (OutputFormat::Json, CommandSuccess::IntegrationCreated(data)) => render_json_success(data),
216        (OutputFormat::Human, CommandSuccess::IntegrationUpdated(data)) => {
217            integrations::updated(data)
218        }
219        (OutputFormat::Json, CommandSuccess::IntegrationUpdated(data)) => render_json_success(data),
220        (OutputFormat::Human, CommandSuccess::IntegrationDeleted(data)) => {
221            integrations::deleted(data)
222        }
223        (OutputFormat::Json, CommandSuccess::IntegrationDeleted(data)) => render_json_success(data),
224        (OutputFormat::Human, CommandSuccess::IntegrationActivitiesChanged(data)) => {
225            integrations::activities(data)
226        }
227        (OutputFormat::Json, CommandSuccess::IntegrationActivitiesChanged(data)) => {
228            render_json_success(data)
229        }
230        (OutputFormat::Human, CommandSuccess::BoardCopied(data)) => {
231            format!(
232                "Board copy acknowledged; effects are unconfirmed:\n{}",
233                formatting::render_object(data)
234            )
235        }
236        (OutputFormat::Json, CommandSuccess::BoardCopied(data)) => render_json_success(data),
237        (OutputFormat::Human, CommandSuccess::CardCopied(data)) => {
238            format!(
239                "Card copy acknowledged; effects are unconfirmed:\n{}",
240                formatting::render_object(data)
241            )
242        }
243        (OutputFormat::Json, CommandSuccess::CardCopied(data)) => render_json_success(data),
244        (
245            OutputFormat::Human,
246            CommandSuccess::ListCopied(data) | CommandSuccess::ListMoved(data),
247        ) => format!(
248            "List transfer acknowledged; effects are unconfirmed:\n{}",
249            formatting::render_object(data)
250        ),
251        (
252            OutputFormat::Json,
253            CommandSuccess::ListCopied(data) | CommandSuccess::ListMoved(data),
254        ) => render_json_success(data),
255        (
256            OutputFormat::Human,
257            CommandSuccess::SwimlaneCopied(data) | CommandSuccess::SwimlaneMoved(data),
258        ) => format!(
259            "Swimlane transfer acknowledged; effects are unconfirmed:\n{}",
260            formatting::render_object(data)
261        ),
262        (
263            OutputFormat::Json,
264            CommandSuccess::SwimlaneCopied(data) | CommandSuccess::SwimlaneMoved(data),
265        ) => render_json_success(data),
266        (OutputFormat::Human, CommandSuccess::CardCount(data)) => cards::render_card_count(data),
267        (OutputFormat::Json, CommandSuccess::CardCount(data)) => render_json_success(data),
268        (OutputFormat::Human, CommandSuccess::SwimlaneCards(data)) => {
269            cards::render_swimlane_cards(data)
270        }
271        (OutputFormat::Json, CommandSuccess::SwimlaneCards(data)) => render_json_success(data),
272        (OutputFormat::Human, CommandSuccess::CardBatch(data)) => cards::render_batch(data),
273        (OutputFormat::Json, CommandSuccess::CardBatch(data)) => match data {
274            crate::command_result::CardBatchResult::CardBulkCreate(data) => {
275                render_json_success(data)
276            }
277            crate::command_result::CardBatchResult::CardBulkDelete(data) => {
278                render_json_success(data)
279            }
280            crate::command_result::CardBatchResult::CardBulkLabels(data) => {
281                render_json_success(data)
282            }
283        },
284        (OutputFormat::Human, CommandSuccess::BoardLabels(data)) => formatting::render_object(data),
285        (OutputFormat::Json, CommandSuccess::BoardLabels(data)) => render_json_success(data),
286        (OutputFormat::Human, CommandSuccess::BoardLabelAdded(data)) => {
287            boards::render_label_added(data)
288        }
289        (OutputFormat::Json, CommandSuccess::BoardLabelAdded(data)) => render_json_success(data),
290        (OutputFormat::Human, CommandSuccess::CardLabels(data)) => formatting::render_object(data),
291        (OutputFormat::Json, CommandSuccess::CardLabels(data)) => render_json_success(data),
292        (OutputFormat::Human, CommandSuccess::BoardMembers(data)) => {
293            formatting::render_object(data)
294        }
295        (OutputFormat::Json, CommandSuccess::BoardMembers(data)) => render_json_success(data),
296        (OutputFormat::Human, CommandSuccess::BoardMemberChanged(data)) => {
297            formatting::render_object(data)
298        }
299        (OutputFormat::Json, CommandSuccess::BoardMemberChanged(data)) => render_json_success(data),
300        (OutputFormat::Human, CommandSuccess::BoardMemberUpdated(data)) => {
301            formatting::render_object(data)
302        }
303        (OutputFormat::Json, CommandSuccess::BoardMemberUpdated(data)) => render_json_success(data),
304        (OutputFormat::Human, CommandSuccess::CardMembers(data)) => formatting::render_object(data),
305        (OutputFormat::Json, CommandSuccess::CardMembers(data)) => render_json_success(data),
306        (OutputFormat::Human, CommandSuccess::CardAssignees(data)) => {
307            formatting::render_object(data)
308        }
309        (OutputFormat::Json, CommandSuccess::CardAssignees(data)) => render_json_success(data),
310        (OutputFormat::Human, CommandSuccess::CardMembershipChanged(data)) => {
311            formatting::render_object(data)
312        }
313        (OutputFormat::Json, CommandSuccess::CardMembershipChanged(data)) => {
314            render_json_success(data)
315        }
316        (OutputFormat::Human, CommandSuccess::CardDependencies(data)) => {
317            dependencies::card_collection(data)
318        }
319        (OutputFormat::Json, CommandSuccess::CardDependencies(data)) => render_json_success(data),
320        (OutputFormat::Human, CommandSuccess::BoardDependencies(data)) => {
321            dependencies::board_collection(data)
322        }
323        (OutputFormat::Json, CommandSuccess::BoardDependencies(data)) => render_json_success(data),
324        (OutputFormat::Human, CommandSuccess::DependencyCreated(data)) => {
325            dependencies::created(data)
326        }
327        (OutputFormat::Json, CommandSuccess::DependencyCreated(data)) => render_json_success(data),
328        (OutputFormat::Human, CommandSuccess::DependencyUpdated(data)) => {
329            dependencies::updated(data)
330        }
331        (OutputFormat::Json, CommandSuccess::DependencyUpdated(data)) => render_json_success(data),
332        (OutputFormat::Human, CommandSuccess::DependencyDeleted(data)) => {
333            dependencies::deleted(data)
334        }
335        (OutputFormat::Json, CommandSuccess::DependencyDeleted(data)) => render_json_success(data),
336        (OutputFormat::Human, CommandSuccess::AttachmentList(data)) => attachments::list(data),
337        (OutputFormat::Json, CommandSuccess::AttachmentList(data)) => render_json_success(data),
338        (OutputFormat::Human, CommandSuccess::AttachmentShown(data)) => attachments::detail(data),
339        (OutputFormat::Json, CommandSuccess::AttachmentShown(data)) => render_json_success(data),
340        (OutputFormat::Human, CommandSuccess::AttachmentUploaded(data)) => {
341            attachments::detail(data)
342        }
343        (OutputFormat::Json, CommandSuccess::AttachmentUploaded(data)) => render_json_success(data),
344        (OutputFormat::Human, CommandSuccess::BackgroundUploaded(data)) => {
345            attachments::detail(data)
346        }
347        (OutputFormat::Json, CommandSuccess::BackgroundUploaded(data)) => render_json_success(data),
348        (OutputFormat::Human, CommandSuccess::AttachmentCopied(data)) => attachments::detail(data),
349        (OutputFormat::Json, CommandSuccess::AttachmentCopied(data)) => render_json_success(data),
350        (OutputFormat::Human, CommandSuccess::AttachmentMoved(data)) => attachments::detail(data),
351        (OutputFormat::Json, CommandSuccess::AttachmentMoved(data)) => render_json_success(data),
352        (OutputFormat::Human, CommandSuccess::AttachmentDeleted(data)) => attachments::detail(data),
353        (OutputFormat::Json, CommandSuccess::AttachmentDeleted(data)) => render_json_success(data),
354        (OutputFormat::Human, CommandSuccess::AttachmentDownloaded(data)) => {
355            attachments::detail(data)
356        }
357        (OutputFormat::Json, CommandSuccess::AttachmentDownloaded(data)) => {
358            render_json_success(data)
359        }
360        (_, CommandSuccess::ApiResponse(_)) => {
361            panic!("streaming API responses must be consumed by output::write_success")
362        }
363        (OutputFormat::Raw, CommandSuccess::Cancelled(_)) => String::new(),
364        (OutputFormat::Raw, _) => panic!("raw results must be consumed by output::write_success"),
365        (OutputFormat::Human, CommandSuccess::Cancelled(_)) => {
366            "Cancelled; no changes made.".to_owned()
367        }
368        (OutputFormat::Json, CommandSuccess::Cancelled(data)) => render_json_success(data),
369        (OutputFormat::Human, CommandSuccess::Registration(data)) => {
370            render_auth_success("Registered user", data)
371        }
372        (OutputFormat::Json, CommandSuccess::Registration(data)) => render_json_success(data),
373        (OutputFormat::Human, CommandSuccess::Login(data)) => {
374            render_auth_success("Logged in user", data)
375        }
376        (OutputFormat::Json, CommandSuccess::Login(data)) => render_json_success(data),
377        (OutputFormat::Human, CommandSuccess::Logout(data)) => render_logout(data),
378        (OutputFormat::Json, CommandSuccess::Logout(data)) => render_json_success(data),
379        (OutputFormat::Human, CommandSuccess::AuthStatus(data)) => render_auth_status(data),
380        (OutputFormat::Json, CommandSuccess::AuthStatus(data)) => render_json_success(data),
381        (OutputFormat::Human, CommandSuccess::UserCurrent(data))
382        | (OutputFormat::Human, CommandSuccess::UserShown(data))
383        | (OutputFormat::Human, CommandSuccess::UserUpdated(data)) => render_user_detail(data),
384        (OutputFormat::Json, CommandSuccess::UserCurrent(data))
385        | (OutputFormat::Json, CommandSuccess::UserShown(data))
386        | (OutputFormat::Json, CommandSuccess::UserUpdated(data)) => render_json_success(data),
387        (OutputFormat::Human, CommandSuccess::UserList(data)) => render_user_list(data),
388        (OutputFormat::Json, CommandSuccess::UserList(data)) => render_json_success(data),
389        (OutputFormat::Human, CommandSuccess::UserCards(data)) => render_user_cards(data),
390        (OutputFormat::Json, CommandSuccess::UserCards(data)) => render_json_success(data),
391        (OutputFormat::Human, CommandSuccess::UserCreated(data)) => render_user_created(data),
392        (OutputFormat::Json, CommandSuccess::UserCreated(data)) => render_json_success(data),
393        (OutputFormat::Human, CommandSuccess::UserBoards(data)) => render_user_boards(data),
394        (OutputFormat::Json, CommandSuccess::UserBoards(data)) => render_json_success(data),
395        (OutputFormat::Human, CommandSuccess::UserOwnershipTaken(data)) => {
396            render_user_ownership(data)
397        }
398        (OutputFormat::Json, CommandSuccess::UserOwnershipTaken(data)) => render_json_success(data),
399        (OutputFormat::Human, CommandSuccess::UserLoginChanged(data)) => {
400            render_user_login_change(data)
401        }
402        (OutputFormat::Json, CommandSuccess::UserLoginChanged(data)) => render_json_success(data),
403        (OutputFormat::Human, CommandSuccess::UserDeleted(data)) => render_user_deleted(data),
404        (OutputFormat::Json, CommandSuccess::UserDeleted(data)) => render_json_success(data),
405        (OutputFormat::Human, CommandSuccess::BoardList(data)) => render_board_list(data),
406        (OutputFormat::Json, CommandSuccess::BoardList(data)) => render_json_success(data),
407        (OutputFormat::Human, CommandSuccess::BoardCount(data)) => render_board_count(data),
408        (OutputFormat::Json, CommandSuccess::BoardCount(data)) => render_json_success(data),
409        (OutputFormat::Human, CommandSuccess::BoardShown(data)) => render_board_detail(data),
410        (OutputFormat::Json, CommandSuccess::BoardShown(data)) => render_json_success(data),
411        (OutputFormat::Human, CommandSuccess::BoardCreated(data)) => render_board_created(data),
412        (OutputFormat::Json, CommandSuccess::BoardCreated(data)) => render_json_success(data),
413        (OutputFormat::Human, CommandSuccess::BoardRenamed(data)) => render_board_renamed(data),
414        (OutputFormat::Json, CommandSuccess::BoardRenamed(data)) => render_json_success(data),
415        (OutputFormat::Human, CommandSuccess::BoardDeleted(data)) => render_board_deleted(data),
416        (OutputFormat::Json, CommandSuccess::BoardDeleted(data)) => render_json_success(data),
417        (OutputFormat::Human, CommandSuccess::ListCollection(data)) => render_list_collection(data),
418        (OutputFormat::Json, CommandSuccess::ListCollection(data)) => render_json_success(data),
419        (OutputFormat::Human, CommandSuccess::ListShown(data)) => render_list_detail(data),
420        (OutputFormat::Json, CommandSuccess::ListShown(data)) => render_json_success(data),
421        (OutputFormat::Human, CommandSuccess::ListCreated(data)) => render_list_created(data),
422        (OutputFormat::Json, CommandSuccess::ListCreated(data)) => render_json_success(data),
423        (OutputFormat::Human, CommandSuccess::ListUpdated(data)) => render_list_updated(data),
424        (OutputFormat::Json, CommandSuccess::ListUpdated(data)) => render_json_success(data),
425        (OutputFormat::Human, CommandSuccess::ListDeleted(data)) => render_list_deleted(data),
426        (OutputFormat::Json, CommandSuccess::ListDeleted(data)) => render_json_success(data),
427        (OutputFormat::Human, CommandSuccess::CardCollection(data)) => render_card_collection(data),
428        (OutputFormat::Json, CommandSuccess::CardCollection(data)) => render_json_success(data),
429        (OutputFormat::Human, CommandSuccess::CardShown(data)) => render_card_detail(data),
430        (OutputFormat::Json, CommandSuccess::CardShown(data)) => render_json_success(data),
431        (OutputFormat::Human, CommandSuccess::CardCreated(data)) => render_card_created(data),
432        (OutputFormat::Json, CommandSuccess::CardCreated(data)) => render_json_success(data),
433        (OutputFormat::Human, CommandSuccess::CardUpdated(data)) => render_card_updated(data),
434        (OutputFormat::Json, CommandSuccess::CardUpdated(data)) => render_json_success(data),
435        (OutputFormat::Human, CommandSuccess::CardArchived(data)) => render_card_archived(data),
436        (OutputFormat::Json, CommandSuccess::CardArchived(data)) => render_json_success(data),
437        (OutputFormat::Human, CommandSuccess::CardRestored(data)) => render_card_restored(data),
438        (OutputFormat::Json, CommandSuccess::CardRestored(data)) => render_json_success(data),
439        (OutputFormat::Human, CommandSuccess::CardDeleted(data)) => render_card_deleted(data),
440        (OutputFormat::Json, CommandSuccess::CardDeleted(data)) => render_json_success(data),
441        (OutputFormat::Human, CommandSuccess::ChecklistCollection(data)) => {
442            render_checklist_collection(data)
443        }
444        (OutputFormat::Json, CommandSuccess::ChecklistCollection(data)) => {
445            render_json_success(data)
446        }
447        (OutputFormat::Human, CommandSuccess::ChecklistShown(data)) => {
448            render_checklist_detail(data)
449        }
450        (OutputFormat::Json, CommandSuccess::ChecklistShown(data)) => render_json_success(data),
451        (OutputFormat::Human, CommandSuccess::ChecklistCreated(data)) => {
452            render_checklist_created(data)
453        }
454        (OutputFormat::Json, CommandSuccess::ChecklistCreated(data)) => render_json_success(data),
455        (OutputFormat::Human, CommandSuccess::ChecklistDeleted(data)) => {
456            render_checklist_deleted(data)
457        }
458        (OutputFormat::Json, CommandSuccess::ChecklistDeleted(data)) => render_json_success(data),
459        (OutputFormat::Human, CommandSuccess::ChecklistItemShown(data)) => {
460            render_checklist_item_detail(data)
461        }
462        (OutputFormat::Json, CommandSuccess::ChecklistItemShown(data)) => render_json_success(data),
463        (OutputFormat::Human, CommandSuccess::ChecklistItemCreated(data)) => {
464            render_checklist_item_created(data)
465        }
466        (OutputFormat::Json, CommandSuccess::ChecklistItemCreated(data)) => {
467            render_json_success(data)
468        }
469        (OutputFormat::Human, CommandSuccess::ChecklistItemUpdated(data)) => {
470            render_checklist_item_updated(data)
471        }
472        (OutputFormat::Json, CommandSuccess::ChecklistItemUpdated(data)) => {
473            render_json_success(data)
474        }
475        (OutputFormat::Human, CommandSuccess::ChecklistItemDeleted(data)) => {
476            render_checklist_item_deleted(data)
477        }
478        (OutputFormat::Json, CommandSuccess::ChecklistItemDeleted(data)) => {
479            render_json_success(data)
480        }
481        (OutputFormat::Human, CommandSuccess::CustomFieldCollection(data)) => {
482            custom_fields::collection(data)
483        }
484        (OutputFormat::Json, CommandSuccess::CustomFieldCollection(data)) => {
485            render_json_success(data)
486        }
487        (OutputFormat::Human, CommandSuccess::CustomFieldShown(data)) => {
488            custom_fields::detail(data)
489        }
490        (OutputFormat::Json, CommandSuccess::CustomFieldShown(data)) => render_json_success(data),
491        (OutputFormat::Human, CommandSuccess::CustomFieldCreated(data)) => {
492            custom_fields::created(data)
493        }
494        (OutputFormat::Json, CommandSuccess::CustomFieldCreated(data)) => render_json_success(data),
495        (OutputFormat::Human, CommandSuccess::CustomFieldUpdated(data)) => {
496            custom_fields::updated(data)
497        }
498        (OutputFormat::Json, CommandSuccess::CustomFieldUpdated(data)) => render_json_success(data),
499        (OutputFormat::Human, CommandSuccess::CustomFieldDeleted(data)) => {
500            custom_fields::deleted(data)
501        }
502        (OutputFormat::Json, CommandSuccess::CustomFieldDeleted(data)) => render_json_success(data),
503        (OutputFormat::Human, CommandSuccess::CustomFieldItemsCreated(data)) => {
504            custom_field_items::created(data)
505        }
506        (OutputFormat::Json, CommandSuccess::CustomFieldItemsCreated(data)) => {
507            render_json_success(data)
508        }
509        (OutputFormat::Human, CommandSuccess::CustomFieldItemUpdated(data)) => {
510            custom_field_items::updated(data)
511        }
512        (OutputFormat::Json, CommandSuccess::CustomFieldItemUpdated(data)) => {
513            render_json_success(data)
514        }
515        (OutputFormat::Human, CommandSuccess::CustomFieldItemDeleted(data)) => {
516            custom_field_items::deleted(data)
517        }
518        (OutputFormat::Json, CommandSuccess::CustomFieldItemDeleted(data)) => {
519            render_json_success(data)
520        }
521        (OutputFormat::Human, CommandSuccess::CardCustomFieldSet(data)) => {
522            cards::render_card_custom_field_set(data)
523        }
524        (OutputFormat::Json, CommandSuccess::CardCustomFieldSet(data)) => render_json_success(data),
525        (OutputFormat::Human, CommandSuccess::CardCustomFieldQuery(data)) => {
526            cards::render_card_custom_field_query(data)
527        }
528        (OutputFormat::Json, CommandSuccess::CardCustomFieldQuery(data)) => {
529            render_json_success(data)
530        }
531        (OutputFormat::Human, CommandSuccess::CommentCollection(data)) => {
532            render_comment_collection(data)
533        }
534        (OutputFormat::Json, CommandSuccess::CommentCollection(data)) => render_json_success(data),
535        (OutputFormat::Human, CommandSuccess::CommentShown(data)) => render_comment_detail(data),
536        (OutputFormat::Json, CommandSuccess::CommentShown(data)) => render_json_success(data),
537        (OutputFormat::Human, CommandSuccess::CommentCreated(data)) => render_comment_created(data),
538        (OutputFormat::Json, CommandSuccess::CommentCreated(data)) => render_json_success(data),
539        (OutputFormat::Human, CommandSuccess::CommentDeleted(data)) => render_comment_deleted(data),
540        (OutputFormat::Json, CommandSuccess::CommentDeleted(data)) => render_json_success(data),
541        (OutputFormat::Human, CommandSuccess::SwimlaneCollection(data)) => {
542            render_swimlane_collection(data)
543        }
544        (OutputFormat::Json, CommandSuccess::SwimlaneCollection(data)) => render_json_success(data),
545        (OutputFormat::Human, CommandSuccess::SwimlaneShown(data)) => render_swimlane_detail(data),
546        (OutputFormat::Json, CommandSuccess::SwimlaneShown(data)) => render_json_success(data),
547        (OutputFormat::Human, CommandSuccess::SwimlaneCreated(data)) => {
548            render_swimlane_created(data)
549        }
550        (OutputFormat::Json, CommandSuccess::SwimlaneCreated(data)) => render_json_success(data),
551        (OutputFormat::Human, CommandSuccess::SwimlaneUpdated(data)) => {
552            render_swimlane_updated(data)
553        }
554        (OutputFormat::Json, CommandSuccess::SwimlaneUpdated(data)) => render_json_success(data),
555        (OutputFormat::Human, CommandSuccess::SwimlaneDeleted(data)) => {
556            render_swimlane_deleted(data)
557        }
558        (OutputFormat::Json, CommandSuccess::SwimlaneDeleted(data)) => render_json_success(data),
559        (OutputFormat::Human, CommandSuccess::ProfileAdded(data)) => {
560            render_profile_item("Added profile", data)
561        }
562        (OutputFormat::Json, CommandSuccess::ProfileAdded(data))
563        | (OutputFormat::Json, CommandSuccess::ProfileShown(data))
564        | (OutputFormat::Json, CommandSuccess::ProfileUsed(data))
565        | (OutputFormat::Json, CommandSuccess::ProfileUpdated(data)) => {
566            serde_json::to_string(&SuccessEnvelope { ok: true, data })
567                .expect("profile success is always serializable")
568        }
569        (OutputFormat::Human, CommandSuccess::ProfileList(data)) => render_profile_list(data),
570        (OutputFormat::Json, CommandSuccess::ProfileList(data)) => {
571            serde_json::to_string(&SuccessEnvelope { ok: true, data })
572                .expect("profile list success is always serializable")
573        }
574        (OutputFormat::Human, CommandSuccess::ProfileShown(data)) => {
575            render_profile_item("Profile", data)
576        }
577        (OutputFormat::Human, CommandSuccess::ProfileUsed(data)) => {
578            render_profile_item("Using profile", data)
579        }
580        (OutputFormat::Human, CommandSuccess::ProfileUpdated(data)) => {
581            render_profile_item("Updated profile", data)
582        }
583        (OutputFormat::Human, CommandSuccess::ProfileRemoved(data)) => render_profile_removed(data),
584        (OutputFormat::Json, CommandSuccess::ProfileRemoved(data)) => {
585            serde_json::to_string(&SuccessEnvelope { ok: true, data })
586                .expect("profile removal success is always serializable")
587        }
588    }
589}
590
591pub async fn write_success(
592    format: OutputFormat,
593    success: CommandSuccess,
594    stdout: &mut dyn Write,
595    stderr: &mut dyn Write,
596) -> Result<ExitCode, AppError> {
597    if let CommandSuccess::ApiResponse(response) = success {
598        return raw::write_response(format, response, stdout, stderr).await;
599    }
600    if format == OutputFormat::Raw {
601        if let CommandSuccess::AttachmentDownloaded(data) = &success {
602            let bytes = data.bytes.as_deref().ok_or_else(|| {
603                AppError::invalid_input("raw attachment output requires --destination -")
604            })?;
605            stdout
606                .write_all(bytes)
607                .and_then(|()| stdout.flush())
608                .map_err(output_write_error)?;
609            return Ok(ExitCode::SUCCESS);
610        }
611    }
612    let rendered = render_success(format, &success);
613    if !rendered.is_empty() {
614        let written = writeln!(stdout, "{rendered}").and_then(|()| {
615            if matches!(&success, CommandSuccess::AttachmentDownloaded(data) if data.file_saved) {
616                stdout.flush()
617            } else {
618                Ok(())
619            }
620        });
621        written.map_err(|e| {
622            let error = output_write_error(e);
623            if let CommandSuccess::AttachmentDownloaded(data) = &success {
624                if data.file_saved {
625                    return error.with_details(crate::error::ErrorDetails {
626                        file_saved: Some(true),
627                        destination: Some(data.destination.clone()),
628                        ..crate::error::ErrorDetails::default()
629                    });
630                }
631            }
632            error
633        })?;
634    }
635    Ok(ExitCode::SUCCESS)
636}
637pub(crate) fn output_write_error(error: std::io::Error) -> AppError {
638    AppError::new(
639        ErrorCode::InternalError,
640        format!("could not write command output: {error}"),
641        StableExitCode::Transport,
642    )
643}
644
645pub(crate) fn render_json_success<T: Serialize>(data: &T) -> String {
646    serde_json::to_string(&SuccessEnvelope { ok: true, data })
647        .expect("command success is always serializable")
648}
649
650pub fn render_error(format: OutputFormat, error: &AppError) -> String {
651    match format {
652        OutputFormat::Human => {
653            let mut rendered = format!(
654                "Error [{}]: {}",
655                error.code().as_str(),
656                error.message().trim()
657            );
658            if let Some(batch) = &error.details().batch_result {
659                rendered.push('\n');
660                rendered.push_str(&cards::render_batch(batch));
661            }
662            if let Some(ids) = &error.details().invalid_label_ids {
663                rendered.push_str(&format!(
664                    "\nInvalid label IDs: {}",
665                    formatting::escape_terminal_controls(&ids.join(", "))
666                ));
667            }
668            if let Some(response) = &error.details().response {
669                rendered.push('\n');
670                rendered.push_str(&render_api_response_human(response));
671            }
672            if error.details().file_saved == Some(true) {
673                if let Some(destination) = &error.details().destination {
674                    rendered.push_str(&format!(
675                        "\nDownload saved to `{}`; completion output failed.",
676                        formatting::escape_terminal_controls(destination)
677                    ));
678                }
679            }
680            rendered
681        }
682        OutputFormat::Json => serde_json::to_string(&ErrorEnvelope {
683            ok: false,
684            error: ErrorBody {
685                code: error.code().as_str(),
686                message: error.message(),
687                details: error.details(),
688            },
689        })
690        .expect("application errors are always serializable"),
691        OutputFormat::Raw => format!(
692            "Error [{}]: {}",
693            error.code().as_str(),
694            error.message().trim()
695        ),
696    }
697}
698
699#[cfg(test)]
700mod tests;