Skip to main content

wekan_cli/
output.rs

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;