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