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 stderr = String::from_utf8_lossy(&output.stderr);
97                    Self::Error(stderr.to_string())
98                }
99            }
100            Err(e) => {
101                let msg = format!("{} failed: {}", cmd.name, e);
102                Self::IO(msg)
103            }
104        }
105    }
106}
107
108#[derive(Debug)]
109pub struct Spawned {
110    pub name: &'static str,
111    pub child: Result<Child, io::Error>,
112}
113
114impl Spawned {
115    pub fn wait(self) -> Cmd {
116        match self.child {
117            Ok(child) => child.wait_with_output().into_cmd(self.name),
118            Err(e) => Cmd {
119                name: self.name,
120                result: Err(e),
121            },
122        }
123    }
124}
125
126trait SpawnedExt {
127    fn into_spawned(self, name: &'static str) -> Spawned;
128}
129
130impl SpawnedExt for Result<Child, io::Error> {
131    fn into_spawned(self, name: &'static str) -> Spawned {
132        Spawned { name, child: self }
133    }
134}
135
136impl From<Vec<Spawned>> for Exit<()> {
137    fn from(spawns: Vec<Spawned>) -> Self {
138        spawns
139            .into_iter()
140            .map(|spawn| spawn.wait())
141            .map(Exit::from)
142            .collect()
143    }
144}
145
146impl From<Spawned> for Exit<()> {
147    fn from(spawn: Spawned) -> Self {
148        spawn.wait().into()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use std::process::Command;
155
156    use super::*;
157
158    #[test]
159    fn exit_from_404() {
160        let splat: Cmd = Command::new("splat").output().into_cmd("splat");
161        assert_eq!(splat.name, "splat");
162        assert!(
163            matches!(splat.result, Result::Err(ref e) if matches!(e.kind(), io::ErrorKind::NotFound))
164        );
165        let exit: Exit<()> = Exit::from(splat);
166        let Exit::IO(ref msg) = exit else {
167            panic!("not an IO2")
168        };
169        eprintln!("{}", msg);
170        assert!(msg.starts_with("splat failed: "));
171    }
172
173    #[test]
174    fn collect_exit() {
175        let exits = [
176            Exit::Ok(()),
177            Exit::IO("one".to_string()),
178            Exit::Error("two".to_string()),
179            Exit::Error("three".to_string()),
180        ];
181        let exit: Exit<()> = exits.into_iter().collect();
182        let expected = "one\ntwo\nthree\n";
183        dbg!(&exit);
184        assert!(matches!(exit, Exit::Error(s) if s == expected));
185    }
186}