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