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