tftio_cli_common/
runner.rs1use crate::err_response;
4use serde_json::json;
5use std::fmt::Display;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct FatalCliError {
10 command: String,
11 json_output: bool,
12 message: String,
13}
14
15impl FatalCliError {
16 #[must_use]
18 pub fn new(command: impl Into<String>, json_output: bool, message: impl Into<String>) -> Self {
19 Self {
20 command: command.into(),
21 json_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 fn json_output(&self) -> bool {
35 self.json_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.json_output {
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.json_output {
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, json_output: bool, 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, json_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 = run_with_fatal_handler(|| Err(fatal_error("scan", false, "bad")));
140 assert_eq!(exit_code, 1);
141 }
142
143 #[test]
144 fn parse_and_run_passes_parsed_value_to_runner() {
145 let exit_code = parse_and_run(
146 || String::from("parsed"),
147 |cli| {
148 if cli == "parsed" {
149 Ok(0)
150 } else {
151 Err(fatal_error("scan", false, "unexpected cli"))
152 }
153 },
154 );
155 assert_eq!(exit_code, 0);
156 }
157
158 #[test]
159 fn fatal_cli_error_renders_json_when_requested() {
160 let rendered = FatalCliError::new("scan", true, "bad").render();
161 assert!(rendered.contains("\"ok\":false"));
162 assert!(rendered.contains("\"code\":\"ERROR\""));
163 assert!(rendered.contains("\"command\":\"scan\""));
164 }
165
166 #[test]
167 fn run_with_display_error_handler_returns_success_code() {
168 let exit_code =
169 run_with_display_error_handler("scan", false, || Ok::<i32, DisplayOnlyError>(9));
170 assert_eq!(exit_code, 9);
171 }
172
173 #[test]
174 fn run_with_display_error_handler_converts_display_errors() {
175 let exit_code = run_with_display_error_handler("scan", false, || {
176 Err::<i32, DisplayOnlyError>(DisplayOnlyError("bad"))
177 });
178 assert_eq!(exit_code, 1);
179 }
180}