1use std::{
2 fmt::{Debug, Display},
3 io,
4 process::Termination as _T,
5};
6
7use exit_safely::Termination;
8use serde_json::Value::{self, Array};
9use try_v2::Try;
10
11#[derive(Debug, Termination, Try, PartialEq, PartialOrd, Eq, Ord)]
12#[FromResidual(Result<_, Self::Residual>)]
13#[repr(u8)]
14#[must_use]
15pub enum Exit<T: _T> {
16 Ok(T) = 0,
17 Error(WithJson<String>) = 1,
18 InvocationError(WithJson<String>) = 2,
19 IO(WithJson<String>) = 3,
20}
21
22#[derive(Debug, PartialEq, Eq, Default)]
23pub struct WithJson<T> {
24 pub value: T,
25 pub json: Option<Value>,
26}
27
28impl<T> _T for WithJson<T>
29where
30 T: _T,
31{
32 fn report(self) -> std::process::ExitCode {
33 if let Some(json) = self.json {
34 println!("{json}");
35 };
36 self.value.report()
37 }
38}
39
40impl<T> Display for WithJson<T>
41where
42 T: Display,
43{
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 self.value.fmt(f)?;
46 if let Some(json) = self.json.clone() {
47 write!(f, "\n{}", json)?;
48 };
49 Ok(())
50 }
51}
52
53impl<T> Ord for WithJson<T>
54where
55 T: Ord,
56{
57 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
58 match self.value.cmp(&other.value) {
59 std::cmp::Ordering::Equal => {}
60 ord => return ord,
61 }
62 Ord::cmp(
63 &self.json.as_ref().unwrap_or_default().to_string(),
64 &other.json.as_ref().unwrap_or_default().to_string(),
65 )
66 }
67}
68
69impl<T> PartialOrd for WithJson<T>
70where
71 T: PartialOrd,
72{
73 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
74 match self.value.partial_cmp(&other.value) {
75 Some(core::cmp::Ordering::Equal) => {}
76 ord => return ord,
77 }
78 PartialOrd::partial_cmp(
79 &self.json.as_ref().unwrap_or_default().to_string(),
80 &other.json.as_ref().unwrap_or_default().to_string(),
81 )
82 }
83}
84
85impl Exit<WithJson<()>> {
86 fn message(&self) -> &str {
87 match self {
88 Exit::Ok(_) => "",
89 Exit::Error(WithJson {
90 value: msg,
91 json: _,
92 }) => msg,
93 Exit::InvocationError(WithJson {
94 value: msg,
95 json: _,
96 }) => msg,
97 Exit::IO(WithJson {
98 value: msg,
99 json: _,
100 }) => msg,
101 }
102 }
103
104 fn replace_message(self, msg: String, jsons: Vec<Value>) -> Self {
105 let json = (!jsons.is_empty()).then(|| jsons.into_iter().collect::<Value>());
106 match self {
107 Exit::Ok(_) => Self::Ok(WithJson { value: (), json }),
108 Exit::Error(_) => Exit::Error(WithJson { value: msg, json }),
109 Exit::InvocationError(_) => Exit::InvocationError(WithJson { value: msg, json }),
110 Exit::IO(_) => Exit::IO(WithJson { value: msg, json }),
111 }
112 }
113
114 pub fn take_json(&mut self) -> Option<Value> {
115 match self {
116 Exit::Ok(WithJson { json, .. }) => json.take(),
117 Exit::Error(WithJson { json, .. }) => json.take(),
118 Exit::InvocationError(WithJson { json, .. }) => json.take(),
119 Exit::IO(WithJson { json, .. }) => json.take(),
120 }
121 }
122}
123
124impl FromIterator<Exit<WithJson<()>>> for Exit<WithJson<()>> {
125 fn from_iter<I: IntoIterator<Item = Exit<WithJson<()>>>>(iter: I) -> Self {
126 let mut msg = String::new();
127 let mut jsons = Vec::<Value>::new();
128 iter.into_iter()
129 .map(|mut exit| {
130 msg.push_str(exit.message());
131 match exit.take_json() {
132 Some(Array(json)) => jsons.extend(json),
133 Some(json) => jsons.push(json),
134 None => {}
135 }
136 exit
137 })
138 .max()
139 .map(|highest_exit_code| highest_exit_code.replace_message(msg, jsons))
140 .unwrap_or(Exit::Ok(Default::default()))
141 }
142}
143
144impl<T: _T> From<clap::Error> for Exit<T> {
145 fn from(e: clap::Error) -> Self {
146 Self::InvocationError(WithJson {
147 value: e.to_string(),
148 json: None,
149 })
150 }
151}
152
153impl<T: _T> From<io::Error> for Exit<T> {
154 fn from(e: io::Error) -> Self {
155 Self::IO(WithJson {
156 value: e.to_string(),
157 json: None,
158 })
159 }
160}
161
162impl<T: _T> From<cargo_metadata::Error> for Exit<T> {
164 fn from(error: cargo_metadata::Error) -> Self {
165 match error {
166 cargo_metadata::Error::CargoMetadata { stderr } => Self::IO(WithJson {
167 value: stderr,
168 json: None,
169 }),
170 cargo_metadata::Error::Io(error) => error.into(),
171 cargo_metadata::Error::Utf8(utf8_error) => Self::IO(WithJson {
172 value: utf8_error.to_string(),
173 json: None,
174 }),
175 cargo_metadata::Error::ErrUtf8(_) => Self::IO(WithJson {
176 value: "Big problem with parsing Config.toml".to_string(),
177 json: None,
178 }),
179 cargo_metadata::Error::Json(error) => Self::IO(WithJson {
180 value: error.to_string(),
181 json: None,
182 }),
183 cargo_metadata::Error::NoJson => Self::IO(WithJson {
184 value: "Small problem with parsing Config.toml".to_string(),
185 json: None,
186 }),
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use std::{assert_matches, io, process::Command};
194
195 use crate::commands::{Cmd, CmdExt as _};
196
197 use super::*;
198
199 #[test]
200 fn exit_from_404() {
201 let splat: Cmd = Command::new("splat").output().into_cmd("splat", None);
202 assert_eq!(splat.name, "splat");
203 assert!(
204 matches!(splat.result, Result::Err(ref e) if matches!(e.kind(), io::ErrorKind::NotFound))
205 );
206 let exit: Exit<WithJson<()>> = Exit::from(splat);
207 let Exit::IO(WithJson {
208 value: msg,
209 json: _,
210 }) = exit
211 else {
212 panic!("not an IO2")
213 };
214 eprintln!("{}", msg);
215 assert!(msg.starts_with("splat failed: "));
216 }
217
218 #[test]
219 fn collect_exit() {
220 let exits = [
221 Exit::Ok(WithJson {
222 value: (),
223 json: None,
224 }),
225 Exit::IO(WithJson {
226 value: "one\n".to_string(),
227 json: None,
228 }),
229 Exit::Error(WithJson {
230 value: "two\n".to_string(),
231 json: None,
232 }),
233 Exit::Error(WithJson {
234 value: "three\n".to_string(),
235 json: None,
236 }),
237 ];
238 let exit: Exit<WithJson<()>> = exits.into_iter().collect();
239 let expected = "one\ntwo\nthree\n";
240 assert_matches!(exit, Exit::IO(s) if s.value == expected);
241 }
242}