tftio_cli_common/
runner.rs1use crate::{JsonOutput, err_response};
4use serde_json::json;
5use std::fmt::Display;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct FatalCliError {
10 command: String,
11 output: JsonOutput,
12 message: String,
13}
14
15impl FatalCliError {
16 #[must_use]
18 pub fn new(command: impl Into<String>, output: JsonOutput, message: impl Into<String>) -> Self {
19 Self {
20 command: command.into(),
21 output,
22 message: message.into(),
23 }
24 }
25
26 #[must_use]
28 pub fn command(&self) -> &str {
29 &self.command
30 }
31
32 #[must_use]
34 pub const fn output(&self) -> JsonOutput {
35 self.output
36 }
37
38 #[must_use]
40 pub fn message(&self) -> &str {
41 &self.message
42 }
43
44 #[must_use]
46 pub fn render(&self) -> String {
47 if self.output.is_json() {
48 err_response(self.command(), "ERROR", self.message(), json!({})).to_string()
49 } else {
50 format!("error: {}", self.message())
51 }
52 }
53
54 pub fn emit(&self) {
56 if self.output.is_json() {
57 println!("{}", self.render());
58 } else {
59 eprintln!("{}", self.render());
60 }
61 }
62
63 #[must_use]
65 pub fn emit_and_exit_code(self) -> i32 {
66 self.emit();
67 1
68 }
69}
70
71#[must_use]
73pub fn run_with_fatal_handler<F>(run: F) -> i32
74where
75 F: FnOnce() -> Result<i32, FatalCliError>,
76{
77 match run() {
78 Ok(exit_code) => exit_code,
79 Err(error) => error.emit_and_exit_code(),
80 }
81}
82
83#[must_use]
85pub fn run_with_display_error_handler<F, E>(command: &str, output: JsonOutput, run: F) -> i32
86where
87 F: FnOnce() -> Result<i32, E>,
88 E: Display,
89{
90 run_with_fatal_handler(|| {
91 run().map_err(|error| FatalCliError::new(command, output, error.to_string()))
92 })
93}
94
95#[must_use]
97pub fn parse_and_run<T, P, F>(parse: P, run: F) -> i32
98where
99 P: FnOnce() -> T,
100 F: FnOnce(T) -> Result<i32, FatalCliError>,
101{
102 run_with_fatal_handler(|| run(parse()))
103}
104
105pub fn parse_and_exit<T, P, F>(parse: P, run: F) -> !
107where
108 P: FnOnce() -> T,
109 F: FnOnce(T) -> Result<i32, FatalCliError>,
110{
111 std::process::exit(parse_and_run(parse, run))
112}
113
114#[cfg(test)]
115mod tests {
116 use std::fmt;
117
118 use crate::error::fatal_error;
119
120 use super::*;
121
122 #[derive(Debug)]
123 struct DisplayOnlyError(&'static str);
124
125 impl fmt::Display for DisplayOnlyError {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 write!(f, "{}", self.0)
128 }
129 }
130
131 #[test]
132 fn run_with_fatal_handler_returns_success_code() {
133 let exit_code = run_with_fatal_handler(|| Ok(7));
134 assert_eq!(exit_code, 7);
135 }
136
137 #[test]
138 fn run_with_fatal_handler_converts_fatal_error_to_failure_code() {
139 let exit_code =
140 run_with_fatal_handler(|| Err(fatal_error("scan", JsonOutput::Text, "bad")));
141 assert_eq!(exit_code, 1);
142 }
143
144 #[test]
145 fn parse_and_run_passes_parsed_value_to_runner() {
146 let exit_code = parse_and_run(
147 || String::from("parsed"),
148 |cli| {
149 if cli == "parsed" {
150 Ok(0)
151 } else {
152 Err(fatal_error("scan", JsonOutput::Text, "unexpected cli"))
153 }
154 },
155 );
156 assert_eq!(exit_code, 0);
157 }
158
159 #[test]
160 fn fatal_cli_error_renders_json_when_requested() {
161 let rendered = FatalCliError::new("scan", JsonOutput::Json, "bad").render();
162 assert!(rendered.contains("\"ok\":false"));
163 assert!(rendered.contains("\"code\":\"ERROR\""));
164 assert!(rendered.contains("\"command\":\"scan\""));
165 }
166
167 #[test]
168 fn run_with_display_error_handler_returns_success_code() {
169 let exit_code = run_with_display_error_handler("scan", JsonOutput::Text, || {
170 Ok::<i32, DisplayOnlyError>(9)
171 });
172 assert_eq!(exit_code, 9);
173 }
174
175 #[test]
176 fn run_with_display_error_handler_converts_display_errors() {
177 let exit_code = run_with_display_error_handler("scan", JsonOutput::Text, || {
178 Err::<i32, DisplayOnlyError>(DisplayOnlyError("bad"))
179 });
180 assert_eq!(exit_code, 1);
181 }
182}