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