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