Skip to main content

ninja_xtask/
lib.rs

1#![feature(never_type)]
2#![feature(try_trait_v2)]
3#![feature(try_trait_v2_residual)]
4
5use std::{
6    fmt::Debug,
7    io,
8    process::{Child, Output, Termination as _T},
9};
10
11use exit_safely::Termination;
12use try_v2::{Try, Try_ConvertResult};
13
14pub mod commands;
15
16#[derive(Debug, Termination, Try, Try_ConvertResult, PartialEq, PartialOrd, Eq, Ord)]
17#[repr(u8)]
18#[must_use]
19pub enum Exit<T: _T> {
20    Ok(T) = 0,
21    Error(String) = 1,
22    InvocationError(String) = 2,
23    IO(String) = 3,
24}
25
26impl Exit<()> {
27    fn message(&self) -> &str {
28        match self {
29            Exit::Ok(_) => "",
30            Exit::Error(m) => m,
31            Exit::InvocationError(m) => m,
32            Exit::IO(m) => m,
33        }
34    }
35
36    fn replace_message(self, msg: String) -> Option<Self> {
37        match self {
38            Exit::Ok(_) => None,
39            Exit::Error(_) => Some(Exit::Error(msg)),
40            Exit::InvocationError(_) => Some(Exit::InvocationError(msg)),
41            Exit::IO(_) => Some(Exit::IO(msg)),
42        }
43    }
44}
45
46impl FromIterator<Exit<()>> for Exit<()> {
47    fn from_iter<I: IntoIterator<Item = Exit<()>>>(iter: I) -> Self {
48        let mut msg = String::new();
49        iter.into_iter()
50            .filter_map(|e| {
51                if let Exit::Ok(_) = e {
52                    None
53                } else {
54                    msg.push_str(e.message());
55                    msg.push('\n');
56                    Some(e)
57                }
58            })
59            .min()
60            .and_then(|e| e.replace_message(msg))
61            .unwrap_or(Exit::Ok(()))
62    }
63}
64
65impl<T: _T> From<clap::Error> for Exit<T> {
66    fn from(e: clap::Error) -> Self {
67        Self::InvocationError(e.to_string())
68    }
69}
70
71#[derive(Debug)]
72pub struct Cmd {
73    pub name: &'static str,
74    pub result: Result<Output, io::Error>,
75}
76
77trait CmdExt {
78    fn into_cmd(self, name: &'static str) -> Cmd;
79}
80
81impl CmdExt for Result<Output, io::Error> {
82    fn into_cmd(self, name: &'static str) -> Cmd {
83        Cmd { name, result: self }
84    }
85}
86
87// TODO: #10 Provide stdout & error code on failure
88impl From<Cmd> for Exit<()> {
89    fn from(cmd: Cmd) -> Self {
90        match cmd.result {
91            Ok(output) => {
92                if output.status.success() {
93                    println!("{}: OK", cmd.name);
94                    Self::Ok(())
95                } else {
96                    let stdout = String::from_utf8_lossy(&output.stdout);
97                    let stderr = String::from_utf8_lossy(&output.stderr);
98                    Self::Error(format!(
99                        "====== {} exited with {} ======\n-- stdout: --\n{}\n\n-- stderr: --\n{}",
100                        cmd.name, output.status, stdout, stderr
101                    ))
102                }
103            }
104            Err(e) => {
105                let msg = format!("{} failed: {}", cmd.name, e);
106                Self::IO(msg)
107            }
108        }
109    }
110}
111
112#[derive(Debug)]
113pub struct Spawned {
114    pub name: &'static str,
115    pub child: Result<Child, io::Error>,
116}
117
118impl Spawned {
119    pub fn wait(self) -> Cmd {
120        match self.child {
121            Ok(child) => child.wait_with_output().into_cmd(self.name),
122            Err(e) => Cmd {
123                name: self.name,
124                result: Err(e),
125            },
126        }
127    }
128}
129
130trait SpawnedExt {
131    fn into_spawned(self, name: &'static str) -> Spawned;
132}
133
134impl SpawnedExt for Result<Child, io::Error> {
135    fn into_spawned(self, name: &'static str) -> Spawned {
136        Spawned { name, child: self }
137    }
138}
139
140impl From<Vec<Spawned>> for Exit<()> {
141    fn from(spawns: Vec<Spawned>) -> Self {
142        spawns
143            .into_iter()
144            .map(|spawn| spawn.wait())
145            .map(Exit::from)
146            .collect()
147    }
148}
149
150impl From<Spawned> for Exit<()> {
151    fn from(spawn: Spawned) -> Self {
152        spawn.wait().into()
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use std::process::Command;
159
160    use super::*;
161
162    #[test]
163    fn exit_from_404() {
164        let splat: Cmd = Command::new("splat").output().into_cmd("splat");
165        assert_eq!(splat.name, "splat");
166        assert!(
167            matches!(splat.result, Result::Err(ref e) if matches!(e.kind(), io::ErrorKind::NotFound))
168        );
169        let exit: Exit<()> = Exit::from(splat);
170        let Exit::IO(ref msg) = exit else {
171            panic!("not an IO2")
172        };
173        eprintln!("{}", msg);
174        assert!(msg.starts_with("splat failed: "));
175    }
176
177    #[test]
178    fn collect_exit() {
179        let exits = [
180            Exit::Ok(()),
181            Exit::IO("one".to_string()),
182            Exit::Error("two".to_string()),
183            Exit::Error("three".to_string()),
184        ];
185        let exit: Exit<()> = exits.into_iter().collect();
186        let expected = "one\ntwo\nthree\n";
187        dbg!(&exit);
188        assert!(matches!(exit, Exit::Error(s) if s == expected));
189    }
190}