diff --git a/Cargo.lock b/Cargo.lock index 5efa7f7..80c1d16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,15 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +[[package]] +name = "cbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b6ad25ae296159fb0da12b970b2fe179b234584d7cd294c891e2bbb284466b" +dependencies = [ + "num-traits", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -93,6 +102,7 @@ dependencies = [ "log", "merge", "pretty_env_logger", + "rnix", "serde", "serde_derive", "serde_json", @@ -562,6 +572,34 @@ version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" +[[package]] +name = "rnix" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbbea4c714e5bbf462fa4316ddf45875d8f0e28e5db81050b5f9ce99746c6863" +dependencies = [ + "cbitset", + "rowan", +] + +[[package]] +name = "rowan" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea7cadf87a9d8432e85cb4eb86bd2e765ace60c24ef86e79084dcae5d1c5a19" +dependencies = [ + "rustc-hash", + "smol_str", + "text_unit", + "thin-dst", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "ryu" version = "1.0.5" @@ -612,6 +650,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +[[package]] +name = "smol_str" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ca0f7ce3a29234210f0f4f0b56f8be2e722488b95cb522077943212da3b32eb" + [[package]] name = "socket2" version = "0.3.15" @@ -650,6 +694,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "text_unit" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20431e104bfecc1a40872578dbc390e10290a0e9c35fffe3ce6f73c15a9dbfc2" + [[package]] name = "textwrap" version = "0.12.1" @@ -659,6 +709,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thin-dst" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c46be180f1af9673ebb27bc1235396f61ef6965b3fe0dbb2e624deb604f0e" + [[package]] name = "thiserror" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 209d812..e9d1fa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ fork = "0.1" thiserror = "1.0" toml = "0.5" yn = "0.1" +rnix = "0.8" [[bin]] diff --git a/src/main.rs b/src/main.rs index 544f563..be7ad40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -338,7 +338,7 @@ async fn run_deploy( extra_build_args: &[String], ) -> Result<(), RunDeployError> { let to_deploy: Vec<((&str, &utils::data::Node), (&str, &utils::data::Profile))> = - match (deploy_flake.node, deploy_flake.profile) { + match (&deploy_flake.node, &deploy_flake.profile) { (Some(node_name), Some(profile_name)) => { let node = match data.nodes.get(node_name) { Some(x) => x, @@ -379,7 +379,7 @@ async fn run_deploy( profiles_list .into_iter() - .map(|x| ((node_name, node), x)) + .map(|x| ((node_name.as_str(), node), x)) .collect() } (None, None) => { @@ -424,7 +424,7 @@ async fn run_deploy( let mut parts: Vec<(utils::DeployData, utils::DeployDefs)> = Vec::new(); - for ((node_name, node), (profile_name, profile)) in &to_deploy { + for ((node_name, node), (profile_name, profile)) in to_deploy { let deploy_data = utils::make_deploy_data( &data.generic_settings, node, @@ -478,6 +478,8 @@ enum RunError { CheckDeploymentError(#[from] CheckDeploymentError), #[error("Failed to evaluate deployment data: {0}")] GetDeploymentDataError(#[from] GetDeploymentDataError), + #[error("Error parsing flake: {0}")] + ParseFlakeError(#[from] utils::ParseFlakeError), #[error("{0}")] RunDeployError(#[from] RunDeployError), } @@ -491,7 +493,7 @@ async fn run() -> Result<(), RunError> { let opts: Opts = Opts::parse(); - let deploy_flake = utils::parse_flake(opts.flake.as_str()); + let deploy_flake = utils::parse_flake(opts.flake.as_str())?; let cmd_overrides = utils::CmdOverrides { ssh_user: opts.ssh_user, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4fae7a6..deea78e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: MPL-2.0 +use rnix::{types::*, NodeOrToken, SyntaxKind::*, SyntaxNode}; + use std::path::PathBuf; use merge::Merge; @@ -36,66 +38,86 @@ pub struct CmdOverrides { #[derive(PartialEq, Debug)] pub struct DeployFlake<'a> { pub repo: &'a str, - pub node: Option<&'a str>, - pub profile: Option<&'a str>, + pub node: Option, + pub profile: Option, } -pub fn parse_flake(flake: &str) -> DeployFlake { +#[derive(Error, Debug)] +pub enum ParseFlakeError { + #[error("The given path was too long, did you mean to put something in quotes?")] + PathTooLong, + #[error("Unrecognized node or token encountered")] + Unrecognized, +} +pub fn parse_flake(flake: &str) -> Result { let flake_fragment_start = flake.find('#'); let (repo, maybe_fragment) = match flake_fragment_start { Some(s) => (&flake[..s], Some(&flake[s + 1..])), None => (flake, None), }; - let (node, profile) = match maybe_fragment { - Some(fragment) => { - let fragment_profile_start = fragment.rfind('.'); + let mut node: Option = None; + let mut profile: Option = None; - match fragment_profile_start { - Some(s) => ( - Some(&fragment[..s]), - // Ignore the trailing `.` - (if (s + 1) == fragment.len() { - None - } else { - Some(&fragment[s + 1..]) - }), - ), - None => (Some(fragment), None), + if let Some(fragment) = maybe_fragment { + let ast = rnix::parse(fragment); + + let first_child = match ast.root().node().first_child() { + Some(x) => x, + None => { + return Ok(DeployFlake { + repo, + node: None, + profile: None, + }) + } + }; + + let mut node_over = false; + + for entry in first_child.children_with_tokens() { + let x: Option = match (entry.kind(), node_over) { + (TOKEN_DOT, false) => { + node_over = true; + None + } + (TOKEN_DOT, true) => { + return Err(ParseFlakeError::PathTooLong); + } + (NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()), + (TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()), + (NODE_STRING, _) => { + let c = entry + .into_node() + .unwrap() + .children_with_tokens() + .nth(1) + .unwrap(); + + Some(c.into_token().unwrap().text().to_string()) + } + _ => return Err(ParseFlakeError::Unrecognized), + }; + + if !node_over { + node = x; + } else { + profile = x; } } - None => (None, None), - }; + } - DeployFlake { + Ok(DeployFlake { repo, node, profile, - } + }) } #[test] fn test_parse_flake() { assert_eq!( - parse_flake("../deploy/examples/system#example"), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("example"), - profile: None - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#example.system"), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("example"), - profile: Some("system") - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system"), + parse_flake("../deploy/examples/system").unwrap(), DeployFlake { repo: "../deploy/examples/system", node: None, @@ -103,33 +125,57 @@ fn test_parse_flake() { } ); - // Trailing `.` should be ignored assert_eq!( - parse_flake("../deploy/examples/system#example."), + parse_flake("../deploy/examples/system#").unwrap(), DeployFlake { repo: "../deploy/examples/system", - node: Some("example"), + node: None, + profile: None, + } + ); + + assert_eq!( + parse_flake("../deploy/examples/system#computer.\"something.nix\"").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: Some("computer".to_string()), + profile: Some("something.nix".to_string()), + } + ); + + assert_eq!( + parse_flake("../deploy/examples/system#\"example.com\".system").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: Some("example.com".to_string()), + profile: Some("system".to_string()), + } + ); + + assert_eq!( + parse_flake("../deploy/examples/system#example").unwrap(), + DeployFlake { + repo: "../deploy/examples/system", + node: Some("example".to_string()), profile: None } ); - // The last `.` should be used for splitting assert_eq!( - parse_flake("../deploy/examples/system#example.com.system"), + parse_flake("../deploy/examples/system#example.system").unwrap(), DeployFlake { repo: "../deploy/examples/system", - node: Some("example.com"), - profile: Some("system") + node: Some("example".to_string()), + profile: Some("system".to_string()) } ); - // The last `.` should be used for splitting, _and_ trailing `.` should be ignored assert_eq!( - parse_flake("../deploy/examples/system#example.com."), + parse_flake("../deploy/examples/system").unwrap(), DeployFlake { repo: "../deploy/examples/system", - node: Some("example.com"), - profile: None + node: None, + profile: None, } ); }