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