Start writing config file parser

This commit is contained in:
Félix Saparelli 2022-01-22 13:58:34 +13:00
parent f41299d7f9
commit 0ed758595f
4 changed files with 325 additions and 4 deletions

42
Cargo.lock generated
View file

@ -965,6 +965,17 @@ dependencies = [
"libc",
]
[[package]]
name = "kdl"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "114071e31456ec827056ca691d141f8e96327d9d9a29140da2e6fba9a5f17b83"
dependencies = [
"nom 7.1.0",
"phf",
"thiserror",
]
[[package]]
name = "kqueue"
version = "1.0.4"
@ -1418,7 +1429,9 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros",
"phf_shared",
"proc-macro-hack",
]
[[package]]
@ -1441,6 +1454,20 @@ dependencies = [
"rand 0.7.3",
]
[[package]]
name = "phf_macros"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro-hack",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -1553,6 +1580,12 @@ dependencies = [
"toml",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.36"
@ -1833,18 +1866,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.133"
version = "1.0.134"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a"
checksum = "96b3c34c1690edf8174f5b289a336ab03f568a4460d8c6df75f2f3a692b3bc6a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.133"
version = "1.0.134"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537"
checksum = "784ed1fbfa13fe191077537b0d70ec8ad1e903cfe04831da608aa36457cb653d"
dependencies = [
"proc-macro2",
"quote",
@ -2583,6 +2616,7 @@ dependencies = [
"embed-resource",
"futures",
"insta",
"kdl",
"miette",
"mimalloc",
"notify-rust",

View file

@ -24,6 +24,7 @@ path = "src/main.rs"
console-subscriber = { version = "0.1.0", optional = true }
dunce = "1.0.2"
futures = "0.3.17"
kdl = "3.0.0"
miette = { version = "3.2.0", features = ["fancy"] }
notify-rust = "4.5.2"
tracing = "0.1.26"

View file

@ -1,5 +1,7 @@
pub mod file;
mod init;
mod runtime;
pub use file::Config as File;
pub use init::init;
pub use runtime::runtime;

284
cli/src/config/file.rs Normal file
View file

@ -0,0 +1,284 @@
use kdl::{parse_document, KdlNode, KdlValue};
use miette::{IntoDiagnostic, Report, Result};
use watchexec::command::Shell;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Config {
pub commands: Vec<Command>,
}
impl Config {
pub fn parse(input: &str) -> Result<Config> {
let kdl = parse_document(input).into_diagnostic()?;
let mut config = Config::default();
for root in kdl {
match root.name.as_str() {
"command" => config.commands.push(Command::parse(root)?),
otherwise => todo!("Root: {:?}", otherwise),
}
}
Ok(config)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Command {
pub name: String,
pub run: Option<Run>,
}
impl Command {
fn parse(node: KdlNode) -> Result<Self> {
let name = node
.values
.first()
.ok_or_else(|| Report::msg("Command has no name"))
.and_then(|name| match name {
KdlValue::String(s) => Ok(s.to_owned()),
otherwise => Err(Report::msg("Command name is not a string")
.wrap_err(format!("{:?}", otherwise))),
})?;
let mut runs = node
.children
.iter()
.filter(|node| node.name == "run")
.map(Run::parse)
.collect::<Result<Vec<_>>>()?;
if runs.len() > 1 {
return Err(Report::msg("Command has multiple runs"));
}
Ok(Command {
name,
run: runs.pop(),
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Run {
pub shell: Shell,
pub args: Vec<String>,
}
impl Run {
fn parse(node: &KdlNode) -> Result<Self> {
let args = node
.values
.iter()
.enumerate()
.map(|(n, v)| match v {
KdlValue::String(s) => Ok(s.to_owned()),
otherwise => Err(Report::msg(format!("Run argument {n} is not a string"))
.wrap_err(format!("{otherwise:?}"))),
})
.collect::<Result<Vec<_>>>()
.and_then(|run| {
if run.is_empty() {
Err(Report::msg("Run has no arguments"))
} else {
Ok(run)
}
})?;
let shell = node
.properties
.get("shell")
.map(|shell| match shell {
KdlValue::String(s) => Ok(s.to_owned()),
otherwise => {
Err(Report::msg("Run shell is not a string").wrap_err(format!("{otherwise:?}")))
}
})
.transpose()?
.map(|shell| match shell.as_str() {
"powershell" | "pwsh" => Shell::Powershell,
"none" => Shell::None,
#[cfg(windows)]
"cmd" => Shell::Cmd,
unix => Shell::Unix(unix.to_owned()),
})
.unwrap_or_default();
if args.len() > 1 && shell != Shell::None {
Err(Report::msg("Run has more than one argument and a shell"))
} else {
Ok(Run { shell, args })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_command() {
let config = Config::parse(r#"command "empty""#).unwrap();
assert_eq!(config.commands.len(), 1);
assert_eq!(config.commands[0].name, "empty");
}
#[test]
fn empty_command_with_braces() {
let config = Config::parse(r#"command "empty" {}"#).unwrap();
assert_eq!(config.commands.len(), 1);
assert_eq!(config.commands[0].name, "empty");
}
#[test]
fn command_with_run_one() {
let config = Config::parse(
r#"command "running" {
run "echo hello-world"
}"#,
)
.unwrap();
assert_eq!(config.commands.len(), 1);
assert_eq!(config.commands[0].name, "running");
assert_eq!(
config.commands[0].run,
Some(Run {
args: vec!["echo hello-world".to_owned()],
shell: Shell::default()
})
);
}
#[test]
fn command_with_run_two() {
let config = Config::parse(
r#"command "running" {
run "echo" "hello-world"
}"#,
)
.unwrap();
assert_eq!(config.commands.len(), 1);
assert_eq!(config.commands[0].name, "running");
assert_eq!(
config.commands[0].run,
Some(Run {
args: vec!["echo".to_owned(), "hello-world".to_owned()],
shell: Shell::default()
})
);
}
#[test]
fn command_with_no_run() {
let config = Config::parse(r#"command "running" {}"#).unwrap();
assert_eq!(config.commands.len(), 1);
assert_eq!(config.commands[0].name, "running");
assert_eq!(config.commands[0].run, None);
}
#[test]
#[should_panic]
fn command_with_empty_run() {
Config::parse(
r#"command "running" {
run
}"#,
)
.unwrap();
}
#[test]
fn run_with_default_shell() {
let config = Config::parse(
r#"command "running" {
run "echo hello-world"
}"#,
)
.unwrap();
assert_eq!(
config.commands[0].run.as_ref().unwrap().shell,
Shell::default()
);
assert_eq!(
config.commands[0].run.as_ref().unwrap().shell,
Shell::None
);
}
#[test]
fn run_with_explicit_shell() {
let config = Config::parse(
r#"command "running" {
run shell="bash" "echo hello-world"
}"#,
)
.unwrap();
assert_eq!(
config.commands[0].run.as_ref().unwrap().shell,
Shell::Unix("bash".to_owned())
);
}
#[test]
fn run_with_powershell() {
let config = Config::parse(
r#"command "running" {
run shell="powershell" "echo hello-world"
}"#,
)
.unwrap();
assert_eq!(
config.commands[0].run.as_ref().unwrap().shell,
Shell::Powershell
);
let config = Config::parse(
r#"command "running" {
run shell="pwsh" "echo hello-world"
}"#,
)
.unwrap();
assert_eq!(
config.commands[0].run.as_ref().unwrap().shell,
Shell::Powershell
);
}
#[cfg(unix)]
#[test]
fn run_with_cmd_unix() {
let config = Config::parse(
r#"command "running" {
run shell="cmd" "echo hello-world"
}"#,
)
.unwrap();
assert_eq!(
config.commands[0].run.as_ref().unwrap().shell,
Shell::Unix("cmd".to_owned())
);
}
#[cfg(windows)]
#[test]
fn run_with_cmd_windows() {
let config = Config::parse(
r#"command "running" {
run shell="cmd" "echo hello-world"
}"#,
)
.unwrap();
assert_eq!(config.commands[0].run.as_ref().unwrap().shell, Shell::Cmd);
}
#[test]
#[should_panic]
fn multi_arg_run_with_shell() {
Config::parse(
r#"command "running" {
run shell="bash" "echo" "hello-world"
}"#,
)
.unwrap();
}
}