Skip to main content

ninja_xtask/
exit_with_json.rs

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
162//TODO: #95 Review error handling WithJson
163impl<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}