diff options
Diffstat (limited to 'pkgs/populate/src/spec.rs')
| -rw-r--r-- | pkgs/populate/src/spec.rs | 162 |
1 files changed, 137 insertions, 25 deletions
diff --git a/pkgs/populate/src/spec.rs b/pkgs/populate/src/spec.rs index 8fc74fc..cde5e29 100644 --- a/pkgs/populate/src/spec.rs +++ b/pkgs/populate/src/spec.rs @@ -1,12 +1,11 @@ -//! The spec: populate's arguments as JSON, as docs/design.md describes them. +//! What populate delivers where: the sources, as JSON, and the target, as +//! the command line gives it. See README.md. +use crate::run::Origin; use serde::Deserialize; use serde_json::{Map, Value}; use std::collections::BTreeMap; -/// The version of the spec this populate reads. -pub const VERSION: u64 = 1; - /// The source types populate knows. pub const SOURCE_TYPES: &[&str] = &[ "derivation", @@ -20,39 +19,86 @@ pub const SOURCE_TYPES: &[&str] = &[ "symlink", ]; -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] +/// The sources, by name, as a JSON object. +pub type Sources = BTreeMap<String, Source>; + +#[derive(Debug)] pub struct Spec { - /// Checked before the rest, so it's only part of the shape here. - #[allow(dead_code)] - pub version: u64, - #[serde(default)] pub backup: bool, - #[serde(default)] pub force: bool, - #[serde(default)] pub prune: bool, - pub source: BTreeMap<String, Source>, + pub source: Sources, pub target: Target, } -#[derive(Clone, Debug, Deserialize)] -#[serde(deny_unknown_fields, rename_all = "camelCase")] +#[derive(Clone, Debug)] pub struct Target { - #[serde(default)] pub user: Option<String>, pub host: String, - #[serde(default)] pub port: Option<String>, pub path: String, - #[serde(default)] pub sudo: bool, - #[serde(default)] pub extra_options: Vec<String>, - /// Whether the target is the machine populate runs on, or null to - /// decide that when it runs. - #[serde(default)] - pub local: Option<bool>, +} + +/// The parts of a target that [USER@][HOST][:PORT][PATH] gives, like krops' +/// lib's mkTarget parses it. +#[derive(Debug, Default, PartialEq)] +pub struct TargetParts { + pub user: Option<String>, + pub host: Option<String>, + pub port: Option<String>, + pub path: Option<String>, +} + +impl TargetParts { + /// Parses text like the regular expression + /// (([^@]*)@)?(([^:/]+))?(:([^/]+))?(/.*)? of mkTarget: the user up to + /// the first @, the host up to a : or a /, the port up to a /, and the + /// path from there. + pub fn parse(text: &str) -> Result<TargetParts, String> { + let invalid = || format!("invalid target {text:?}: not [USER@][HOST][:PORT][PATH]"); + let (user, rest) = match text.split_once('@') { + Some((user, rest)) => (Some(user.to_string()), rest), + None => (None, text), + }; + let end = rest.find([':', '/']).unwrap_or(rest.len()); + let (host, rest) = rest.split_at(end); + let (port, path) = match rest.strip_prefix(':') { + Some(rest) => { + let end = rest.find('/').unwrap_or(rest.len()); + let (port, path) = rest.split_at(end); + if port.is_empty() { + return Err(invalid()); + } + (Some(port.to_string()), path) + } + None => (None, rest), + }; + Ok(TargetParts { + user, + host: Some(host.to_string()).filter(|h| !h.is_empty()), + port, + path: Some(path.to_string()).filter(|p| !p.is_empty()), + }) + } + + /// The target, with what's missing as mkTarget defaults it: the user + /// running populate, the machine's name, and /var/src. An empty user + /// means ssh's default. + pub fn complete(self, origin: &Origin, sudo: bool, extra_options: Vec<String>) -> Target { + Target { + user: match self.user { + Some(user) => Some(user).filter(|u| !u.is_empty()), + None => origin.user.clone(), + }, + host: self.host.unwrap_or_else(|| origin.host.clone()), + port: self.port, + path: self.path.unwrap_or_else(|| "/var/src".to_string()), + sudo, + extra_options, + } + } } /// A source: its type, and the settings of that type, under the type's name. @@ -97,8 +143,8 @@ pub struct Follows { } impl Source { - /// The inputs this source draws from other sources, like krops' lib's - /// flakeFollows. + /// The inputs this source draws from other sources, like flakeFollows + /// of populate's Nix library. pub fn follows(&self) -> Vec<Follows> { fn walk(prefix: &[String], inputs: &Map<String, Value>, out: &mut Vec<Follows>) { for (name, input) in inputs { @@ -122,3 +168,69 @@ impl Source { out } } + +#[cfg(test)] +mod tests { + use super::*; + + fn parts(text: &str) -> TargetParts { + TargetParts::parse(text).unwrap() + } + + fn some(s: &str) -> Option<String> { + Some(s.to_string()) + } + + #[test] + fn target_syntax() { + assert_eq!(parts(""), TargetParts::default()); + assert_eq!( + parts("/srv/src"), + TargetParts { + path: some("/srv/src"), + ..Default::default() + } + ); + assert_eq!( + parts("host"), + TargetParts { + host: some("host"), + ..Default::default() + } + ); + assert_eq!( + parts("tv@host:22/x"), + TargetParts { + user: some("tv"), + host: some("host"), + port: some("22"), + path: some("/x") + } + ); + // The user ends at the first @, the port at the first /. + assert_eq!(parts("a@b@c").host, some("b@c")); + assert_eq!(parts("h:p:q/r").port, some("p:q")); + assert_eq!(parts(":22").host, None); + assert!(TargetParts::parse("host:").is_err()); + assert!(TargetParts::parse("host:/x").is_err()); + } + + #[test] + fn target_defaults() { + let origin = Origin { + user: some("tv"), + host: "ni".to_string(), + }; + let target = parts("").complete(&origin, false, vec![]); + assert_eq!( + ( + target.user.as_deref(), + target.host.as_str(), + target.path.as_str() + ), + (Some("tv"), "ni", "/var/src") + ); + // An empty user means ssh's default. + assert_eq!(parts("@h").complete(&origin, true, vec![]).user, None); + } +} |
