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