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