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