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;