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