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