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::BoardList(data)) => render_board_list(data),
419 (OutputFormat::Json, CommandSuccess::BoardList(data)) => render_json_success(data),
420 (OutputFormat::Human, CommandSuccess::BoardCount(data)) => render_board_count(data),
421 (OutputFormat::Json, CommandSuccess::BoardCount(data)) => render_json_success(data),
422 (OutputFormat::Human, CommandSuccess::BoardShown(data)) => render_board_detail(data),
423 (OutputFormat::Json, CommandSuccess::BoardShown(data)) => render_json_success(data),
424 (OutputFormat::Human, CommandSuccess::BoardCreated(data)) => render_board_created(data),
425 (OutputFormat::Json, CommandSuccess::BoardCreated(data)) => render_json_success(data),
426 (OutputFormat::Human, CommandSuccess::BoardRenamed(data)) => render_board_renamed(data),
427 (OutputFormat::Json, CommandSuccess::BoardRenamed(data)) => render_json_success(data),
428 (OutputFormat::Human, CommandSuccess::BoardDeleted(data)) => render_board_deleted(data),
429 (OutputFormat::Json, CommandSuccess::BoardDeleted(data)) => render_json_success(data),
430 (OutputFormat::Human, CommandSuccess::ListCollection(data)) => render_list_collection(data),
431 (OutputFormat::Json, CommandSuccess::ListCollection(data)) => render_json_success(data),
432 (OutputFormat::Human, CommandSuccess::ListShown(data)) => render_list_detail(data),
433 (OutputFormat::Json, CommandSuccess::ListShown(data)) => render_json_success(data),
434 (OutputFormat::Human, CommandSuccess::ListCreated(data)) => render_list_created(data),
435 (OutputFormat::Json, CommandSuccess::ListCreated(data)) => render_json_success(data),
436 (OutputFormat::Human, CommandSuccess::ListUpdated(data)) => render_list_updated(data),
437 (OutputFormat::Json, CommandSuccess::ListUpdated(data)) => render_json_success(data),
438 (OutputFormat::Human, CommandSuccess::ListDeleted(data)) => render_list_deleted(data),
439 (OutputFormat::Json, CommandSuccess::ListDeleted(data)) => render_json_success(data),
440 (OutputFormat::Human, CommandSuccess::CardCollection(data)) => render_card_collection(data),
441 (OutputFormat::Json, CommandSuccess::CardCollection(data)) => render_json_success(data),
442 (OutputFormat::Human, CommandSuccess::CardShown(data)) => render_card_detail(data),
443 (OutputFormat::Json, CommandSuccess::CardShown(data)) => render_json_success(data),
444 (OutputFormat::Human, CommandSuccess::CardCreated(data)) => render_card_created(data),
445 (OutputFormat::Json, CommandSuccess::CardCreated(data)) => render_json_success(data),
446 (OutputFormat::Human, CommandSuccess::CardUpdated(data)) => render_card_updated(data),
447 (OutputFormat::Json, CommandSuccess::CardUpdated(data)) => render_json_success(data),
448 (OutputFormat::Human, CommandSuccess::CardArchived(data)) => render_card_archived(data),
449 (OutputFormat::Json, CommandSuccess::CardArchived(data)) => render_json_success(data),
450 (OutputFormat::Human, CommandSuccess::CardRestored(data)) => render_card_restored(data),
451 (OutputFormat::Json, CommandSuccess::CardRestored(data)) => render_json_success(data),
452 (OutputFormat::Human, CommandSuccess::CardDeleted(data)) => render_card_deleted(data),
453 (OutputFormat::Json, CommandSuccess::CardDeleted(data)) => render_json_success(data),
454 (OutputFormat::Human, CommandSuccess::ChecklistCollection(data)) => {
455 render_checklist_collection(data)
456 }
457 (OutputFormat::Json, CommandSuccess::ChecklistCollection(data)) => {
458 render_json_success(data)
459 }
460 (OutputFormat::Human, CommandSuccess::ChecklistShown(data)) => {
461 render_checklist_detail(data)
462 }
463 (OutputFormat::Json, CommandSuccess::ChecklistShown(data)) => render_json_success(data),
464 (OutputFormat::Human, CommandSuccess::ChecklistCreated(data)) => {
465 render_checklist_created(data)
466 }
467 (OutputFormat::Json, CommandSuccess::ChecklistCreated(data)) => render_json_success(data),
468 (OutputFormat::Human, CommandSuccess::ChecklistDeleted(data)) => {
469 render_checklist_deleted(data)
470 }
471 (OutputFormat::Json, CommandSuccess::ChecklistDeleted(data)) => render_json_success(data),
472 (OutputFormat::Human, CommandSuccess::ChecklistItemShown(data)) => {
473 render_checklist_item_detail(data)
474 }
475 (OutputFormat::Json, CommandSuccess::ChecklistItemShown(data)) => render_json_success(data),
476 (OutputFormat::Human, CommandSuccess::ChecklistItemCreated(data)) => {
477 render_checklist_item_created(data)
478 }
479 (OutputFormat::Json, CommandSuccess::ChecklistItemCreated(data)) => {
480 render_json_success(data)
481 }
482 (OutputFormat::Human, CommandSuccess::ChecklistItemUpdated(data)) => {
483 render_checklist_item_updated(data)
484 }
485 (OutputFormat::Json, CommandSuccess::ChecklistItemUpdated(data)) => {
486 render_json_success(data)
487 }
488 (OutputFormat::Human, CommandSuccess::ChecklistItemDeleted(data)) => {
489 render_checklist_item_deleted(data)
490 }
491 (OutputFormat::Json, CommandSuccess::ChecklistItemDeleted(data)) => {
492 render_json_success(data)
493 }
494 (OutputFormat::Human, CommandSuccess::CustomFieldCollection(data)) => {
495 custom_fields::collection(data)
496 }
497 (OutputFormat::Json, CommandSuccess::CustomFieldCollection(data)) => {
498 render_json_success(data)
499 }
500 (OutputFormat::Human, CommandSuccess::CustomFieldShown(data)) => {
501 custom_fields::detail(data)
502 }
503 (OutputFormat::Json, CommandSuccess::CustomFieldShown(data)) => render_json_success(data),
504 (OutputFormat::Human, CommandSuccess::CustomFieldCreated(data)) => {
505 custom_fields::created(data)
506 }
507 (OutputFormat::Json, CommandSuccess::CustomFieldCreated(data)) => render_json_success(data),
508 (OutputFormat::Human, CommandSuccess::CustomFieldUpdated(data)) => {
509 custom_fields::updated(data)
510 }
511 (OutputFormat::Json, CommandSuccess::CustomFieldUpdated(data)) => render_json_success(data),
512 (OutputFormat::Human, CommandSuccess::CustomFieldDeleted(data)) => {
513 custom_fields::deleted(data)
514 }
515 (OutputFormat::Json, CommandSuccess::CustomFieldDeleted(data)) => render_json_success(data),
516 (OutputFormat::Human, CommandSuccess::CustomFieldItemsCreated(data)) => {
517 custom_field_items::created(data)
518 }
519 (OutputFormat::Json, CommandSuccess::CustomFieldItemsCreated(data)) => {
520 render_json_success(data)
521 }
522 (OutputFormat::Human, CommandSuccess::CustomFieldItemUpdated(data)) => {
523 custom_field_items::updated(data)
524 }
525 (OutputFormat::Json, CommandSuccess::CustomFieldItemUpdated(data)) => {
526 render_json_success(data)
527 }
528 (OutputFormat::Human, CommandSuccess::CustomFieldItemDeleted(data)) => {
529 custom_field_items::deleted(data)
530 }
531 (OutputFormat::Json, CommandSuccess::CustomFieldItemDeleted(data)) => {
532 render_json_success(data)
533 }
534 (OutputFormat::Human, CommandSuccess::CardCustomFieldSet(data)) => {
535 cards::render_card_custom_field_set(data)
536 }
537 (OutputFormat::Json, CommandSuccess::CardCustomFieldSet(data)) => render_json_success(data),
538 (OutputFormat::Human, CommandSuccess::CardCustomFieldQuery(data)) => {
539 cards::render_card_custom_field_query(data)
540 }
541 (OutputFormat::Json, CommandSuccess::CardCustomFieldQuery(data)) => {
542 render_json_success(data)
543 }
544 (OutputFormat::Human, CommandSuccess::CommentCollection(data)) => {
545 render_comment_collection(data)
546 }
547 (OutputFormat::Json, CommandSuccess::CommentCollection(data)) => render_json_success(data),
548 (OutputFormat::Human, CommandSuccess::CommentShown(data)) => render_comment_detail(data),
549 (OutputFormat::Json, CommandSuccess::CommentShown(data)) => render_json_success(data),
550 (OutputFormat::Human, CommandSuccess::CommentCreated(data)) => render_comment_created(data),
551 (OutputFormat::Json, CommandSuccess::CommentCreated(data)) => render_json_success(data),
552 (OutputFormat::Human, CommandSuccess::CommentDeleted(data)) => render_comment_deleted(data),
553 (OutputFormat::Json, CommandSuccess::CommentDeleted(data)) => render_json_success(data),
554 (OutputFormat::Human, CommandSuccess::SwimlaneCollection(data)) => {
555 render_swimlane_collection(data)
556 }
557 (OutputFormat::Json, CommandSuccess::SwimlaneCollection(data)) => render_json_success(data),
558 (OutputFormat::Human, CommandSuccess::SwimlaneShown(data)) => render_swimlane_detail(data),
559 (OutputFormat::Json, CommandSuccess::SwimlaneShown(data)) => render_json_success(data),
560 (OutputFormat::Human, CommandSuccess::SwimlaneCreated(data)) => {
561 render_swimlane_created(data)
562 }
563 (OutputFormat::Json, CommandSuccess::SwimlaneCreated(data)) => render_json_success(data),
564 (OutputFormat::Human, CommandSuccess::SwimlaneUpdated(data)) => {
565 render_swimlane_updated(data)
566 }
567 (OutputFormat::Json, CommandSuccess::SwimlaneUpdated(data)) => render_json_success(data),
568 (OutputFormat::Human, CommandSuccess::SwimlaneDeleted(data)) => {
569 render_swimlane_deleted(data)
570 }
571 (OutputFormat::Json, CommandSuccess::SwimlaneDeleted(data)) => render_json_success(data),
572 (OutputFormat::Human, CommandSuccess::ProfileAdded(data)) => {
573 render_profile_item("Added profile", data)
574 }
575 (OutputFormat::Json, CommandSuccess::ProfileAdded(data))
576 | (OutputFormat::Json, CommandSuccess::ProfileShown(data))
577 | (OutputFormat::Json, CommandSuccess::ProfileUsed(data))
578 | (OutputFormat::Json, CommandSuccess::ProfileUpdated(data)) => {
579 serde_json::to_string(&SuccessEnvelope { ok: true, data })
580 .expect("profile success is always serializable")
581 }
582 (OutputFormat::Human, CommandSuccess::ProfileList(data)) => render_profile_list(data),
583 (OutputFormat::Json, CommandSuccess::ProfileList(data)) => {
584 serde_json::to_string(&SuccessEnvelope { ok: true, data })
585 .expect("profile list success is always serializable")
586 }
587 (OutputFormat::Human, CommandSuccess::ProfileShown(data)) => {
588 render_profile_item("Profile", data)
589 }
590 (OutputFormat::Human, CommandSuccess::ProfileUsed(data)) => {
591 render_profile_item("Using profile", data)
592 }
593 (OutputFormat::Human, CommandSuccess::ProfileUpdated(data)) => {
594 render_profile_item("Updated profile", data)
595 }
596 (OutputFormat::Human, CommandSuccess::ProfileRemoved(data)) => render_profile_removed(data),
597 (OutputFormat::Json, CommandSuccess::ProfileRemoved(data)) => {
598 serde_json::to_string(&SuccessEnvelope { ok: true, data })
599 .expect("profile removal success is always serializable")
600 }
601 }
602}
603
604pub async fn write_success(
605 format: OutputFormat,
606 success: CommandSuccess,
607 stdout: &mut dyn Write,
608 stderr: &mut dyn Write,
609) -> Result<ExitCode, AppError> {
610 if let CommandSuccess::ApiResponse(response) = success {
611 return raw::write_response(format, response, stdout, stderr).await;
612 }
613 if format == OutputFormat::Raw {
614 if let CommandSuccess::AttachmentDownloaded(data) = &success {
615 let bytes = data.bytes.as_deref().ok_or_else(|| {
616 AppError::invalid_input("raw attachment output requires --destination -")
617 })?;
618 stdout
619 .write_all(bytes)
620 .and_then(|()| stdout.flush())
621 .map_err(output_write_error)?;
622 return Ok(ExitCode::SUCCESS);
623 }
624 }
625 if format == OutputFormat::Raw {
626 if let CommandSuccess::Exported(data) = &success {
627 let staged = data.staged.as_ref().ok_or_else(|| {
628 AppError::invalid_input("raw export output requires --destination -")
629 })?;
630 let mut file = staged.0.reopen().map_err(export_read_error)?;
631 let mut buffer = [0; 64 * 1024];
632 loop {
633 let read = file.read(&mut buffer).map_err(export_read_error)?;
634 if read == 0 {
635 break;
636 }
637 stdout
638 .write_all(&buffer[..read])
639 .map_err(output_write_error)?;
640 }
641 stdout.flush().map_err(output_write_error)?;
642 return Ok(ExitCode::SUCCESS);
643 }
644 }
645 let rendered = render_success(format, &success);
646 if !rendered.is_empty() {
647 let written = writeln!(stdout, "{rendered}").and_then(|()| {
648 if matches!(&success, CommandSuccess::AttachmentDownloaded(data) if data.file_saved)
649 || matches!(&success, CommandSuccess::Exported(data) if data.file_saved)
650 {
651 stdout.flush()
652 } else {
653 Ok(())
654 }
655 });
656 written.map_err(|e| {
657 let error = output_write_error(e);
658 if let CommandSuccess::Exported(data) = &success {
659 if data.file_saved {
660 return error.with_details(crate::error::ErrorDetails {
661 file_saved: Some(true),
662 destination: Some(data.destination.clone()),
663 ..crate::error::ErrorDetails::default()
664 });
665 }
666 }
667 if let CommandSuccess::AttachmentDownloaded(data) = &success {
668 if data.file_saved {
669 return error.with_details(crate::error::ErrorDetails {
670 file_saved: Some(true),
671 destination: Some(data.destination.clone()),
672 ..crate::error::ErrorDetails::default()
673 });
674 }
675 }
676 error
677 })?;
678 }
679 Ok(ExitCode::SUCCESS)
680}
681fn export_read_error(error: std::io::Error) -> AppError {
682 AppError::new(
683 ErrorCode::FileIoError,
684 format!(
685 "could not read the staged export file: {}",
686 crate::input::escape_terminal_text(&error.to_string())
687 ),
688 StableExitCode::Transport,
689 )
690}
691pub(crate) fn output_write_error(error: std::io::Error) -> AppError {
692 AppError::new(
693 ErrorCode::InternalError,
694 format!("could not write command output: {error}"),
695 StableExitCode::Transport,
696 )
697}
698
699pub(crate) fn render_json_success<T: Serialize>(data: &T) -> String {
700 serde_json::to_string(&SuccessEnvelope { ok: true, data })
701 .expect("command success is always serializable")
702}
703
704pub fn render_error(format: OutputFormat, error: &AppError) -> String {
705 match format {
706 OutputFormat::Human => {
707 let mut rendered = format!(
708 "Error [{}]: {}",
709 error.code().as_str(),
710 error.message().trim()
711 );
712 if let Some(batch) = &error.details().batch_result {
713 rendered.push('\n');
714 rendered.push_str(&cards::render_batch(batch));
715 }
716 if let Some(ids) = &error.details().invalid_label_ids {
717 rendered.push_str(&format!(
718 "\nInvalid label IDs: {}",
719 formatting::escape_terminal_controls(&ids.join(", "))
720 ));
721 }
722 if let Some(response) = &error.details().response {
723 rendered.push('\n');
724 rendered.push_str(&render_api_response_human(response));
725 }
726 if error.details().file_saved == Some(true) {
727 if let Some(destination) = &error.details().destination {
728 rendered.push_str(&format!(
729 "\nDownload saved to `{}`; completion output failed.",
730 formatting::escape_terminal_controls(destination)
731 ));
732 }
733 }
734 rendered
735 }
736 OutputFormat::Json => serde_json::to_string(&ErrorEnvelope {
737 ok: false,
738 error: ErrorBody {
739 code: error.code().as_str(),
740 message: error.message(),
741 details: error.details(),
742 },
743 })
744 .expect("application errors are always serializable"),
745 OutputFormat::Raw => format!(
746 "Error [{}]: {}",
747 error.code().as_str(),
748 error.message().trim()
749 ),
750 }
751}
752
753#[cfg(test)]
754mod tests;