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 imports;
13mod integrations;
14mod lists;
15mod profiles;
16mod raw;
17mod rules;
18mod skills;
19mod swimlanes;
20mod users;
21
22use auth::{render_auth_status, render_auth_success, render_logout};
23use boards::{
24    render_board_count, render_board_created, render_board_deleted, render_board_detail,
25    render_board_list, render_board_renamed,
26};
27use cards::{
28    render_card_archived, render_card_collection, render_card_created, render_card_deleted,
29    render_card_detail, render_card_restored, render_card_updated,
30};
31use checklist_items::{
32    render_checklist_item_created, render_checklist_item_deleted, render_checklist_item_detail,
33    render_checklist_item_updated,
34};
35use checklists::{
36    render_checklist_collection, render_checklist_created, render_checklist_deleted,
37    render_checklist_detail,
38};
39use comments::{
40    render_comment_collection, render_comment_created, render_comment_deleted,
41    render_comment_detail,
42};
43use lists::{
44    render_list_collection, render_list_created, render_list_deleted, render_list_detail,
45    render_list_updated,
46};
47use profiles::{render_profile_item, render_profile_list, render_profile_removed};
48use swimlanes::{
49    render_swimlane_collection, render_swimlane_created, render_swimlane_deleted,
50    render_swimlane_detail, render_swimlane_updated,
51};
52use users::{
53    render_user_boards, render_user_cards, render_user_created, render_user_deleted,
54    render_user_detail, render_user_list, render_user_login_change, render_user_ownership,
55};
56
57use std::{
58    ffi::OsString,
59    io::{Read, Write},
60    process::ExitCode,
61};
62
63use clap::ValueEnum;
64use serde::Serialize;
65
66use crate::command_result::CommandSuccess;
67use crate::{
68    error::{AppError, ErrorCode},
69    exit_code::StableExitCode,
70};
71
72pub(crate) use raw::render_human_response as render_api_response_human;
73#[cfg(test)]
74pub(crate) use raw::structured_api_response;
75
76#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
77#[value(rename_all = "lower")]
78pub enum OutputFormat {
79    #[default]
80    Human,
81    Json,
82    Raw,
83}
84
85impl OutputFormat {
86    pub fn detect_from_args(args: &[OsString]) -> Self {
87        let mut format = Self::Human;
88        let mut command = crate::command();
89        command.build();
90        let mut current = &command;
91        let mut iter = args.iter().skip(1).peekable();
92        while let Some(argument) = iter.next() {
93            let Some(arg) = argument.to_str() else {
94                continue;
95            };
96            if arg == "--" {
97                break;
98            }
99            if arg == "--output" {
100                format = match iter.next().and_then(|value| value.to_str()) {
101                    Some("json") => Self::Json,
102                    Some("raw") => Self::Raw,
103                    _ => Self::Human,
104                };
105            } else if let Some(value) = arg.strip_prefix("--output=") {
106                format = match value {
107                    "json" => Self::Json,
108                    "raw" => Self::Raw,
109                    _ => Self::Human,
110                };
111            } else if let Some(subcommand) = current.find_subcommand(arg) {
112                current = subcommand;
113            } else if let Some(option) = arg.strip_prefix("--").and_then(|name| {
114                current
115                    .get_arguments()
116                    .find(|option| option.get_long() == Some(name))
117            }) {
118                // Hyphen-prefixed values (for example user cards --from) are
119                // data even when they happen to look like an output option.
120                if option
121                    .get_num_args()
122                    .is_some_and(|arity| arity.takes_values())
123                    && iter.peek().is_some_and(|value| {
124                        option.is_allow_hyphen_values_set()
125                            || !value.to_string_lossy().starts_with('-')
126                    })
127                {
128                    iter.next();
129                }
130            }
131        }
132        format
133    }
134}
135
136#[derive(Serialize)]
137struct SuccessEnvelope<'a, T> {
138    ok: bool,
139    data: &'a T,
140}
141
142#[derive(Serialize)]
143struct ErrorEnvelope<'a> {
144    ok: bool,
145    error: ErrorBody<'a>,
146}
147
148#[derive(Serialize)]
149struct ErrorBody<'a> {
150    code: &'static str,
151    message: &'a str,
152    details: &'a crate::error::ErrorDetails,
153}
154
155pub fn render_success(format: OutputFormat, success: &CommandSuccess) -> String {
156    match (format, success) {
157        (OutputFormat::Human, CommandSuccess::BotRunReported(data)) => {
158            formatting::render_object(data)
159        }
160        (OutputFormat::Json, CommandSuccess::BotRunReported(data)) => render_json_success(data),
161        (OutputFormat::Human, CommandSuccess::InboundEmailSubmitted(data)) => {
162            formatting::render_object(data)
163        }
164        (OutputFormat::Json, CommandSuccess::InboundEmailSubmitted(data)) => {
165            render_json_success(data)
166        }
167        (OutputFormat::Human, CommandSuccess::EmailBounceSubmitted(data)) => {
168            formatting::render_object(data)
169        }
170        (OutputFormat::Json, CommandSuccess::EmailBounceSubmitted(data)) => {
171            render_json_success(data)
172        }
173
174        (OutputFormat::Human, CommandSuccess::OauthProviderSettings(data)) => {
175            formatting::render_object(data)
176        }
177        (OutputFormat::Json, CommandSuccess::OauthProviderSettings(data)) => {
178            render_json_success(data)
179        }
180        (OutputFormat::Human, CommandSuccess::PasswordlessSettings(data)) => {
181            formatting::render_object(data)
182        }
183        (OutputFormat::Json, CommandSuccess::PasswordlessSettings(data)) => {
184            render_json_success(data)
185        }
186        (OutputFormat::Human, CommandSuccess::ProblemsOverview(data)) => {
187            formatting::render_object(data)
188        }
189        (OutputFormat::Json, CommandSuccess::ProblemsOverview(data)) => render_json_success(data),
190        (OutputFormat::Human, CommandSuccess::ProblemStream(data)) => {
191            formatting::render_object(data)
192        }
193        (OutputFormat::Json, CommandSuccess::ProblemStream(data)) => render_json_success(data),
194        (OutputFormat::Human, CommandSuccess::ProblemAcknowledged(data)) => {
195            formatting::render_object(data)
196        }
197        (OutputFormat::Json, CommandSuccess::ProblemAcknowledged(data)) => {
198            render_json_success(data)
199        }
200        (OutputFormat::Human, CommandSuccess::AttachmentSoftDeleted(data)) => {
201            formatting::render_object(data)
202        }
203        (OutputFormat::Json, CommandSuccess::AttachmentSoftDeleted(data)) => {
204            render_json_success(data)
205        }
206        (OutputFormat::Human, CommandSuccess::AttachmentRestored(data)) => {
207            formatting::render_object(data)
208        }
209        (OutputFormat::Json, CommandSuccess::AttachmentRestored(data)) => render_json_success(data),
210        (OutputFormat::Human, CommandSuccess::DeletedAttachments(data)) => {
211            formatting::render_object(data)
212        }
213        (OutputFormat::Json, CommandSuccess::DeletedAttachments(data)) => render_json_success(data),
214        (OutputFormat::Human, CommandSuccess::BoardViewSettings(data)) => {
215            formatting::render_object(data)
216        }
217        (OutputFormat::Json, CommandSuccess::BoardViewSettings(data)) => render_json_success(data),
218        (OutputFormat::Human, CommandSuccess::BoardCardFieldOrder(data)) => {
219            formatting::render_object(data)
220        }
221        (OutputFormat::Json, CommandSuccess::BoardCardFieldOrder(data)) => {
222            render_json_success(data)
223        }
224        (
225            OutputFormat::Human,
226            CommandSuccess::ListArchived(data) | CommandSuccess::ListRestored(data),
227        ) => formatting::render_object(data),
228        (
229            OutputFormat::Json,
230            CommandSuccess::ListArchived(data) | CommandSuccess::ListRestored(data),
231        ) => render_json_success(data),
232        (OutputFormat::Human, CommandSuccess::ChecklistUpdated(data)) => {
233            formatting::render_object(data)
234        }
235        (OutputFormat::Json, CommandSuccess::ChecklistUpdated(data)) => render_json_success(data),
236        (OutputFormat::Human, CommandSuccess::CommentUpdated(data)) => {
237            formatting::render_object(data)
238        }
239        (OutputFormat::Json, CommandSuccess::CommentUpdated(data)) => render_json_success(data),
240        (OutputFormat::Human, CommandSuccess::SkillList(data)) => skills::list(data),
241        (OutputFormat::Json, CommandSuccess::SkillList(data)) => render_json_success(data),
242        (OutputFormat::Human, CommandSuccess::SkillShown(data)) => {
243            data.content.trim_end_matches(['\r', '\n']).to_owned()
244        }
245        (OutputFormat::Json, CommandSuccess::SkillShown(data)) => render_json_success(data),
246        (OutputFormat::Human, CommandSuccess::AuthPreflight(data)) => {
247            formatting::render_object(data)
248        }
249        (OutputFormat::Json, CommandSuccess::AuthPreflight(data)) => render_json_success(data),
250        (OutputFormat::Human, CommandSuccess::SchemaUpgradeStatus(data)) => {
251            formatting::render_object(data)
252        }
253        (OutputFormat::Json, CommandSuccess::SchemaUpgradeStatus(data)) => {
254            render_json_success(data)
255        }
256        (OutputFormat::Human, CommandSuccess::BoardAttachments(data)) => {
257            formatting::render_object(data)
258        }
259        (OutputFormat::Json, CommandSuccess::BoardAttachments(data)) => render_json_success(data),
260        (OutputFormat::Human, CommandSuccess::BoardImported(data)) => imports::board(data),
261        (OutputFormat::Json, CommandSuccess::BoardImported(data)) => render_json_success(data),
262        (OutputFormat::Human, CommandSuccess::TrelloImported(data)) => imports::trello(data),
263        (OutputFormat::Json, CommandSuccess::TrelloImported(data)) => render_json_success(data),
264        (OutputFormat::Human, CommandSuccess::ZipImported(data)) => imports::zip(data),
265        (OutputFormat::Json, CommandSuccess::ZipImported(data)) => render_json_success(data),
266        (OutputFormat::Human, CommandSuccess::IcsImported(data)) => imports::ics(data),
267        (OutputFormat::Json, CommandSuccess::IcsImported(data)) => render_json_success(data),
268        (OutputFormat::Human, CommandSuccess::Exported(data)) => formatting::render_object(data),
269        (OutputFormat::Json, CommandSuccess::Exported(data)) => render_json_success(data),
270        (OutputFormat::Human, CommandSuccess::AdminDomains(data)) => {
271            formatting::render_object(data)
272        }
273        (OutputFormat::Json, CommandSuccess::AdminDomains(data)) => render_json_success(data),
274        (OutputFormat::Human, CommandSuccess::AdminOrganizations(data)) => {
275            formatting::render_object(data)
276        }
277        (OutputFormat::Json, CommandSuccess::AdminOrganizations(data)) => render_json_success(data),
278        (OutputFormat::Human, CommandSuccess::AdminTeams(data)) => formatting::render_object(data),
279        (OutputFormat::Json, CommandSuccess::AdminTeams(data)) => render_json_success(data),
280        (OutputFormat::Human, CommandSuccess::AdminOrganizationUpdated(data)) => {
281            formatting::render_object(data)
282        }
283        (OutputFormat::Json, CommandSuccess::AdminOrganizationUpdated(data)) => {
284            render_json_success(data)
285        }
286        (OutputFormat::Human, CommandSuccess::AdminTeamUpdated(data)) => {
287            formatting::render_object(data)
288        }
289        (OutputFormat::Json, CommandSuccess::AdminTeamUpdated(data)) => render_json_success(data),
290        (OutputFormat::Human, CommandSuccess::AdminOrganizationFeaturesUpdatedAll(data)) => {
291            formatting::render_object(data)
292        }
293        (OutputFormat::Json, CommandSuccess::AdminOrganizationFeaturesUpdatedAll(data)) => {
294            render_json_success(data)
295        }
296        (OutputFormat::Human, CommandSuccess::AdminTeamFeaturesUpdatedAll(data)) => {
297            formatting::render_object(data)
298        }
299        (OutputFormat::Json, CommandSuccess::AdminTeamFeaturesUpdatedAll(data)) => {
300            render_json_success(data)
301        }
302        (OutputFormat::Human, CommandSuccess::GlobalSettings(data)) => {
303            formatting::render_object(data)
304        }
305        (OutputFormat::Json, CommandSuccess::GlobalSettings(data)) => render_json_success(data),
306        (OutputFormat::Human, CommandSuccess::GlobalSettingsUpdated(data)) => {
307            formatting::render_object(data)
308        }
309        (OutputFormat::Json, CommandSuccess::GlobalSettingsUpdated(data)) => {
310            render_json_success(data)
311        }
312        (OutputFormat::Human, CommandSuccess::AttachmentSettings(data)) => {
313            formatting::render_object(data)
314        }
315        (OutputFormat::Json, CommandSuccess::AttachmentSettings(data)) => render_json_success(data),
316        (OutputFormat::Human, CommandSuccess::AttachmentSettingsUpdated(data)) => {
317            formatting::render_object(data)
318        }
319        (OutputFormat::Json, CommandSuccess::AttachmentSettingsUpdated(data)) => {
320            render_json_success(data)
321        }
322
323        (OutputFormat::Human, CommandSuccess::BoardCardSettings(data)) => {
324            formatting::render_object(data)
325        }
326        (OutputFormat::Json, CommandSuccess::BoardCardSettings(data)) => render_json_success(data),
327        (OutputFormat::Human, CommandSuccess::BoardCardSettingsUpdated(data)) => {
328            formatting::render_object(data)
329        }
330        (OutputFormat::Json, CommandSuccess::BoardCardSettingsUpdated(data)) => {
331            render_json_success(data)
332        }
333        (OutputFormat::Human, CommandSuccess::BoardDomains(data)) => {
334            formatting::render_object(data)
335        }
336        (OutputFormat::Json, CommandSuccess::BoardDomains(data)) => render_json_success(data),
337        (OutputFormat::Human, CommandSuccess::BoardDomainChanged(data)) => format!(
338            "Board domains updated:\n{}",
339            formatting::render_object(data)
340        ),
341        (OutputFormat::Json, CommandSuccess::BoardDomainChanged(data)) => render_json_success(data),
342        (OutputFormat::Human, CommandSuccess::RuleCollection(data)) => rules::collection(data),
343        (OutputFormat::Json, CommandSuccess::RuleCollection(data)) => render_json_success(data),
344        (OutputFormat::Human, CommandSuccess::RuleShown(data)) => rules::detail(data),
345        (OutputFormat::Json, CommandSuccess::RuleShown(data)) => render_json_success(data),
346        (OutputFormat::Human, CommandSuccess::RuleCreated(data)) => rules::created(data),
347        (OutputFormat::Json, CommandSuccess::RuleCreated(data)) => render_json_success(data),
348        (OutputFormat::Human, CommandSuccess::RuleUpdated(data)) => rules::updated(data),
349        (OutputFormat::Json, CommandSuccess::RuleUpdated(data)) => render_json_success(data),
350        (OutputFormat::Human, CommandSuccess::RuleDeleted(data)) => rules::deleted(data),
351        (OutputFormat::Json, CommandSuccess::RuleDeleted(data)) => render_json_success(data),
352        (OutputFormat::Human, CommandSuccess::IntegrationCollection(data)) => {
353            integrations::collection(data)
354        }
355        (OutputFormat::Json, CommandSuccess::IntegrationCollection(data)) => {
356            render_json_success(data)
357        }
358        (OutputFormat::Human, CommandSuccess::IntegrationShown(data)) => integrations::detail(data),
359        (OutputFormat::Json, CommandSuccess::IntegrationShown(data)) => render_json_success(data),
360        (OutputFormat::Human, CommandSuccess::IntegrationCreated(data)) => {
361            integrations::created(data)
362        }
363        (OutputFormat::Json, CommandSuccess::IntegrationCreated(data)) => render_json_success(data),
364        (OutputFormat::Human, CommandSuccess::IntegrationUpdated(data)) => {
365            integrations::updated(data)
366        }
367        (OutputFormat::Json, CommandSuccess::IntegrationUpdated(data)) => render_json_success(data),
368        (OutputFormat::Human, CommandSuccess::IntegrationDeleted(data)) => {
369            integrations::deleted(data)
370        }
371        (OutputFormat::Json, CommandSuccess::IntegrationDeleted(data)) => render_json_success(data),
372        (OutputFormat::Human, CommandSuccess::IntegrationActivitiesChanged(data)) => {
373            integrations::activities(data)
374        }
375        (OutputFormat::Json, CommandSuccess::IntegrationActivitiesChanged(data)) => {
376            render_json_success(data)
377        }
378        (OutputFormat::Human, CommandSuccess::BoardCopied(data)) => {
379            format!("Board copied:\n{}", formatting::render_object(data))
380        }
381        (OutputFormat::Json, CommandSuccess::BoardCopied(data)) => render_json_success(data),
382        (OutputFormat::Human, CommandSuccess::CardCopied(data)) => {
383            format!("Card copied:\n{}", formatting::render_object(data))
384        }
385        (OutputFormat::Json, CommandSuccess::CardCopied(data)) => render_json_success(data),
386        (OutputFormat::Human, CommandSuccess::ListCopied(data)) => {
387            format!("List copied:\n{}", formatting::render_object(data))
388        }
389        (OutputFormat::Human, CommandSuccess::ListMoved(data)) => {
390            format!(
391                "{}:\n{}",
392                if data.source_board_id == data.target_board_id {
393                    "List moved"
394                } else {
395                    "Cross-board list move request accepted (source list retained; position ignored)"
396                },
397                formatting::render_object(data)
398            )
399        }
400        (
401            OutputFormat::Json,
402            CommandSuccess::ListCopied(data) | CommandSuccess::ListMoved(data),
403        ) => render_json_success(data),
404        (OutputFormat::Human, CommandSuccess::SwimlaneCopied(data)) => {
405            format!("Swimlane copied:\n{}", formatting::render_object(data))
406        }
407        (OutputFormat::Human, CommandSuccess::SwimlaneMoved(data)) => {
408            format!("Swimlane moved:\n{}", formatting::render_object(data))
409        }
410        (
411            OutputFormat::Json,
412            CommandSuccess::SwimlaneCopied(data) | CommandSuccess::SwimlaneMoved(data),
413        ) => render_json_success(data),
414        (OutputFormat::Human, CommandSuccess::CardCount(data)) => cards::render_card_count(data),
415        (OutputFormat::Json, CommandSuccess::CardCount(data)) => render_json_success(data),
416        (OutputFormat::Human, CommandSuccess::SwimlaneCards(data)) => {
417            cards::render_swimlane_cards(data)
418        }
419        (OutputFormat::Json, CommandSuccess::SwimlaneCards(data)) => render_json_success(data),
420        (OutputFormat::Human, CommandSuccess::CardBatch(data)) => cards::render_batch(data),
421        (OutputFormat::Json, CommandSuccess::CardBatch(data)) => match data {
422            crate::command_result::CardBatchResult::CardBulkCreate(data) => {
423                render_json_success(data)
424            }
425            crate::command_result::CardBatchResult::CardBulkDelete(data) => {
426                render_json_success(data)
427            }
428            crate::command_result::CardBatchResult::CardBulkLabels(data) => {
429                render_json_success(data)
430            }
431        },
432        (OutputFormat::Human, CommandSuccess::BoardLabels(data)) => formatting::render_object(data),
433        (OutputFormat::Json, CommandSuccess::BoardLabels(data)) => render_json_success(data),
434        (OutputFormat::Human, CommandSuccess::BoardLabelAdded(data)) => {
435            boards::render_label_added(data)
436        }
437        (OutputFormat::Json, CommandSuccess::BoardLabelAdded(data)) => render_json_success(data),
438        (OutputFormat::Human, CommandSuccess::CardLabels(data)) => formatting::render_object(data),
439        (OutputFormat::Json, CommandSuccess::CardLabels(data)) => render_json_success(data),
440        (OutputFormat::Human, CommandSuccess::BoardMembers(data)) => {
441            formatting::render_object(data)
442        }
443        (OutputFormat::Json, CommandSuccess::BoardMembers(data)) => render_json_success(data),
444        (OutputFormat::Human, CommandSuccess::BoardMemberChanged(data)) => {
445            let action = match data.action {
446                crate::command_result::MembershipAction::Add => "Addition",
447                crate::command_result::MembershipAction::Remove => "Removal",
448            };
449            format!(
450                "Board member {action} request accepted:\n{}",
451                formatting::render_object(data)
452            )
453        }
454        (OutputFormat::Json, CommandSuccess::BoardMemberChanged(data)) => render_json_success(data),
455        (OutputFormat::Human, CommandSuccess::BoardMemberUpdated(data)) => {
456            formatting::render_object(data)
457        }
458        (OutputFormat::Json, CommandSuccess::BoardMemberUpdated(data)) => render_json_success(data),
459        (OutputFormat::Human, CommandSuccess::CardMembers(data)) => formatting::render_object(data),
460        (OutputFormat::Json, CommandSuccess::CardMembers(data)) => render_json_success(data),
461        (OutputFormat::Human, CommandSuccess::CardAssignees(data)) => {
462            formatting::render_object(data)
463        }
464        (OutputFormat::Json, CommandSuccess::CardAssignees(data)) => render_json_success(data),
465        (OutputFormat::Human, CommandSuccess::CardMembershipChanged(data)) => {
466            formatting::render_object(data)
467        }
468        (OutputFormat::Json, CommandSuccess::CardMembershipChanged(data)) => {
469            render_json_success(data)
470        }
471        (OutputFormat::Human, CommandSuccess::CardDependencies(data)) => {
472            dependencies::card_collection(data)
473        }
474        (OutputFormat::Json, CommandSuccess::CardDependencies(data)) => render_json_success(data),
475        (OutputFormat::Human, CommandSuccess::BoardDependencies(data)) => {
476            dependencies::board_collection(data)
477        }
478        (OutputFormat::Json, CommandSuccess::BoardDependencies(data)) => render_json_success(data),
479        (OutputFormat::Human, CommandSuccess::DependencyCreated(data)) => {
480            dependencies::created(data)
481        }
482        (OutputFormat::Json, CommandSuccess::DependencyCreated(data)) => render_json_success(data),
483        (OutputFormat::Human, CommandSuccess::DependencyUpdated(data)) => {
484            dependencies::updated(data)
485        }
486        (OutputFormat::Json, CommandSuccess::DependencyUpdated(data)) => render_json_success(data),
487        (OutputFormat::Human, CommandSuccess::DependencyDeleted(data)) => {
488            dependencies::deleted(data)
489        }
490        (OutputFormat::Json, CommandSuccess::DependencyDeleted(data)) => render_json_success(data),
491        (OutputFormat::Human, CommandSuccess::AttachmentList(data)) => attachments::list(data),
492        (OutputFormat::Json, CommandSuccess::AttachmentList(data)) => render_json_success(data),
493        (OutputFormat::Human, CommandSuccess::AttachmentShown(data)) => attachments::detail(data),
494        (OutputFormat::Json, CommandSuccess::AttachmentShown(data)) => render_json_success(data),
495        (OutputFormat::Human, CommandSuccess::AttachmentUploaded(data)) => {
496            format!(
497                "Attachment upload accepted; final availability is unconfirmed:\n{}",
498                attachments::detail(data)
499            )
500        }
501        (OutputFormat::Json, CommandSuccess::AttachmentUploaded(data)) => render_json_success(data),
502        (OutputFormat::Human, CommandSuccess::BackgroundUploaded(data)) => {
503            format!("Board background uploaded:\n{}", attachments::detail(data))
504        }
505        (OutputFormat::Json, CommandSuccess::BackgroundUploaded(data)) => render_json_success(data),
506        (OutputFormat::Human, CommandSuccess::AttachmentCopied(data)) => {
507            format!("Attachment copied:\n{}", attachments::detail(data))
508        }
509        (OutputFormat::Json, CommandSuccess::AttachmentCopied(data)) => render_json_success(data),
510        (OutputFormat::Human, CommandSuccess::AttachmentMoved(data)) => {
511            format!("Attachment moved:\n{}", attachments::detail(data))
512        }
513        (OutputFormat::Json, CommandSuccess::AttachmentMoved(data)) => render_json_success(data),
514        (OutputFormat::Human, CommandSuccess::AttachmentDeleted(data)) => {
515            format!("Attachment deleted:\n{}", attachments::detail(data))
516        }
517        (OutputFormat::Json, CommandSuccess::AttachmentDeleted(data)) => render_json_success(data),
518        (OutputFormat::Human, CommandSuccess::AttachmentDownloaded(data)) => {
519            attachments::detail(data)
520        }
521        (OutputFormat::Json, CommandSuccess::AttachmentDownloaded(data)) => {
522            render_json_success(data)
523        }
524        (_, CommandSuccess::ApiResponse(_)) => {
525            panic!("streaming API responses must be consumed by output::write_success")
526        }
527        (OutputFormat::Raw, CommandSuccess::Cancelled(_)) => String::new(),
528        (OutputFormat::Raw, _) => panic!("raw results must be consumed by output::write_success"),
529        (OutputFormat::Human, CommandSuccess::Cancelled(_)) => {
530            "Cancelled; no changes made.".to_owned()
531        }
532        (OutputFormat::Json, CommandSuccess::Cancelled(data)) => render_json_success(data),
533        (OutputFormat::Human, CommandSuccess::Registration(data)) => {
534            render_auth_success("Registered user", data)
535        }
536        (OutputFormat::Json, CommandSuccess::Registration(data)) => render_json_success(data),
537        (OutputFormat::Human, CommandSuccess::Login(data)) => {
538            render_auth_success("Logged in user", data)
539        }
540        (OutputFormat::Json, CommandSuccess::Login(data)) => render_json_success(data),
541        (OutputFormat::Human, CommandSuccess::Logout(data)) => render_logout(data),
542        (OutputFormat::Json, CommandSuccess::Logout(data)) => render_json_success(data),
543        (OutputFormat::Human, CommandSuccess::AuthStatus(data)) => render_auth_status(data),
544        (OutputFormat::Json, CommandSuccess::AuthStatus(data)) => render_json_success(data),
545        (OutputFormat::Human, CommandSuccess::UserCurrent(data))
546        | (OutputFormat::Human, CommandSuccess::UserShown(data))
547        | (OutputFormat::Human, CommandSuccess::UserUpdated(data)) => render_user_detail(data),
548        (OutputFormat::Json, CommandSuccess::UserCurrent(data))
549        | (OutputFormat::Json, CommandSuccess::UserShown(data))
550        | (OutputFormat::Json, CommandSuccess::UserUpdated(data)) => render_json_success(data),
551        (OutputFormat::Human, CommandSuccess::UserList(data)) => render_user_list(data),
552        (OutputFormat::Json, CommandSuccess::UserList(data)) => render_json_success(data),
553        (OutputFormat::Human, CommandSuccess::UserCards(data)) => render_user_cards(data),
554        (OutputFormat::Json, CommandSuccess::UserCards(data)) => render_json_success(data),
555        (OutputFormat::Human, CommandSuccess::UserCreated(data)) => render_user_created(data),
556        (OutputFormat::Json, CommandSuccess::UserCreated(data)) => render_json_success(data),
557        (OutputFormat::Human, CommandSuccess::UserBoards(data)) => render_user_boards(data),
558        (OutputFormat::Json, CommandSuccess::UserBoards(data)) => render_json_success(data),
559        (OutputFormat::Human, CommandSuccess::UserOwnershipTaken(data)) => {
560            render_user_ownership(data)
561        }
562        (OutputFormat::Json, CommandSuccess::UserOwnershipTaken(data)) => render_json_success(data),
563        (OutputFormat::Human, CommandSuccess::UserLoginChanged(data)) => {
564            render_user_login_change(data)
565        }
566        (OutputFormat::Json, CommandSuccess::UserLoginChanged(data)) => render_json_success(data),
567        (OutputFormat::Human, CommandSuccess::UserDeleted(data)) => render_user_deleted(data),
568        (OutputFormat::Json, CommandSuccess::UserDeleted(data)) => render_json_success(data),
569        (OutputFormat::Human, CommandSuccess::UserTokenCreated(data)) => {
570            users::render_token_created(data)
571        }
572        (OutputFormat::Json, CommandSuccess::UserTokenCreated(data)) => render_json_success(data),
573        (OutputFormat::Human, CommandSuccess::UserTokenDeleted(data)) => {
574            users::render_token_deleted(data)
575        }
576        (OutputFormat::Json, CommandSuccess::UserTokenDeleted(data)) => render_json_success(data),
577        (OutputFormat::Human, CommandSuccess::BoardList(data)) => render_board_list(data),
578        (OutputFormat::Json, CommandSuccess::BoardList(data)) => render_json_success(data),
579        (OutputFormat::Human, CommandSuccess::BoardCount(data)) => render_board_count(data),
580        (OutputFormat::Json, CommandSuccess::BoardCount(data)) => render_json_success(data),
581        (OutputFormat::Human, CommandSuccess::BoardShown(data)) => render_board_detail(data),
582        (OutputFormat::Json, CommandSuccess::BoardShown(data)) => render_json_success(data),
583        (OutputFormat::Human, CommandSuccess::BoardCreated(data)) => render_board_created(data),
584        (OutputFormat::Json, CommandSuccess::BoardCreated(data)) => render_json_success(data),
585        (OutputFormat::Human, CommandSuccess::BoardRenamed(data)) => render_board_renamed(data),
586        (OutputFormat::Json, CommandSuccess::BoardRenamed(data)) => render_json_success(data),
587        (OutputFormat::Human, CommandSuccess::BoardDeleted(data)) => render_board_deleted(data),
588        (OutputFormat::Json, CommandSuccess::BoardDeleted(data)) => render_json_success(data),
589        (OutputFormat::Human, CommandSuccess::ListCollection(data)) => render_list_collection(data),
590        (OutputFormat::Json, CommandSuccess::ListCollection(data)) => render_json_success(data),
591        (OutputFormat::Human, CommandSuccess::ListShown(data)) => render_list_detail(data),
592        (OutputFormat::Json, CommandSuccess::ListShown(data)) => render_json_success(data),
593        (OutputFormat::Human, CommandSuccess::ListCreated(data)) => render_list_created(data),
594        (OutputFormat::Json, CommandSuccess::ListCreated(data)) => render_json_success(data),
595        (OutputFormat::Human, CommandSuccess::ListUpdated(data)) => render_list_updated(data),
596        (OutputFormat::Json, CommandSuccess::ListUpdated(data)) => render_json_success(data),
597        (OutputFormat::Human, CommandSuccess::ListDeleted(data)) => render_list_deleted(data),
598        (OutputFormat::Json, CommandSuccess::ListDeleted(data)) => render_json_success(data),
599        (OutputFormat::Human, CommandSuccess::CardCollection(data)) => render_card_collection(data),
600        (OutputFormat::Json, CommandSuccess::CardCollection(data)) => render_json_success(data),
601        (OutputFormat::Human, CommandSuccess::CardShown(data)) => render_card_detail(data),
602        (OutputFormat::Json, CommandSuccess::CardShown(data)) => render_json_success(data),
603        (OutputFormat::Human, CommandSuccess::CardCreated(data)) => render_card_created(data),
604        (OutputFormat::Json, CommandSuccess::CardCreated(data)) => render_json_success(data),
605        (OutputFormat::Human, CommandSuccess::CardUpdated(data)) => render_card_updated(data),
606        (OutputFormat::Json, CommandSuccess::CardUpdated(data)) => render_json_success(data),
607        (OutputFormat::Human, CommandSuccess::CardArchived(data)) => render_card_archived(data),
608        (OutputFormat::Json, CommandSuccess::CardArchived(data)) => render_json_success(data),
609        (OutputFormat::Human, CommandSuccess::CardRestored(data)) => render_card_restored(data),
610        (OutputFormat::Json, CommandSuccess::CardRestored(data)) => render_json_success(data),
611        (OutputFormat::Human, CommandSuccess::CardDeleted(data)) => render_card_deleted(data),
612        (OutputFormat::Json, CommandSuccess::CardDeleted(data)) => render_json_success(data),
613        (OutputFormat::Human, CommandSuccess::ChecklistCollection(data)) => {
614            render_checklist_collection(data)
615        }
616        (OutputFormat::Json, CommandSuccess::ChecklistCollection(data)) => {
617            render_json_success(data)
618        }
619        (OutputFormat::Human, CommandSuccess::ChecklistShown(data)) => {
620            render_checklist_detail(data)
621        }
622        (OutputFormat::Json, CommandSuccess::ChecklistShown(data)) => render_json_success(data),
623        (OutputFormat::Human, CommandSuccess::ChecklistCreated(data)) => {
624            render_checklist_created(data)
625        }
626        (OutputFormat::Json, CommandSuccess::ChecklistCreated(data)) => render_json_success(data),
627        (OutputFormat::Human, CommandSuccess::ChecklistDeleted(data)) => {
628            render_checklist_deleted(data)
629        }
630        (OutputFormat::Json, CommandSuccess::ChecklistDeleted(data)) => render_json_success(data),
631        (OutputFormat::Human, CommandSuccess::ChecklistItemShown(data)) => {
632            render_checklist_item_detail(data)
633        }
634        (OutputFormat::Json, CommandSuccess::ChecklistItemShown(data)) => render_json_success(data),
635        (OutputFormat::Human, CommandSuccess::ChecklistItemCreated(data)) => {
636            render_checklist_item_created(data)
637        }
638        (OutputFormat::Json, CommandSuccess::ChecklistItemCreated(data)) => {
639            render_json_success(data)
640        }
641        (OutputFormat::Human, CommandSuccess::ChecklistItemUpdated(data)) => {
642            render_checklist_item_updated(data)
643        }
644        (OutputFormat::Json, CommandSuccess::ChecklistItemUpdated(data)) => {
645            render_json_success(data)
646        }
647        (OutputFormat::Human, CommandSuccess::ChecklistItemDeleted(data)) => {
648            render_checklist_item_deleted(data)
649        }
650        (OutputFormat::Json, CommandSuccess::ChecklistItemDeleted(data)) => {
651            render_json_success(data)
652        }
653        (OutputFormat::Human, CommandSuccess::CustomFieldCollection(data)) => {
654            custom_fields::collection(data)
655        }
656        (OutputFormat::Json, CommandSuccess::CustomFieldCollection(data)) => {
657            render_json_success(data)
658        }
659        (OutputFormat::Human, CommandSuccess::CustomFieldShown(data)) => {
660            custom_fields::detail(data)
661        }
662        (OutputFormat::Json, CommandSuccess::CustomFieldShown(data)) => render_json_success(data),
663        (OutputFormat::Human, CommandSuccess::CustomFieldCreated(data)) => {
664            custom_fields::created(data)
665        }
666        (OutputFormat::Json, CommandSuccess::CustomFieldCreated(data)) => render_json_success(data),
667        (OutputFormat::Human, CommandSuccess::CustomFieldUpdated(data)) => {
668            custom_fields::updated(data)
669        }
670        (OutputFormat::Json, CommandSuccess::CustomFieldUpdated(data)) => render_json_success(data),
671        (OutputFormat::Human, CommandSuccess::CustomFieldDeleted(data)) => {
672            custom_fields::deleted(data)
673        }
674        (OutputFormat::Json, CommandSuccess::CustomFieldDeleted(data)) => render_json_success(data),
675        (OutputFormat::Human, CommandSuccess::CustomFieldItemsCreated(data)) => {
676            custom_field_items::created(data)
677        }
678        (OutputFormat::Json, CommandSuccess::CustomFieldItemsCreated(data)) => {
679            render_json_success(data)
680        }
681        (OutputFormat::Human, CommandSuccess::CustomFieldItemUpdated(data)) => {
682            custom_field_items::updated(data)
683        }
684        (OutputFormat::Json, CommandSuccess::CustomFieldItemUpdated(data)) => {
685            render_json_success(data)
686        }
687        (OutputFormat::Human, CommandSuccess::CustomFieldItemDeleted(data)) => {
688            custom_field_items::deleted(data)
689        }
690        (OutputFormat::Json, CommandSuccess::CustomFieldItemDeleted(data)) => {
691            render_json_success(data)
692        }
693        (OutputFormat::Human, CommandSuccess::CardCustomFieldSet(data)) => {
694            cards::render_card_custom_field_set(data)
695        }
696        (OutputFormat::Json, CommandSuccess::CardCustomFieldSet(data)) => render_json_success(data),
697        (OutputFormat::Human, CommandSuccess::CardCustomFieldQuery(data)) => {
698            cards::render_card_custom_field_query(data)
699        }
700        (OutputFormat::Json, CommandSuccess::CardCustomFieldQuery(data)) => {
701            render_json_success(data)
702        }
703        (OutputFormat::Human, CommandSuccess::CommentCollection(data)) => {
704            render_comment_collection(data)
705        }
706        (OutputFormat::Json, CommandSuccess::CommentCollection(data)) => render_json_success(data),
707        (OutputFormat::Human, CommandSuccess::CommentShown(data)) => render_comment_detail(data),
708        (OutputFormat::Json, CommandSuccess::CommentShown(data)) => render_json_success(data),
709        (OutputFormat::Human, CommandSuccess::CommentCreated(data)) => render_comment_created(data),
710        (OutputFormat::Json, CommandSuccess::CommentCreated(data)) => render_json_success(data),
711        (OutputFormat::Human, CommandSuccess::CommentDeleted(data)) => render_comment_deleted(data),
712        (OutputFormat::Json, CommandSuccess::CommentDeleted(data)) => render_json_success(data),
713        (OutputFormat::Human, CommandSuccess::SwimlaneCollection(data)) => {
714            render_swimlane_collection(data)
715        }
716        (OutputFormat::Json, CommandSuccess::SwimlaneCollection(data)) => render_json_success(data),
717        (OutputFormat::Human, CommandSuccess::SwimlaneShown(data)) => render_swimlane_detail(data),
718        (OutputFormat::Json, CommandSuccess::SwimlaneShown(data)) => render_json_success(data),
719        (OutputFormat::Human, CommandSuccess::SwimlaneCreated(data)) => {
720            render_swimlane_created(data)
721        }
722        (OutputFormat::Json, CommandSuccess::SwimlaneCreated(data)) => render_json_success(data),
723        (OutputFormat::Human, CommandSuccess::SwimlaneUpdated(data)) => {
724            render_swimlane_updated(data)
725        }
726        (OutputFormat::Json, CommandSuccess::SwimlaneUpdated(data)) => render_json_success(data),
727        (OutputFormat::Human, CommandSuccess::SwimlaneDeleted(data)) => {
728            render_swimlane_deleted(data)
729        }
730        (OutputFormat::Json, CommandSuccess::SwimlaneDeleted(data)) => render_json_success(data),
731        (OutputFormat::Human, CommandSuccess::ProfileAdded(data)) => {
732            render_profile_item("Added profile", data)
733        }
734        (OutputFormat::Json, CommandSuccess::ProfileAdded(data))
735        | (OutputFormat::Json, CommandSuccess::ProfileShown(data))
736        | (OutputFormat::Json, CommandSuccess::ProfileUsed(data))
737        | (OutputFormat::Json, CommandSuccess::ProfileUpdated(data)) => {
738            serde_json::to_string(&SuccessEnvelope { ok: true, data })
739                .expect("profile success is always serializable")
740        }
741        (OutputFormat::Human, CommandSuccess::ProfileList(data)) => render_profile_list(data),
742        (OutputFormat::Json, CommandSuccess::ProfileList(data)) => {
743            serde_json::to_string(&SuccessEnvelope { ok: true, data })
744                .expect("profile list success is always serializable")
745        }
746        (OutputFormat::Human, CommandSuccess::ProfileShown(data)) => {
747            render_profile_item("Profile", data)
748        }
749        (OutputFormat::Human, CommandSuccess::ProfileUsed(data)) => {
750            render_profile_item("Using profile", data)
751        }
752        (OutputFormat::Human, CommandSuccess::ProfileUpdated(data)) => {
753            render_profile_item("Updated profile", data)
754        }
755        (OutputFormat::Human, CommandSuccess::ProfileRemoved(data)) => render_profile_removed(data),
756        (OutputFormat::Json, CommandSuccess::ProfileRemoved(data)) => {
757            serde_json::to_string(&SuccessEnvelope { ok: true, data })
758                .expect("profile removal success is always serializable")
759        }
760    }
761}
762
763pub async fn write_success(
764    format: OutputFormat,
765    success: CommandSuccess,
766    stdout: &mut dyn Write,
767    stderr: &mut dyn Write,
768) -> Result<ExitCode, AppError> {
769    if let CommandSuccess::ApiResponse(response) = success {
770        return raw::write_response(format, response, stdout, stderr).await;
771    }
772    if format == OutputFormat::Raw {
773        if let CommandSuccess::AttachmentDownloaded(data) = &success {
774            let bytes = data.bytes.as_deref().ok_or_else(|| {
775                AppError::invalid_input("raw attachment output requires --destination -")
776            })?;
777            stdout
778                .write_all(bytes)
779                .and_then(|()| stdout.flush())
780                .map_err(output_write_error)?;
781            return Ok(ExitCode::SUCCESS);
782        }
783    }
784    if format == OutputFormat::Raw {
785        if let CommandSuccess::Exported(data) = &success {
786            let staged = data.staged.as_ref().ok_or_else(|| {
787                AppError::invalid_input("raw export output requires --destination -")
788            })?;
789            let mut file = staged.0.reopen().map_err(export_read_error)?;
790            let mut buffer = [0; 64 * 1024];
791            loop {
792                let read = file.read(&mut buffer).map_err(export_read_error)?;
793                if read == 0 {
794                    break;
795                }
796                stdout
797                    .write_all(&buffer[..read])
798                    .map_err(output_write_error)?;
799            }
800            stdout.flush().map_err(output_write_error)?;
801            return Ok(ExitCode::SUCCESS);
802        }
803    }
804    let rendered = render_success(format, &success);
805    if !rendered.is_empty() {
806        let written = writeln!(stdout, "{rendered}").and_then(|()| {
807            if matches!(&success, CommandSuccess::AttachmentDownloaded(data) if data.file_saved)
808                || matches!(&success, CommandSuccess::Exported(data) if data.file_saved)
809            {
810                stdout.flush()
811            } else {
812                Ok(())
813            }
814        });
815        written.map_err(|e| {
816            let error = output_write_error(e);
817            if let CommandSuccess::Exported(data) = &success {
818                if data.file_saved {
819                    return error.with_details(crate::error::ErrorDetails {
820                        file_saved: Some(true),
821                        destination: Some(data.destination.clone()),
822                        ..crate::error::ErrorDetails::default()
823                    });
824                }
825            }
826            if let CommandSuccess::AttachmentDownloaded(data) = &success {
827                if data.file_saved {
828                    return error.with_details(crate::error::ErrorDetails {
829                        file_saved: Some(true),
830                        destination: Some(data.destination.clone()),
831                        ..crate::error::ErrorDetails::default()
832                    });
833                }
834            }
835            error
836        })?;
837    }
838    Ok(ExitCode::SUCCESS)
839}
840fn export_read_error(error: std::io::Error) -> AppError {
841    AppError::new(
842        ErrorCode::FileIoError,
843        format!(
844            "could not read the staged export file: {}",
845            crate::input::escape_terminal_text(&error.to_string())
846        ),
847        StableExitCode::Transport,
848    )
849}
850pub(crate) fn output_write_error(error: std::io::Error) -> AppError {
851    AppError::new(
852        ErrorCode::InternalError,
853        format!("could not write command output: {error}"),
854        StableExitCode::Transport,
855    )
856}
857
858pub(crate) fn render_json_success<T: Serialize>(data: &T) -> String {
859    serde_json::to_string(&SuccessEnvelope { ok: true, data })
860        .expect("command success is always serializable")
861}
862
863pub fn render_error(format: OutputFormat, error: &AppError) -> String {
864    match format {
865        OutputFormat::Human => {
866            let mut rendered = format!(
867                "Error [{}]: {}",
868                error.code().as_str(),
869                error.message().trim()
870            );
871            if let Some(batch) = &error.details().batch_result {
872                rendered.push('\n');
873                rendered.push_str(&cards::render_batch(batch));
874            }
875            if let Some(result) = &error.details().trello_import_result {
876                rendered.push('\n');
877                rendered.push_str(&imports::trello(result));
878            }
879            if let Some(ids) = &error.details().invalid_label_ids {
880                rendered.push_str(&format!(
881                    "\nInvalid label IDs: {}",
882                    formatting::escape_terminal_controls(&ids.join(", "))
883                ));
884            }
885            if let Some(response) = &error.details().response {
886                rendered.push('\n');
887                rendered.push_str(&render_api_response_human(response));
888            }
889            if error.details().file_saved == Some(true) {
890                if let Some(destination) = &error.details().destination {
891                    rendered.push_str(&format!(
892                        "\nDownload saved to `{}`; completion output failed.",
893                        formatting::escape_terminal_controls(destination)
894                    ));
895                }
896            }
897            rendered
898        }
899        OutputFormat::Json => serde_json::to_string(&ErrorEnvelope {
900            ok: false,
901            error: ErrorBody {
902                code: error.code().as_str(),
903                message: error.message(),
904                details: error.details(),
905            },
906        })
907        .expect("application errors are always serializable"),
908        OutputFormat::Raw => format!(
909            "Error [{}]: {}",
910            error.code().as_str(),
911            error.message().trim()
912        ),
913    }
914}
915
916#[cfg(test)]
917mod tests;