#![forbid(unsafe_code)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TargetSource {
#[default]
Argv,
ActiveMarker,
Selector,
}
impl TargetSource {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Argv => "argv",
Self::ActiveMarker => "active_marker",
Self::Selector => "selector",
}
}
#[must_use]
pub fn is_ambient(self) -> bool {
matches!(self, Self::ActiveMarker)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecTarget {
pub host: String,
pub source: TargetSource,
}
impl ExecTarget {
#[must_use]
pub fn new(host: impl Into<String>, source: TargetSource) -> Self {
Self {
host: host.into(),
source,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct TargetEcho {
#[serde(default)]
pub target_resolved: String,
#[serde(default)]
pub target_source: TargetSource,
#[serde(default)]
pub host_resolved: String,
#[serde(default)]
pub host_source: TargetSource,
}
impl TargetEcho {
#[must_use]
pub fn new(target: &ExecTarget) -> Self {
Self {
target_resolved: target.host.clone(),
target_source: target.source,
host_resolved: target.host.clone(),
host_source: target.source,
}
}
#[must_use]
pub fn from_parts(host: impl Into<String>, source: TargetSource) -> Self {
Self::new(&ExecTarget::new(host, source))
}
}
static RESOLVED_TARGET: std::sync::Mutex<Option<ExecTarget>> = std::sync::Mutex::new(None);
fn lock_target() -> std::sync::MutexGuard<'static, Option<ExecTarget>> {
RESOLVED_TARGET.lock().unwrap_or_else(|poisoned| {
tracing::warn!("resolved-target mutex was poisoned; recovering (one-shot CLI)");
poisoned.into_inner()
})
}
pub fn set_resolved_target(target: &ExecTarget) {
*lock_target() = Some(target.clone());
}
#[must_use]
pub fn resolved_target() -> Option<ExecTarget> {
lock_target().clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_canonical_and_alias_spellings_travel_together() {
let echo = TargetEcho::from_parts("typed-host", TargetSource::Argv);
let s = serde_json::to_string(&echo).expect("echo serializes");
assert!(s.contains(r#""target_resolved":"typed-host""#), "{s}");
assert!(s.contains(r#""target_source":"argv""#), "{s}");
assert!(s.contains(r#""host_resolved":"typed-host""#), "{s}");
assert!(s.contains(r#""host_source":"argv""#), "{s}");
}
#[test]
fn either_spelling_round_trips() {
let echo = TargetEcho::from_parts("h", TargetSource::Selector);
let s = serde_json::to_string(&echo).expect("echo serializes");
let back: TargetEcho = serde_json::from_str(&s).expect("echo deserializes");
assert_eq!(back, echo);
assert_eq!(back.target_resolved, back.host_resolved);
assert_eq!(back.target_source, back.host_source);
}
#[test]
fn the_alias_cannot_drift_from_the_canonical_name() {
let echo = TargetEcho::new(&ExecTarget::new("h", TargetSource::ActiveMarker));
assert_eq!(echo.target_resolved, echo.host_resolved);
assert_eq!(echo.target_source, echo.host_source);
}
}