1mod auth;
2mod boards;
3mod cards;
4mod checklist_items;
5mod checklists;
6mod comments;
7mod custom_field_items;
8mod custom_fields;
9mod formatting;
10mod lists;
11mod profiles;
12mod raw;
13mod swimlanes;
14mod users;
15
16use auth::{render_auth_status, render_auth_success, render_logout};
17use boards::{
18 render_board_count, render_board_created, render_board_deleted, render_board_detail,
19 render_board_list, render_board_renamed,
20};
21use cards::{
22 render_card_archived, render_card_collection, render_card_created, render_card_deleted,
23 render_card_detail, render_card_restored, render_card_updated,
24};
25use checklist_items::{
26 render_checklist_item_created, render_checklist_item_deleted, render_checklist_item_detail,
27 render_checklist_item_updated,
28};
29use checklists::{
30 render_checklist_collection, render_checklist_created, render_checklist_deleted,
31 render_checklist_detail,
32};
33use comments::{
34 render_comment_collection, render_comment_created, render_comment_deleted,
35 render_comment_detail,
36};
37use lists::{
38 render_list_collection, render_list_created, render_list_deleted, render_list_detail,
39 render_list_updated,
40};
41use profiles::{render_profile_item, render_profile_list, render_profile_removed};
42use swimlanes::{
43 render_swimlane_collection, render_swimlane_created, render_swimlane_deleted,
44 render_swimlane_detail, render_swimlane_updated,
45};
46use users::{
47 render_user_boards, render_user_cards, render_user_created, render_user_deleted,
48 render_user_detail, render_user_list, render_user_login_change, render_user_ownership,
49};
50
51use std::{ffi::OsString, io::Write, process::ExitCode};
52
53use clap::ValueEnum;
54use serde::Serialize;
55
56use crate::command_result::CommandSuccess;
57use crate::{
58 error::{AppError, ErrorCode},
59 exit_code::StableExitCode,
60};
61
62pub(crate) use raw::render_human_response as render_api_response_human;
63#[cfg(test)]
64pub(crate) use raw::structured_api_response;
65
66#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
67#[value(rename_all = "lower")]
68pub enum OutputFormat {
69 #[default]
70 Human,
71 Json,
72 Raw,
73}
74
75impl OutputFormat {
76 pub fn detect_from_args(args: &[OsString]) -> Self {
77 let mut iter = args.iter().filter_map(|arg| arg.to_str());
78 while let Some(arg) = iter.next() {
79 if arg == "--output" {
80 return match iter.next() {
81 Some("json") => Self::Json,
82 Some("raw") => Self::Raw,
83 _ => Self::Human,
84 };
85 }
86 if arg == "--output=json" {
87 return Self::Json;
88 }
89 if arg == "--output=raw" {
90 return Self::Raw;
91 }
92 }
93 Self::Human
94 }
95}
96
97#[derive(Serialize)]
98struct SuccessEnvelope<'a, T> {
99 ok: bool,
100 data: &'a T,
101}
102
103#[derive(Serialize)]
104struct ErrorEnvelope<'a> {
105 ok: bool,
106 error: ErrorBody<'a>,
107}
108
109#[derive(Serialize)]
110struct ErrorBody<'a> {
111 code: &'static str,
112 message: &'a str,
113 details: &'a crate::error::ErrorDetails,
114}
115
116pub fn render_success(format: OutputFormat, success: &CommandSuccess) -> String {
117 match (format, success) {
118 (_, CommandSuccess::ApiResponse(_)) => {
119 panic!("streaming API responses must be consumed by output::write_success")
120 }
121 (OutputFormat::Raw, CommandSuccess::Cancelled(_)) => String::new(),
122 (OutputFormat::Raw, _) => panic!("raw output is valid only for api request"),
123 (OutputFormat::Human, CommandSuccess::Cancelled(_)) => {
124 "Cancelled; no changes made.".to_owned()
125 }
126 (OutputFormat::Json, CommandSuccess::Cancelled(data)) => render_json_success(data),
127 (OutputFormat::Human, CommandSuccess::Registration(data)) => {
128 render_auth_success("Registered user", data)
129 }
130 (OutputFormat::Json, CommandSuccess::Registration(data)) => render_json_success(data),
131 (OutputFormat::Human, CommandSuccess::Login(data)) => {
132 render_auth_success("Logged in user", data)
133 }
134 (OutputFormat::Json, CommandSuccess::Login(data)) => render_json_success(data),
135 (OutputFormat::Human, CommandSuccess::Logout(data)) => render_logout(data),
136 (OutputFormat::Json, CommandSuccess::Logout(data)) => render_json_success(data),
137 (OutputFormat::Human, CommandSuccess::AuthStatus(data)) => render_auth_status(data),
138 (OutputFormat::Json, CommandSuccess::AuthStatus(data)) => render_json_success(data),
139 (OutputFormat::Human, CommandSuccess::UserCurrent(data))
140 | (OutputFormat::Human, CommandSuccess::UserShown(data))
141 | (OutputFormat::Human, CommandSuccess::UserUpdated(data)) => render_user_detail(data),
142 (OutputFormat::Json, CommandSuccess::UserCurrent(data))
143 | (OutputFormat::Json, CommandSuccess::UserShown(data))
144 | (OutputFormat::Json, CommandSuccess::UserUpdated(data)) => render_json_success(data),
145 (OutputFormat::Human, CommandSuccess::UserList(data)) => render_user_list(data),
146 (OutputFormat::Json, CommandSuccess::UserList(data)) => render_json_success(data),
147 (OutputFormat::Human, CommandSuccess::UserCards(data)) => render_user_cards(data),
148 (OutputFormat::Json, CommandSuccess::UserCards(data)) => render_json_success(data),
149 (OutputFormat::Human, CommandSuccess::UserCreated(data)) => render_user_created(data),
150 (OutputFormat::Json, CommandSuccess::UserCreated(data)) => render_json_success(data),
151 (OutputFormat::Human, CommandSuccess::UserBoards(data)) => render_user_boards(data),
152 (OutputFormat::Json, CommandSuccess::UserBoards(data)) => render_json_success(data),
153 (OutputFormat::Human, CommandSuccess::UserOwnershipTaken(data)) => {
154 render_user_ownership(data)
155 }
156 (OutputFormat::Json, CommandSuccess::UserOwnershipTaken(data)) => render_json_success(data),
157 (OutputFormat::Human, CommandSuccess::UserLoginChanged(data)) => {
158 render_user_login_change(data)
159 }
160 (OutputFormat::Json, CommandSuccess::UserLoginChanged(data)) => render_json_success(data),
161 (OutputFormat::Human, CommandSuccess::UserDeleted(data)) => render_user_deleted(data),
162 (OutputFormat::Json, CommandSuccess::UserDeleted(data)) => render_json_success(data),
163 (OutputFormat::Human, CommandSuccess::BoardList(data)) => render_board_list(data),
164 (OutputFormat::Json, CommandSuccess::BoardList(data)) => render_json_success(data),
165 (OutputFormat::Human, CommandSuccess::BoardCount(data)) => render_board_count(data),
166 (OutputFormat::Json, CommandSuccess::BoardCount(data)) => render_json_success(data),
167 (OutputFormat::Human, CommandSuccess::BoardShown(data)) => render_board_detail(data),
168 (OutputFormat::Json, CommandSuccess::BoardShown(data)) => render_json_success(data),
169 (OutputFormat::Human, CommandSuccess::BoardCreated(data)) => render_board_created(data),
170 (OutputFormat::Json, CommandSuccess::BoardCreated(data)) => render_json_success(data),
171 (OutputFormat::Human, CommandSuccess::BoardRenamed(data)) => render_board_renamed(data),
172 (OutputFormat::Json, CommandSuccess::BoardRenamed(data)) => render_json_success(data),
173 (OutputFormat::Human, CommandSuccess::BoardDeleted(data)) => render_board_deleted(data),
174 (OutputFormat::Json, CommandSuccess::BoardDeleted(data)) => render_json_success(data),
175 (OutputFormat::Human, CommandSuccess::ListCollection(data)) => render_list_collection(data),
176 (OutputFormat::Json, CommandSuccess::ListCollection(data)) => render_json_success(data),
177 (OutputFormat::Human, CommandSuccess::ListShown(data)) => render_list_detail(data),
178 (OutputFormat::Json, CommandSuccess::ListShown(data)) => render_json_success(data),
179 (OutputFormat::Human, CommandSuccess::ListCreated(data)) => render_list_created(data),
180 (OutputFormat::Json, CommandSuccess::ListCreated(data)) => render_json_success(data),
181 (OutputFormat::Human, CommandSuccess::ListUpdated(data)) => render_list_updated(data),
182 (OutputFormat::Json, CommandSuccess::ListUpdated(data)) => render_json_success(data),
183 (OutputFormat::Human, CommandSuccess::ListDeleted(data)) => render_list_deleted(data),
184 (OutputFormat::Json, CommandSuccess::ListDeleted(data)) => render_json_success(data),
185 (OutputFormat::Human, CommandSuccess::CardCollection(data)) => render_card_collection(data),
186 (OutputFormat::Json, CommandSuccess::CardCollection(data)) => render_json_success(data),
187 (OutputFormat::Human, CommandSuccess::CardShown(data)) => render_card_detail(data),
188 (OutputFormat::Json, CommandSuccess::CardShown(data)) => render_json_success(data),
189 (OutputFormat::Human, CommandSuccess::CardCreated(data)) => render_card_created(data),
190 (OutputFormat::Json, CommandSuccess::CardCreated(data)) => render_json_success(data),
191 (OutputFormat::Human, CommandSuccess::CardUpdated(data)) => render_card_updated(data),
192 (OutputFormat::Json, CommandSuccess::CardUpdated(data)) => render_json_success(data),
193 (OutputFormat::Human, CommandSuccess::CardArchived(data)) => render_card_archived(data),
194 (OutputFormat::Json, CommandSuccess::CardArchived(data)) => render_json_success(data),
195 (OutputFormat::Human, CommandSuccess::CardRestored(data)) => render_card_restored(data),
196 (OutputFormat::Json, CommandSuccess::CardRestored(data)) => render_json_success(data),
197 (OutputFormat::Human, CommandSuccess::CardDeleted(data)) => render_card_deleted(data),
198 (OutputFormat::Json, CommandSuccess::CardDeleted(data)) => render_json_success(data),
199 (OutputFormat::Human, CommandSuccess::ChecklistCollection(data)) => {
200 render_checklist_collection(data)
201 }
202 (OutputFormat::Json, CommandSuccess::ChecklistCollection(data)) => {
203 render_json_success(data)
204 }
205 (OutputFormat::Human, CommandSuccess::ChecklistShown(data)) => {
206 render_checklist_detail(data)
207 }
208 (OutputFormat::Json, CommandSuccess::ChecklistShown(data)) => render_json_success(data),
209 (OutputFormat::Human, CommandSuccess::ChecklistCreated(data)) => {
210 render_checklist_created(data)
211 }
212 (OutputFormat::Json, CommandSuccess::ChecklistCreated(data)) => render_json_success(data),
213 (OutputFormat::Human, CommandSuccess::ChecklistDeleted(data)) => {
214 render_checklist_deleted(data)
215 }
216 (OutputFormat::Json, CommandSuccess::ChecklistDeleted(data)) => render_json_success(data),
217 (OutputFormat::Human, CommandSuccess::ChecklistItemShown(data)) => {
218 render_checklist_item_detail(data)
219 }
220 (OutputFormat::Json, CommandSuccess::ChecklistItemShown(data)) => render_json_success(data),
221 (OutputFormat::Human, CommandSuccess::ChecklistItemCreated(data)) => {
222 render_checklist_item_created(data)
223 }
224 (OutputFormat::Json, CommandSuccess::ChecklistItemCreated(data)) => {
225 render_json_success(data)
226 }
227 (OutputFormat::Human, CommandSuccess::ChecklistItemUpdated(data)) => {
228 render_checklist_item_updated(data)
229 }
230 (OutputFormat::Json, CommandSuccess::ChecklistItemUpdated(data)) => {
231 render_json_success(data)
232 }
233 (OutputFormat::Human, CommandSuccess::ChecklistItemDeleted(data)) => {
234 render_checklist_item_deleted(data)
235 }
236 (OutputFormat::Json, CommandSuccess::ChecklistItemDeleted(data)) => {
237 render_json_success(data)
238 }
239 (OutputFormat::Human, CommandSuccess::CustomFieldCollection(data)) => {
240 custom_fields::collection(data)
241 }
242 (OutputFormat::Json, CommandSuccess::CustomFieldCollection(data)) => {
243 render_json_success(data)
244 }
245 (OutputFormat::Human, CommandSuccess::CustomFieldShown(data)) => {
246 custom_fields::detail(data)
247 }
248 (OutputFormat::Json, CommandSuccess::CustomFieldShown(data)) => render_json_success(data),
249 (OutputFormat::Human, CommandSuccess::CustomFieldCreated(data)) => {
250 custom_fields::created(data)
251 }
252 (OutputFormat::Json, CommandSuccess::CustomFieldCreated(data)) => render_json_success(data),
253 (OutputFormat::Human, CommandSuccess::CustomFieldUpdated(data)) => {
254 custom_fields::updated(data)
255 }
256 (OutputFormat::Json, CommandSuccess::CustomFieldUpdated(data)) => render_json_success(data),
257 (OutputFormat::Human, CommandSuccess::CustomFieldDeleted(data)) => {
258 custom_fields::deleted(data)
259 }
260 (OutputFormat::Json, CommandSuccess::CustomFieldDeleted(data)) => render_json_success(data),
261 (OutputFormat::Human, CommandSuccess::CustomFieldItemsCreated(data)) => {
262 custom_field_items::created(data)
263 }
264 (OutputFormat::Json, CommandSuccess::CustomFieldItemsCreated(data)) => {
265 render_json_success(data)
266 }
267 (OutputFormat::Human, CommandSuccess::CustomFieldItemUpdated(data)) => {
268 custom_field_items::updated(data)
269 }
270 (OutputFormat::Json, CommandSuccess::CustomFieldItemUpdated(data)) => {
271 render_json_success(data)
272 }
273 (OutputFormat::Human, CommandSuccess::CustomFieldItemDeleted(data)) => {
274 custom_field_items::deleted(data)
275 }
276 (OutputFormat::Json, CommandSuccess::CustomFieldItemDeleted(data)) => {
277 render_json_success(data)
278 }
279 (OutputFormat::Human, CommandSuccess::CardCustomFieldSet(data)) => {
280 cards::render_card_custom_field_set(data)
281 }
282 (OutputFormat::Json, CommandSuccess::CardCustomFieldSet(data)) => render_json_success(data),
283 (OutputFormat::Human, CommandSuccess::CardCustomFieldQuery(data)) => {
284 cards::render_card_custom_field_query(data)
285 }
286 (OutputFormat::Json, CommandSuccess::CardCustomFieldQuery(data)) => {
287 render_json_success(data)
288 }
289 (OutputFormat::Human, CommandSuccess::CommentCollection(data)) => {
290 render_comment_collection(data)
291 }
292 (OutputFormat::Json, CommandSuccess::CommentCollection(data)) => render_json_success(data),
293 (OutputFormat::Human, CommandSuccess::CommentShown(data)) => render_comment_detail(data),
294 (OutputFormat::Json, CommandSuccess::CommentShown(data)) => render_json_success(data),
295 (OutputFormat::Human, CommandSuccess::CommentCreated(data)) => render_comment_created(data),
296 (OutputFormat::Json, CommandSuccess::CommentCreated(data)) => render_json_success(data),
297 (OutputFormat::Human, CommandSuccess::CommentDeleted(data)) => render_comment_deleted(data),
298 (OutputFormat::Json, CommandSuccess::CommentDeleted(data)) => render_json_success(data),
299 (OutputFormat::Human, CommandSuccess::SwimlaneCollection(data)) => {
300 render_swimlane_collection(data)
301 }
302 (OutputFormat::Json, CommandSuccess::SwimlaneCollection(data)) => render_json_success(data),
303 (OutputFormat::Human, CommandSuccess::SwimlaneShown(data)) => render_swimlane_detail(data),
304 (OutputFormat::Json, CommandSuccess::SwimlaneShown(data)) => render_json_success(data),
305 (OutputFormat::Human, CommandSuccess::SwimlaneCreated(data)) => {
306 render_swimlane_created(data)
307 }
308 (OutputFormat::Json, CommandSuccess::SwimlaneCreated(data)) => render_json_success(data),
309 (OutputFormat::Human, CommandSuccess::SwimlaneUpdated(data)) => {
310 render_swimlane_updated(data)
311 }
312 (OutputFormat::Json, CommandSuccess::SwimlaneUpdated(data)) => render_json_success(data),
313 (OutputFormat::Human, CommandSuccess::SwimlaneDeleted(data)) => {
314 render_swimlane_deleted(data)
315 }
316 (OutputFormat::Json, CommandSuccess::SwimlaneDeleted(data)) => render_json_success(data),
317 (OutputFormat::Human, CommandSuccess::ProfileAdded(data)) => {
318 render_profile_item("Added profile", data)
319 }
320 (OutputFormat::Json, CommandSuccess::ProfileAdded(data))
321 | (OutputFormat::Json, CommandSuccess::ProfileShown(data))
322 | (OutputFormat::Json, CommandSuccess::ProfileUsed(data))
323 | (OutputFormat::Json, CommandSuccess::ProfileUpdated(data)) => {
324 serde_json::to_string(&SuccessEnvelope { ok: true, data })
325 .expect("profile success is always serializable")
326 }
327 (OutputFormat::Human, CommandSuccess::ProfileList(data)) => render_profile_list(data),
328 (OutputFormat::Json, CommandSuccess::ProfileList(data)) => {
329 serde_json::to_string(&SuccessEnvelope { ok: true, data })
330 .expect("profile list success is always serializable")
331 }
332 (OutputFormat::Human, CommandSuccess::ProfileShown(data)) => {
333 render_profile_item("Profile", data)
334 }
335 (OutputFormat::Human, CommandSuccess::ProfileUsed(data)) => {
336 render_profile_item("Using profile", data)
337 }
338 (OutputFormat::Human, CommandSuccess::ProfileUpdated(data)) => {
339 render_profile_item("Updated profile", data)
340 }
341 (OutputFormat::Human, CommandSuccess::ProfileRemoved(data)) => render_profile_removed(data),
342 (OutputFormat::Json, CommandSuccess::ProfileRemoved(data)) => {
343 serde_json::to_string(&SuccessEnvelope { ok: true, data })
344 .expect("profile removal success is always serializable")
345 }
346 }
347}
348
349pub async fn write_success(
350 format: OutputFormat,
351 success: CommandSuccess,
352 stdout: &mut dyn Write,
353 stderr: &mut dyn Write,
354) -> Result<ExitCode, AppError> {
355 if let CommandSuccess::ApiResponse(response) = success {
356 return raw::write_response(format, response, stdout, stderr).await;
357 }
358 let rendered = render_success(format, &success);
359 if !rendered.is_empty() {
360 writeln!(stdout, "{rendered}").map_err(output_write_error)?;
361 }
362 Ok(ExitCode::SUCCESS)
363}
364pub(crate) fn output_write_error(error: std::io::Error) -> AppError {
365 AppError::new(
366 ErrorCode::InternalError,
367 format!("could not write command output: {error}"),
368 StableExitCode::Transport,
369 )
370}
371
372pub(crate) fn render_json_success<T: Serialize>(data: &T) -> String {
373 serde_json::to_string(&SuccessEnvelope { ok: true, data })
374 .expect("command success is always serializable")
375}
376
377pub fn render_error(format: OutputFormat, error: &AppError) -> String {
378 match format {
379 OutputFormat::Human => {
380 let mut rendered = format!(
381 "Error [{}]: {}",
382 error.code().as_str(),
383 error.message().trim()
384 );
385 if let Some(response) = &error.details().response {
386 rendered.push('\n');
387 rendered.push_str(&render_api_response_human(response));
388 }
389 rendered
390 }
391 OutputFormat::Json => serde_json::to_string(&ErrorEnvelope {
392 ok: false,
393 error: ErrorBody {
394 code: error.code().as_str(),
395 message: error.message(),
396 details: error.details(),
397 },
398 })
399 .expect("application errors are always serializable"),
400 OutputFormat::Raw => format!(
401 "Error [{}]: {}",
402 error.code().as_str(),
403 error.message().trim()
404 ),
405 }
406}
407
408#[cfg(test)]
409mod tests;