aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/populate/src/spec.rs
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/populate/src/spec.rs')
-rw-r--r--pkgs/populate/src/spec.rs162
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);
+ }
+}