Skip to main content

wekan_cli/
output.rs

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;