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