Skip to main content

wekan_cli/
output.rs

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))
129        | (OutputFormat::Human, CommandSuccess::UserUpdated(data)) => render_user_detail(data),
130        (OutputFormat::Json, CommandSuccess::UserCurrent(data))
131        | (OutputFormat::Json, CommandSuccess::UserShown(data))
132        | (OutputFormat::Json, CommandSuccess::UserUpdated(data)) => render_json_success(data),
133        (OutputFormat::Human, CommandSuccess::UserList(data)) => render_user_list(data),
134        (OutputFormat::Json, CommandSuccess::UserList(data)) => render_json_success(data),
135        (OutputFormat::Human, CommandSuccess::UserCards(data)) => render_user_cards(data),
136        (OutputFormat::Json, CommandSuccess::UserCards(data)) => render_json_success(data),
137        (OutputFormat::Human, CommandSuccess::UserCreated(data)) => render_user_created(data),
138        (OutputFormat::Json, CommandSuccess::UserCreated(data)) => render_json_success(data),
139        (OutputFormat::Human, CommandSuccess::UserBoards(data)) => render_user_boards(data),
140        (OutputFormat::Json, CommandSuccess::UserBoards(data)) => render_json_success(data),
141        (OutputFormat::Human, CommandSuccess::UserOwnershipTaken(data)) => {
142            render_user_ownership(data)
143        }
144        (OutputFormat::Json, CommandSuccess::UserOwnershipTaken(data)) => render_json_success(data),
145        (OutputFormat::Human, CommandSuccess::UserLoginChanged(data)) => {
146            render_user_login_change(data)
147        }
148        (OutputFormat::Json, CommandSuccess::UserLoginChanged(data)) => render_json_success(data),
149        (OutputFormat::Human, CommandSuccess::UserDeleted(data)) => render_user_deleted(data),
150        (OutputFormat::Json, CommandSuccess::UserDeleted(data)) => render_json_success(data),
151        (OutputFormat::Human, CommandSuccess::BoardList(data)) => render_board_list(data),
152        (OutputFormat::Json, CommandSuccess::BoardList(data)) => render_json_success(data),
153        (OutputFormat::Human, CommandSuccess::BoardCount(data)) => render_board_count(data),
154        (OutputFormat::Json, CommandSuccess::BoardCount(data)) => render_json_success(data),
155        (OutputFormat::Human, CommandSuccess::BoardShown(data)) => render_board_detail(data),
156        (OutputFormat::Json, CommandSuccess::BoardShown(data)) => render_json_success(data),
157        (OutputFormat::Human, CommandSuccess::BoardCreated(data)) => render_board_created(data),
158        (OutputFormat::Json, CommandSuccess::BoardCreated(data)) => render_json_success(data),
159        (OutputFormat::Human, CommandSuccess::BoardRenamed(data)) => render_board_renamed(data),
160        (OutputFormat::Json, CommandSuccess::BoardRenamed(data)) => render_json_success(data),
161        (OutputFormat::Human, CommandSuccess::BoardDeleted(data)) => render_board_deleted(data),
162        (OutputFormat::Json, CommandSuccess::BoardDeleted(data)) => render_json_success(data),
163        (OutputFormat::Human, CommandSuccess::ListCollection(data)) => render_list_collection(data),
164        (OutputFormat::Json, CommandSuccess::ListCollection(data)) => render_json_success(data),
165        (OutputFormat::Human, CommandSuccess::ListShown(data)) => render_list_detail(data),
166        (OutputFormat::Json, CommandSuccess::ListShown(data)) => render_json_success(data),
167        (OutputFormat::Human, CommandSuccess::ListCreated(data)) => render_list_created(data),
168        (OutputFormat::Json, CommandSuccess::ListCreated(data)) => render_json_success(data),
169        (OutputFormat::Human, CommandSuccess::ListUpdated(data)) => render_list_updated(data),
170        (OutputFormat::Json, CommandSuccess::ListUpdated(data)) => render_json_success(data),
171        (OutputFormat::Human, CommandSuccess::ListDeleted(data)) => render_list_deleted(data),
172        (OutputFormat::Json, CommandSuccess::ListDeleted(data)) => render_json_success(data),
173        (OutputFormat::Human, CommandSuccess::CardCollection(data)) => render_card_collection(data),
174        (OutputFormat::Json, CommandSuccess::CardCollection(data)) => render_json_success(data),
175        (OutputFormat::Human, CommandSuccess::CardShown(data)) => render_card_detail(data),
176        (OutputFormat::Json, CommandSuccess::CardShown(data)) => render_json_success(data),
177        (OutputFormat::Human, CommandSuccess::CardCreated(data)) => render_card_created(data),
178        (OutputFormat::Json, CommandSuccess::CardCreated(data)) => render_json_success(data),
179        (OutputFormat::Human, CommandSuccess::CardUpdated(data)) => render_card_updated(data),
180        (OutputFormat::Json, CommandSuccess::CardUpdated(data)) => render_json_success(data),
181        (OutputFormat::Human, CommandSuccess::CardArchived(data)) => render_card_archived(data),
182        (OutputFormat::Json, CommandSuccess::CardArchived(data)) => render_json_success(data),
183        (OutputFormat::Human, CommandSuccess::CardRestored(data)) => render_card_restored(data),
184        (OutputFormat::Json, CommandSuccess::CardRestored(data)) => render_json_success(data),
185        (OutputFormat::Human, CommandSuccess::CardDeleted(data)) => render_card_deleted(data),
186        (OutputFormat::Json, CommandSuccess::CardDeleted(data)) => render_json_success(data),
187        (OutputFormat::Human, CommandSuccess::CommentCollection(data)) => {
188            render_comment_collection(data)
189        }
190        (OutputFormat::Json, CommandSuccess::CommentCollection(data)) => render_json_success(data),
191        (OutputFormat::Human, CommandSuccess::CommentShown(data)) => render_comment_detail(data),
192        (OutputFormat::Json, CommandSuccess::CommentShown(data)) => render_json_success(data),
193        (OutputFormat::Human, CommandSuccess::CommentCreated(data)) => render_comment_created(data),
194        (OutputFormat::Json, CommandSuccess::CommentCreated(data)) => render_json_success(data),
195        (OutputFormat::Human, CommandSuccess::CommentDeleted(data)) => render_comment_deleted(data),
196        (OutputFormat::Json, CommandSuccess::CommentDeleted(data)) => render_json_success(data),
197        (OutputFormat::Human, CommandSuccess::SwimlaneCollection(data)) => {
198            render_swimlane_collection(data)
199        }
200        (OutputFormat::Json, CommandSuccess::SwimlaneCollection(data)) => render_json_success(data),
201        (OutputFormat::Human, CommandSuccess::SwimlaneShown(data)) => render_swimlane_detail(data),
202        (OutputFormat::Json, CommandSuccess::SwimlaneShown(data)) => render_json_success(data),
203        (OutputFormat::Human, CommandSuccess::SwimlaneCreated(data)) => {
204            render_swimlane_created(data)
205        }
206        (OutputFormat::Json, CommandSuccess::SwimlaneCreated(data)) => render_json_success(data),
207        (OutputFormat::Human, CommandSuccess::SwimlaneUpdated(data)) => {
208            render_swimlane_updated(data)
209        }
210        (OutputFormat::Json, CommandSuccess::SwimlaneUpdated(data)) => render_json_success(data),
211        (OutputFormat::Human, CommandSuccess::SwimlaneDeleted(data)) => {
212            render_swimlane_deleted(data)
213        }
214        (OutputFormat::Json, CommandSuccess::SwimlaneDeleted(data)) => render_json_success(data),
215        (OutputFormat::Human, CommandSuccess::ProfileAdded(data)) => {
216            render_profile_item("Added profile", data)
217        }
218        (OutputFormat::Json, CommandSuccess::ProfileAdded(data))
219        | (OutputFormat::Json, CommandSuccess::ProfileShown(data))
220        | (OutputFormat::Json, CommandSuccess::ProfileUsed(data))
221        | (OutputFormat::Json, CommandSuccess::ProfileUpdated(data)) => {
222            serde_json::to_string(&SuccessEnvelope { ok: true, data })
223                .expect("profile success is always serializable")
224        }
225        (OutputFormat::Human, CommandSuccess::ProfileList(data)) => render_profile_list(data),
226        (OutputFormat::Json, CommandSuccess::ProfileList(data)) => {
227            serde_json::to_string(&SuccessEnvelope { ok: true, data })
228                .expect("profile list success is always serializable")
229        }
230        (OutputFormat::Human, CommandSuccess::ProfileShown(data)) => {
231            render_profile_item("Profile", data)
232        }
233        (OutputFormat::Human, CommandSuccess::ProfileUsed(data)) => {
234            render_profile_item("Using profile", data)
235        }
236        (OutputFormat::Human, CommandSuccess::ProfileUpdated(data)) => {
237            render_profile_item("Updated profile", data)
238        }
239        (OutputFormat::Human, CommandSuccess::ProfileRemoved(data)) => render_profile_removed(data),
240        (OutputFormat::Json, CommandSuccess::ProfileRemoved(data)) => {
241            serde_json::to_string(&SuccessEnvelope { ok: true, data })
242                .expect("profile removal success is always serializable")
243        }
244    }
245}
246
247pub async fn write_success(
248    format: OutputFormat,
249    success: CommandSuccess,
250    stdout: &mut dyn Write,
251    stderr: &mut dyn Write,
252) -> Result<ExitCode, AppError> {
253    if let CommandSuccess::ApiResponse(response) = success {
254        return raw::write_response(format, response, stdout, stderr).await;
255    }
256    let rendered = render_success(format, &success);
257    if !rendered.is_empty() {
258        writeln!(stdout, "{rendered}").map_err(output_write_error)?;
259    }
260    Ok(ExitCode::SUCCESS)
261}
262pub(crate) fn output_write_error(error: std::io::Error) -> AppError {
263    AppError::new(
264        ErrorCode::InternalError,
265        format!("could not write command output: {error}"),
266        StableExitCode::Transport,
267    )
268}
269
270pub(crate) fn render_json_success<T: Serialize>(data: &T) -> String {
271    serde_json::to_string(&SuccessEnvelope { ok: true, data })
272        .expect("command success is always serializable")
273}
274
275pub fn render_error(format: OutputFormat, error: &AppError) -> String {
276    match format {
277        OutputFormat::Human => {
278            let mut rendered = format!(
279                "Error [{}]: {}",
280                error.code().as_str(),
281                error.message().trim()
282            );
283            if let Some(response) = &error.details().response {
284                rendered.push('\n');
285                rendered.push_str(&render_api_response_human(response));
286            }
287            rendered
288        }
289        OutputFormat::Json => serde_json::to_string(&ErrorEnvelope {
290            ok: false,
291            error: ErrorBody {
292                code: error.code().as_str(),
293                message: error.message(),
294                details: error.details(),
295            },
296        })
297        .expect("application errors are always serializable"),
298        OutputFormat::Raw => format!(
299            "Error [{}]: {}",
300            error.code().as_str(),
301            error.message().trim()
302        ),
303    }
304}
305
306#[cfg(test)]
307mod tests;