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