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