//! Remote-location value objects: the strategy that locates where a project's //! filesystem and processes actually live (local, SSH, or WSL). use serde::{Deserialize, Serialize}; use crate::error::DomainError; /// SSH authentication strategy. /// /// The domain only models *which* strategy is selected; resolving keys, /// agents or known-hosts is an infrastructure concern. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "type")] pub enum SshAuth { /// Use the running SSH agent. Agent, /// Use a private-key file at the given (remote-machine-agnostic) path. Key { /// Path to the private key on the local machine. path: String, }, /// Interactive / stored password authentication. Password, } /// Strategy describing where a project is physically located and executed. /// /// Invariants: /// - `Ssh.port` ∈ `1..=65535`, /// - `Ssh.host` / `Ssh.user` / `Ssh.remote_root` non-empty, /// - `Wsl.distro` non-empty. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "kind")] pub enum RemoteRef { /// The local machine. Local, /// A remote host reached over SSH. #[serde(rename_all = "camelCase")] Ssh { /// Hostname or IP. host: String, /// TCP port (`1..=65535`). port: u16, /// Remote user. user: String, /// Authentication strategy. auth: SshAuth, /// Absolute root path on the remote machine. remote_root: String, }, /// A Windows Subsystem for Linux distribution. Wsl { /// Distribution name (e.g. `Ubuntu-22.04`). distro: String, }, } impl RemoteRef { /// Convenience constructor for the local machine. #[must_use] pub const fn local() -> Self { Self::Local } /// Validated constructor for an SSH remote. /// /// # Errors /// Returns [`DomainError`] if the port is `0`, or if `host`, `user` or /// `remote_root` is empty. pub fn ssh( host: impl Into, port: u16, user: impl Into, auth: SshAuth, remote_root: impl Into, ) -> Result { let host = host.into(); let user = user.into(); let remote_root = remote_root.into(); crate::validation::non_empty(&host, "ssh.host")?; crate::validation::non_empty(&user, "ssh.user")?; crate::validation::non_empty(&remote_root, "ssh.remote_root")?; // u16 already bounds the upper end; only port 0 is invalid. if port == 0 { return Err(DomainError::InvalidPort { port: 0 }); } Ok(Self::Ssh { host, port, user, auth, remote_root, }) } /// Validated constructor for a WSL remote. /// /// # Errors /// Returns [`DomainError`] if `distro` is empty. pub fn wsl(distro: impl Into) -> Result { let distro = distro.into(); crate::validation::non_empty(&distro, "wsl.distro")?; Ok(Self::Wsl { distro }) } /// Returns the coarse kind of this remote, for adapter selection. #[must_use] pub fn kind(&self) -> RemoteKind { match self { Self::Local => RemoteKind::Local, Self::Ssh { .. } => RemoteKind::Ssh, Self::Wsl { .. } => RemoteKind::Wsl, } } } /// Coarse discriminant used by the [`crate::ports::RemoteHost`] port to advertise /// its kind without exposing connection details. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum RemoteKind { /// Local execution. Local, /// SSH remote. Ssh, /// WSL distribution. Wsl, }