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::BoardCardSettings(data)) => {
123            formatting::render_object(data)
124        }
125        (OutputFormat::Json, CommandSuccess::BoardCardSettings(data)) => render_json_success(data),
126        (OutputFormat::Human, CommandSuccess::BoardCardSettingsUpdated(data)) => {
127            formatting::render_object(data)
128        }
129        (OutputFormat::Json, CommandSuccess::BoardCardSettingsUpdated(data)) => {
130            render_json_success(data)
131        }
132        (OutputFormat::Human, CommandSuccess::BoardDomains(data)) => {
133            formatting::render_object(data)
134        }
135        (OutputFormat::Json, CommandSuccess::BoardDomains(data)) => render_json_success(data),
136        (OutputFormat::Human, CommandSuccess::BoardDomainChanged(data)) => format!(
137            "Board domains returned for the requested sharing change:\n{}",
138            formatting::render_object(data)
139        ),
140        (OutputFormat::Json, CommandSuccess::BoardDomainChanged(data)) => render_json_success(data),
141        (OutputFormat::Human, CommandSuccess::RuleCollection(data)) => rules::collection(data),
142        (OutputFormat::Json, CommandSuccess::RuleCollection(data)) => render_json_success(data),
143        (OutputFormat::Human, CommandSuccess::RuleShown(data)) => rules::detail(data),
144        (OutputFormat::Json, CommandSuccess::RuleShown(data)) => render_json_success(data),
145        (OutputFormat::Human, CommandSuccess::RuleCreated(data)) => rules::created(data),
146        (OutputFormat::Json, CommandSuccess::RuleCreated(data)) => render_json_success(data),
147        (OutputFormat::Human, CommandSuccess::RuleUpdated(data)) => rules::updated(data),
148        (OutputFormat::Json, CommandSuccess::RuleUpdated(data)) => render_json_success(data),
149        (OutputFormat::Human, CommandSuccess::RuleDeleted(data)) => rules::deleted(data),
150        (OutputFormat::Json, CommandSuccess::RuleDeleted(data)) => render_json_success(data),
151        (OutputFormat::Human, CommandSuccess::IntegrationCollection(data)) => {
152            integrations::collection(data)
153        }
154        (OutputFormat::Json, CommandSuccess::IntegrationCollection(data)) => {
155            render_json_success(data)
156        }
157        (OutputFormat::Human, CommandSuccess::IntegrationShown(data)) => integrations::detail(data),
158        (OutputFormat::Json, CommandSuccess::IntegrationShown(data)) => render_json_success(data),
159        (OutputFormat::Human, CommandSuccess::IntegrationCreated(data)) => {
160            integrations::created(data)
161        }
162        (OutputFormat::Json, CommandSuccess::IntegrationCreated(data)) => render_json_success(data),
163        (OutputFormat::Human, CommandSuccess::IntegrationUpdated(data)) => {
164            integrations::updated(data)
165        }
166        (OutputFormat::Json, CommandSuccess::IntegrationUpdated(data)) => render_json_success(data),
167        (OutputFormat::Human, CommandSuccess::IntegrationDeleted(data)) => {
168            integrations::deleted(data)
169        }
170        (OutputFormat::Json, CommandSuccess::IntegrationDeleted(data)) => render_json_success(data),
171        (OutputFormat::Human, CommandSuccess::IntegrationActivitiesChanged(data)) => {
172            integrations::activities(data)
173        }
174        (OutputFormat::Json, CommandSuccess::IntegrationActivitiesChanged(data)) => {
175            render_json_success(data)
176        }
177        (OutputFormat::Human, CommandSuccess::BoardCopied(data)) => {
178            format!(
179                "Board copy acknowledged; effects are unconfirmed:\n{}",
180                formatting::render_object(data)
181            )
182        }
183        (OutputFormat::Json, CommandSuccess::BoardCopied(data)) => render_json_success(data),
184        (OutputFormat::Human, CommandSuccess::CardCopied(data)) => {
185            format!(
186                "Card copy acknowledged; effects are unconfirmed:\n{}",
187                formatting::render_object(data)
188            )
189        }
190        (OutputFormat::Json, CommandSuccess::CardCopied(data)) => render_json_success(data),
191        (
192            OutputFormat::Human,
193            CommandSuccess::ListCopied(data) | CommandSuccess::ListMoved(data),
194        ) => format!(
195            "List transfer acknowledged; effects are unconfirmed:\n{}",
196            formatting::render_object(data)
197        ),
198        (
199            OutputFormat::Json,
200            CommandSuccess::ListCopied(data) | CommandSuccess::ListMoved(data),
201        ) => render_json_success(data),
202        (
203            OutputFormat::Human,
204            CommandSuccess::SwimlaneCopied(data) | CommandSuccess::SwimlaneMoved(data),
205        ) => format!(
206            "Swimlane transfer acknowledged; effects are unconfirmed:\n{}",
207            formatting::render_object(data)
208        ),
209        (
210            OutputFormat::Json,
211            CommandSuccess::SwimlaneCopied(data) | CommandSuccess::SwimlaneMoved(data),
212        ) => render_json_success(data),
213        (OutputFormat::Human, CommandSuccess::CardCount(data)) => cards::render_card_count(data),
214        (OutputFormat::Json, CommandSuccess::CardCount(data)) => render_json_success(data),
215        (OutputFormat::Human, CommandSuccess::SwimlaneCards(data)) => {
216            cards::render_swimlane_cards(data)
217        }
218        (OutputFormat::Json, CommandSuccess::SwimlaneCards(data)) => render_json_success(data),
219        (OutputFormat::Human, CommandSuccess::CardBatch(data)) => cards::render_batch(data),
220        (OutputFormat::Json, CommandSuccess::CardBatch(data)) => match data {
221            crate::command_result::CardBatchResult::CardBulkCreate(data) => {
222                render_json_success(data)
223            }
224            crate::command_result::CardBatchResult::CardBulkDelete(data) => {
225                render_json_success(data)
226            }
227            crate::command_result::CardBatchResult::CardBulkLabels(data) => {
228                render_json_success(data)
229            }
230        },
231        (OutputFormat::Human, CommandSuccess::BoardLabels(data)) => formatting::render_object(data),
232        (OutputFormat::Json, CommandSuccess::BoardLabels(data)) => render_json_success(data),
233        (OutputFormat::Human, CommandSuccess::BoardLabelAdded(data)) => {
234            boards::render_label_added(data)
235        }
236        (OutputFormat::Json, CommandSuccess::BoardLabelAdded(data)) => render_json_success(data),
237        (OutputFormat::Human, CommandSuccess::CardLabels(data)) => formatting::render_object(data),
238        (OutputFormat::Json, CommandSuccess::CardLabels(data)) => render_json_success(data),
239        (OutputFormat::Human, CommandSuccess::BoardMembers(data)) => {
240            formatting::render_object(data)
241        }
242        (OutputFormat::Json, CommandSuccess::BoardMembers(data)) => render_json_success(data),
243        (OutputFormat::Human, CommandSuccess::BoardMemberChanged(data)) => {
244            formatting::render_object(data)
245        }
246        (OutputFormat::Json, CommandSuccess::BoardMemberChanged(data)) => render_json_success(data),
247        (OutputFormat::Human, CommandSuccess::BoardMemberUpdated(data)) => {
248            formatting::render_object(data)
249        }
250        (OutputFormat::Json, CommandSuccess::BoardMemberUpdated(data)) => render_json_success(data),
251        (OutputFormat::Human, CommandSuccess::CardMembers(data)) => formatting::render_object(data),
252        (OutputFormat::Json, CommandSuccess::CardMembers(data)) => render_json_success(data),
253        (OutputFormat::Human, CommandSuccess::CardAssignees(data)) => {
254            formatting::render_object(data)
255        }
256        (OutputFormat::Json, CommandSuccess::CardAssignees(data)) => render_json_success(data),
257        (OutputFormat::Human, CommandSuccess::CardMembershipChanged(data)) => {
258            formatting::render_object(data)
259        }
260        (OutputFormat::Json, CommandSuccess::CardMembershipChanged(data)) => {
261            render_json_success(data)
262        }
263        (OutputFormat::Human, CommandSuccess::CardDependencies(data)) => {
264            dependencies::card_collection(data)
265        }
266        (OutputFormat::Json, CommandSuccess::CardDependencies(data)) => render_json_success(data),
267        (OutputFormat::Human, CommandSuccess::BoardDependencies(data)) => {
268            dependencies::board_collection(data)
269        }
270        (OutputFormat::Json, CommandSuccess::BoardDependencies(data)) => render_json_success(data),
271        (OutputFormat::Human, CommandSuccess::DependencyCreated(data)) => {
272            dependencies::created(data)
273        }
274        (OutputFormat::Json, CommandSuccess::DependencyCreated(data)) => render_json_success(data),
275        (OutputFormat::Human, CommandSuccess::DependencyUpdated(data)) => {
276            dependencies::updated(data)
277        }
278        (OutputFormat::Json, CommandSuccess::DependencyUpdated(data)) => render_json_success(data),
279        (OutputFormat::Human, CommandSuccess::DependencyDeleted(data)) => {
280            dependencies::deleted(data)
281        }
282        (OutputFormat::Json, CommandSuccess::DependencyDeleted(data)) => render_json_success(data),
283        (OutputFormat::Human, CommandSuccess::AttachmentList(data)) => attachments::list(data),
284        (OutputFormat::Json, CommandSuccess::AttachmentList(data)) => render_json_success(data),
285        (OutputFormat::Human, CommandSuccess::AttachmentShown(data)) => attachments::detail(data),
286        (OutputFormat::Json, CommandSuccess::AttachmentShown(data)) => render_json_success(data),
287        (OutputFormat::Human, CommandSuccess::AttachmentUploaded(data)) => {
288            attachments::detail(data)
289        }
290        (OutputFormat::Json, CommandSuccess::AttachmentUploaded(data)) => render_json_success(data),
291        (OutputFormat::Human, CommandSuccess::BackgroundUploaded(data)) => {
292            attachments::detail(data)
293        }
294        (OutputFormat::Json, CommandSuccess::BackgroundUploaded(data)) => render_json_success(data),
295        (OutputFormat::Human, CommandSuccess::AttachmentCopied(data)) => attachments::detail(data),
296        (OutputFormat::Json, CommandSuccess::AttachmentCopied(data)) => render_json_success(data),
297        (OutputFormat::Human, CommandSuccess::AttachmentMoved(data)) => attachments::detail(data),
298        (OutputFormat::Json, CommandSuccess::AttachmentMoved(data)) => render_json_success(data),
299        (OutputFormat::Human, CommandSuccess::AttachmentDeleted(data)) => attachments::detail(data),
300        (OutputFormat::Json, CommandSuccess::AttachmentDeleted(data)) => render_json_success(data),
301        (OutputFormat::Human, CommandSuccess::AttachmentDownloaded(data)) => {
302            attachments::detail(data)
303        }
304        (OutputFormat::Json, CommandSuccess::AttachmentDownloaded(data)) => {
305            render_json_success(data)
306        }
307        (_, CommandSuccess::ApiResponse(_)) => {
308            panic!("streaming API responses must be consumed by output::write_success")
309        }
310        (OutputFormat::Raw, CommandSuccess::Cancelled(_)) => String::new(),
311        (OutputFormat::Raw, _) => panic!("raw results must be consumed by output::write_success"),
312        (OutputFormat::Human, CommandSuccess::Cancelled(_)) => {
313            "Cancelled; no changes made.".to_owned()
314        }
315        (OutputFormat::Json, CommandSuccess::Cancelled(data)) => render_json_success(data),
316        (OutputFormat::Human, CommandSuccess::Registration(data)) => {
317            render_auth_success("Registered user", data)
318        }
319        (OutputFormat::Json, CommandSuccess::Registration(data)) => render_json_success(data),
320        (OutputFormat::Human, CommandSuccess::Login(data)) => {
321            render_auth_success("Logged in user", data)
322        }
323        (OutputFormat::Json, CommandSuccess::Login(data)) => render_json_success(data),
324        (OutputFormat::Human, CommandSuccess::Logout(data)) => render_logout(data),
325        (OutputFormat::Json, CommandSuccess::Logout(data)) => render_json_success(data),
326        (OutputFormat::Human, CommandSuccess::AuthStatus(data)) => render_auth_status(data),
327        (OutputFormat::Json, CommandSuccess::AuthStatus(data)) => render_json_success(data),
328        (OutputFormat::Human, CommandSuccess::UserCurrent(data))
329        | (OutputFormat::Human, CommandSuccess::UserShown(data))
330        | (OutputFormat::Human, CommandSuccess::UserUpdated(data)) => render_user_detail(data),
331        (OutputFormat::Json, CommandSuccess::UserCurrent(data))
332        | (OutputFormat::Json, CommandSuccess::UserShown(data))
333        | (OutputFormat::Json, CommandSuccess::UserUpdated(data)) => render_json_success(data),
334        (OutputFormat::Human, CommandSuccess::UserList(data)) => render_user_list(data),
335        (OutputFormat::Json, CommandSuccess::UserList(data)) => render_json_success(data),
336        (OutputFormat::Human, CommandSuccess::UserCards(data)) => render_user_cards(data),
337        (OutputFormat::Json, CommandSuccess::UserCards(data)) => render_json_success(data),
338        (OutputFormat::Human, CommandSuccess::UserCreated(data)) => render_user_created(data),
339        (OutputFormat::Json, CommandSuccess::UserCreated(data)) => render_json_success(data),
340        (OutputFormat::Human, CommandSuccess::UserBoards(data)) => render_user_boards(data),
341        (OutputFormat::Json, CommandSuccess::UserBoards(data)) => render_json_success(data),
342        (OutputFormat::Human, CommandSuccess::UserOwnershipTaken(data)) => {
343            render_user_ownership(data)
344        }
345        (OutputFormat::Json, CommandSuccess::UserOwnershipTaken(data)) => render_json_success(data),
346        (OutputFormat::Human, CommandSuccess::UserLoginChanged(data)) => {
347            render_user_login_change(data)
348        }
349        (OutputFormat::Json, CommandSuccess::UserLoginChanged(data)) => render_json_success(data),
350        (OutputFormat::Human, CommandSuccess::UserDeleted(data)) => render_user_deleted(data),
351        (OutputFormat::Json, CommandSuccess::UserDeleted(data)) => render_json_success(data),
352        (OutputFormat::Human, CommandSuccess::BoardList(data)) => render_board_list(data),
353        (OutputFormat::Json, CommandSuccess::BoardList(data)) => render_json_success(data),
354        (OutputFormat::Human, CommandSuccess::BoardCount(data)) => render_board_count(data),
355        (OutputFormat::Json, CommandSuccess::BoardCount(data)) => render_json_success(data),
356        (OutputFormat::Human, CommandSuccess::BoardShown(data)) => render_board_detail(data),
357        (OutputFormat::Json, CommandSuccess::BoardShown(data)) => render_json_success(data),
358        (OutputFormat::Human, CommandSuccess::BoardCreated(data)) => render_board_created(data),
359        (OutputFormat::Json, CommandSuccess::BoardCreated(data)) => render_json_success(data),
360        (OutputFormat::Human, CommandSuccess::BoardRenamed(data)) => render_board_renamed(data),
361        (OutputFormat::Json, CommandSuccess::BoardRenamed(data)) => render_json_success(data),
362        (OutputFormat::Human, CommandSuccess::BoardDeleted(data)) => render_board_deleted(data),
363        (OutputFormat::Json, CommandSuccess::BoardDeleted(data)) => render_json_success(data),
364        (OutputFormat::Human, CommandSuccess::ListCollection(data)) => render_list_collection(data),
365        (OutputFormat::Json, CommandSuccess::ListCollection(data)) => render_json_success(data),
366        (OutputFormat::Human, CommandSuccess::ListShown(data)) => render_list_detail(data),
367        (OutputFormat::Json, CommandSuccess::ListShown(data)) => render_json_success(data),
368        (OutputFormat::Human, CommandSuccess::ListCreated(data)) => render_list_created(data),
369        (OutputFormat::Json, CommandSuccess::ListCreated(data)) => render_json_success(data),
370        (OutputFormat::Human, CommandSuccess::ListUpdated(data)) => render_list_updated(data),
371        (OutputFormat::Json, CommandSuccess::ListUpdated(data)) => render_json_success(data),
372        (OutputFormat::Human, CommandSuccess::ListDeleted(data)) => render_list_deleted(data),
373        (OutputFormat::Json, CommandSuccess::ListDeleted(data)) => render_json_success(data),
374        (OutputFormat::Human, CommandSuccess::CardCollection(data)) => render_card_collection(data),
375        (OutputFormat::Json, CommandSuccess::CardCollection(data)) => render_json_success(data),
376        (OutputFormat::Human, CommandSuccess::CardShown(data)) => render_card_detail(data),
377        (OutputFormat::Json, CommandSuccess::CardShown(data)) => render_json_success(data),
378        (OutputFormat::Human, CommandSuccess::CardCreated(data)) => render_card_created(data),
379        (OutputFormat::Json, CommandSuccess::CardCreated(data)) => render_json_success(data),
380        (OutputFormat::Human, CommandSuccess::CardUpdated(data)) => render_card_updated(data),
381        (OutputFormat::Json, CommandSuccess::CardUpdated(data)) => render_json_success(data),
382        (OutputFormat::Human, CommandSuccess::CardArchived(data)) => render_card_archived(data),
383        (OutputFormat::Json, CommandSuccess::CardArchived(data)) => render_json_success(data),
384        (OutputFormat::Human, CommandSuccess::CardRestored(data)) => render_card_restored(data),
385        (OutputFormat::Json, CommandSuccess::CardRestored(data)) => render_json_success(data),
386        (OutputFormat::Human, CommandSuccess::CardDeleted(data)) => render_card_deleted(data),
387        (OutputFormat::Json, CommandSuccess::CardDeleted(data)) => render_json_success(data),
388        (OutputFormat::Human, CommandSuccess::ChecklistCollection(data)) => {
389            render_checklist_collection(data)
390        }
391        (OutputFormat::Json, CommandSuccess::ChecklistCollection(data)) => {
392            render_json_success(data)
393        }
394        (OutputFormat::Human, CommandSuccess::ChecklistShown(data)) => {
395            render_checklist_detail(data)
396        }
397        (OutputFormat::Json, CommandSuccess::ChecklistShown(data)) => render_json_success(data),
398        (OutputFormat::Human, CommandSuccess::ChecklistCreated(data)) => {
399            render_checklist_created(data)
400        }
401        (OutputFormat::Json, CommandSuccess::ChecklistCreated(data)) => render_json_success(data),
402        (OutputFormat::Human, CommandSuccess::ChecklistDeleted(data)) => {
403            render_checklist_deleted(data)
404        }
405        (OutputFormat::Json, CommandSuccess::ChecklistDeleted(data)) => render_json_success(data),
406        (OutputFormat::Human, CommandSuccess::ChecklistItemShown(data)) => {
407            render_checklist_item_detail(data)
408        }
409        (OutputFormat::Json, CommandSuccess::ChecklistItemShown(data)) => render_json_success(data),
410        (OutputFormat::Human, CommandSuccess::ChecklistItemCreated(data)) => {
411            render_checklist_item_created(data)
412        }
413        (OutputFormat::Json, CommandSuccess::ChecklistItemCreated(data)) => {
414            render_json_success(data)
415        }
416        (OutputFormat::Human, CommandSuccess::ChecklistItemUpdated(data)) => {
417            render_checklist_item_updated(data)
418        }
419        (OutputFormat::Json, CommandSuccess::ChecklistItemUpdated(data)) => {
420            render_json_success(data)
421        }
422        (OutputFormat::Human, CommandSuccess::ChecklistItemDeleted(data)) => {
423            render_checklist_item_deleted(data)
424        }
425        (OutputFormat::Json, CommandSuccess::ChecklistItemDeleted(data)) => {
426            render_json_success(data)
427        }
428        (OutputFormat::Human, CommandSuccess::CustomFieldCollection(data)) => {
429            custom_fields::collection(data)
430        }
431        (OutputFormat::Json, CommandSuccess::CustomFieldCollection(data)) => {
432            render_json_success(data)
433        }
434        (OutputFormat::Human, CommandSuccess::CustomFieldShown(data)) => {
435            custom_fields::detail(data)
436        }
437        (OutputFormat::Json, CommandSuccess::CustomFieldShown(data)) => render_json_success(data),
438        (OutputFormat::Human, CommandSuccess::CustomFieldCreated(data)) => {
439            custom_fields::created(data)
440        }
441        (OutputFormat::Json, CommandSuccess::CustomFieldCreated(data)) => render_json_success(data),
442        (OutputFormat::Human, CommandSuccess::CustomFieldUpdated(data)) => {
443            custom_fields::updated(data)
444        }
445        (OutputFormat::Json, CommandSuccess::CustomFieldUpdated(data)) => render_json_success(data),
446        (OutputFormat::Human, CommandSuccess::CustomFieldDeleted(data)) => {
447            custom_fields::deleted(data)
448        }
449        (OutputFormat::Json, CommandSuccess::CustomFieldDeleted(data)) => render_json_success(data),
450        (OutputFormat::Human, CommandSuccess::CustomFieldItemsCreated(data)) => {
451            custom_field_items::created(data)
452        }
453        (OutputFormat::Json, CommandSuccess::CustomFieldItemsCreated(data)) => {
454            render_json_success(data)
455        }
456        (OutputFormat::Human, CommandSuccess::CustomFieldItemUpdated(data)) => {
457            custom_field_items::updated(data)
458        }
459        (OutputFormat::Json, CommandSuccess::CustomFieldItemUpdated(data)) => {
460            render_json_success(data)
461        }
462        (OutputFormat::Human, CommandSuccess::CustomFieldItemDeleted(data)) => {
463            custom_field_items::deleted(data)
464        }
465        (OutputFormat::Json, CommandSuccess::CustomFieldItemDeleted(data)) => {
466            render_json_success(data)
467        }
468        (OutputFormat::Human, CommandSuccess::CardCustomFieldSet(data)) => {
469            cards::render_card_custom_field_set(data)
470        }
471        (OutputFormat::Json, CommandSuccess::CardCustomFieldSet(data)) => render_json_success(data),
472        (OutputFormat::Human, CommandSuccess::CardCustomFieldQuery(data)) => {
473            cards::render_card_custom_field_query(data)
474        }
475        (OutputFormat::Json, CommandSuccess::CardCustomFieldQuery(data)) => {
476            render_json_success(data)
477        }
478        (OutputFormat::Human, CommandSuccess::CommentCollection(data)) => {
479            render_comment_collection(data)
480        }
481        (OutputFormat::Json, CommandSuccess::CommentCollection(data)) => render_json_success(data),
482        (OutputFormat::Human, CommandSuccess::CommentShown(data)) => render_comment_detail(data),
483        (OutputFormat::Json, CommandSuccess::CommentShown(data)) => render_json_success(data),
484        (OutputFormat::Human, CommandSuccess::CommentCreated(data)) => render_comment_created(data),
485        (OutputFormat::Json, CommandSuccess::CommentCreated(data)) => render_json_success(data),
486        (OutputFormat::Human, CommandSuccess::CommentDeleted(data)) => render_comment_deleted(data),
487        (OutputFormat::Json, CommandSuccess::CommentDeleted(data)) => render_json_success(data),
488        (OutputFormat::Human, CommandSuccess::SwimlaneCollection(data)) => {
489            render_swimlane_collection(data)
490        }
491        (OutputFormat::Json, CommandSuccess::SwimlaneCollection(data)) => render_json_success(data),
492        (OutputFormat::Human, CommandSuccess::SwimlaneShown(data)) => render_swimlane_detail(data),
493        (OutputFormat::Json, CommandSuccess::SwimlaneShown(data)) => render_json_success(data),
494        (OutputFormat::Human, CommandSuccess::SwimlaneCreated(data)) => {
495            render_swimlane_created(data)
496        }
497        (OutputFormat::Json, CommandSuccess::SwimlaneCreated(data)) => render_json_success(data),
498        (OutputFormat::Human, CommandSuccess::SwimlaneUpdated(data)) => {
499            render_swimlane_updated(data)
500        }
501        (OutputFormat::Json, CommandSuccess::SwimlaneUpdated(data)) => render_json_success(data),
502        (OutputFormat::Human, CommandSuccess::SwimlaneDeleted(data)) => {
503            render_swimlane_deleted(data)
504        }
505        (OutputFormat::Json, CommandSuccess::SwimlaneDeleted(data)) => render_json_success(data),
506        (OutputFormat::Human, CommandSuccess::ProfileAdded(data)) => {
507            render_profile_item("Added profile", data)
508        }
509        (OutputFormat::Json, CommandSuccess::ProfileAdded(data))
510        | (OutputFormat::Json, CommandSuccess::ProfileShown(data))
511        | (OutputFormat::Json, CommandSuccess::ProfileUsed(data))
512        | (OutputFormat::Json, CommandSuccess::ProfileUpdated(data)) => {
513            serde_json::to_string(&SuccessEnvelope { ok: true, data })
514                .expect("profile success is always serializable")
515        }
516        (OutputFormat::Human, CommandSuccess::ProfileList(data)) => render_profile_list(data),
517        (OutputFormat::Json, CommandSuccess::ProfileList(data)) => {
518            serde_json::to_string(&SuccessEnvelope { ok: true, data })
519                .expect("profile list success is always serializable")
520        }
521        (OutputFormat::Human, CommandSuccess::ProfileShown(data)) => {
522            render_profile_item("Profile", data)
523        }
524        (OutputFormat::Human, CommandSuccess::ProfileUsed(data)) => {
525            render_profile_item("Using profile", data)
526        }
527        (OutputFormat::Human, CommandSuccess::ProfileUpdated(data)) => {
528            render_profile_item("Updated profile", data)
529        }
530        (OutputFormat::Human, CommandSuccess::ProfileRemoved(data)) => render_profile_removed(data),
531        (OutputFormat::Json, CommandSuccess::ProfileRemoved(data)) => {
532            serde_json::to_string(&SuccessEnvelope { ok: true, data })
533                .expect("profile removal success is always serializable")
534        }
535    }
536}
537
538pub async fn write_success(
539    format: OutputFormat,
540    success: CommandSuccess,
541    stdout: &mut dyn Write,
542    stderr: &mut dyn Write,
543) -> Result<ExitCode, AppError> {
544    if let CommandSuccess::ApiResponse(response) = success {
545        return raw::write_response(format, response, stdout, stderr).await;
546    }
547    if format == OutputFormat::Raw {
548        if let CommandSuccess::AttachmentDownloaded(data) = &success {
549            let bytes = data.bytes.as_deref().ok_or_else(|| {
550                AppError::invalid_input("raw attachment output requires --destination -")
551            })?;
552            stdout
553                .write_all(bytes)
554                .and_then(|()| stdout.flush())
555                .map_err(output_write_error)?;
556            return Ok(ExitCode::SUCCESS);
557        }
558    }
559    let rendered = render_success(format, &success);
560    if !rendered.is_empty() {
561        let written = writeln!(stdout, "{rendered}").and_then(|()| {
562            if matches!(&success, CommandSuccess::AttachmentDownloaded(data) if data.file_saved) {
563                stdout.flush()
564            } else {
565                Ok(())
566            }
567        });
568        written.map_err(|e| {
569            let error = output_write_error(e);
570            if let CommandSuccess::AttachmentDownloaded(data) = &success {
571                if data.file_saved {
572                    return error.with_details(crate::error::ErrorDetails {
573                        file_saved: Some(true),
574                        destination: Some(data.destination.clone()),
575                        ..crate::error::ErrorDetails::default()
576                    });
577                }
578            }
579            error
580        })?;
581    }
582    Ok(ExitCode::SUCCESS)
583}
584pub(crate) fn output_write_error(error: std::io::Error) -> AppError {
585    AppError::new(
586        ErrorCode::InternalError,
587        format!("could not write command output: {error}"),
588        StableExitCode::Transport,
589    )
590}
591
592pub(crate) fn render_json_success<T: Serialize>(data: &T) -> String {
593    serde_json::to_string(&SuccessEnvelope { ok: true, data })
594        .expect("command success is always serializable")
595}
596
597pub fn render_error(format: OutputFormat, error: &AppError) -> String {
598    match format {
599        OutputFormat::Human => {
600            let mut rendered = format!(
601                "Error [{}]: {}",
602                error.code().as_str(),
603                error.message().trim()
604            );
605            if let Some(batch) = &error.details().batch_result {
606                rendered.push('\n');
607                rendered.push_str(&cards::render_batch(batch));
608            }
609            if let Some(ids) = &error.details().invalid_label_ids {
610                rendered.push_str(&format!(
611                    "\nInvalid label IDs: {}",
612                    formatting::escape_terminal_controls(&ids.join(", "))
613                ));
614            }
615            if let Some(response) = &error.details().response {
616                rendered.push('\n');
617                rendered.push_str(&render_api_response_human(response));
618            }
619            if error.details().file_saved == Some(true) {
620                if let Some(destination) = &error.details().destination {
621                    rendered.push_str(&format!(
622                        "\nDownload saved to `{}`; completion output failed.",
623                        formatting::escape_terminal_controls(destination)
624                    ));
625                }
626            }
627            rendered
628        }
629        OutputFormat::Json => serde_json::to_string(&ErrorEnvelope {
630            ok: false,
631            error: ErrorBody {
632                code: error.code().as_str(),
633                message: error.message(),
634                details: error.details(),
635            },
636        })
637        .expect("application errors are always serializable"),
638        OutputFormat::Raw => format!(
639            "Error [{}]: {}",
640            error.code().as_str(),
641            error.message().trim()
642        ),
643    }
644}
645
646#[cfg(test)]
647mod tests;