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