use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DepKind {
Binary {
repo: String,
asset_pattern: String,
version: String,
},
DockerImage {
image: String,
tag: String,
ports: Vec<String>,
},
NpmPackage {
package: String,
version: String,
entry_point: String,
},
PipPackage {
package: String,
version: String,
entry_point: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum HealthCheck {
WebSocket { url: String },
Http { url: String },
TcpPort { port: u16 },
Command { command: String },
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
pub kind: DepKind,
pub health_check: HealthCheck,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub args: Vec<String>,
}
pub trait HasDependencies {
fn dependencies(&self) -> Vec<Dependency> {
vec![]
}
}
pub fn current_platform() -> (&'static str, &'static str) {
let os = if cfg!(target_os = "macos") {
"darwin"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else {
"unknown"
};
let arch = if cfg!(target_arch = "x86_64") {
"amd64"
} else if cfg!(target_arch = "aarch64") {
"arm64"
} else {
"unknown"
};
(os, arch)
}
pub fn resolve_asset_pattern(pattern: &str) -> String {
let (os, arch) = current_platform();
pattern.replace("{os}", os).replace("{arch}", arch)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dep_kind_binary() {
let kind = DepKind::Binary {
repo: "qhkm/whatsmeow-rs".to_string(),
asset_pattern: "whatsmeow-bridge-{os}-{arch}".to_string(),
version: "v0.1.0".to_string(),
};
match kind {
DepKind::Binary { repo, .. } => assert_eq!(repo, "qhkm/whatsmeow-rs"),
_ => panic!("expected Binary"),
}
}
#[test]
fn test_dep_kind_docker() {
let kind = DepKind::DockerImage {
image: "redis".to_string(),
tag: "7-alpine".to_string(),
ports: vec!["6379:6379".to_string()],
};
match kind {
DepKind::DockerImage { image, tag, ports } => {
assert_eq!(image, "redis");
assert_eq!(tag, "7-alpine");
assert_eq!(ports.len(), 1);
}
_ => panic!("expected DockerImage"),
}
}
#[test]
fn test_dep_kind_npm() {
let kind = DepKind::NpmPackage {
package: "@mcp/server".to_string(),
version: "^1.0.0".to_string(),
entry_point: "mcp-server".to_string(),
};
match kind {
DepKind::NpmPackage { package, .. } => assert_eq!(package, "@mcp/server"),
_ => panic!("expected NpmPackage"),
}
}
#[test]
fn test_dep_kind_pip() {
let kind = DepKind::PipPackage {
package: "mcp-server-sqlite".to_string(),
version: ">=1.0".to_string(),
entry_point: "mcp-server-sqlite".to_string(),
};
match kind {
DepKind::PipPackage { package, .. } => assert_eq!(package, "mcp-server-sqlite"),
_ => panic!("expected PipPackage"),
}
}
#[test]
fn test_health_check_websocket() {
let hc = HealthCheck::WebSocket {
url: "ws://localhost:3001".to_string(),
};
assert_eq!(
hc,
HealthCheck::WebSocket {
url: "ws://localhost:3001".to_string()
}
);
}
#[test]
fn test_health_check_http() {
let hc = HealthCheck::Http {
url: "http://localhost:8080/health".to_string(),
};
match hc {
HealthCheck::Http { url } => assert!(url.contains("/health")),
_ => panic!("expected Http"),
}
}
#[test]
fn test_health_check_tcp() {
let hc = HealthCheck::TcpPort { port: 6379 };
assert_eq!(hc, HealthCheck::TcpPort { port: 6379 });
}
#[test]
fn test_health_check_command() {
let hc = HealthCheck::Command {
command: "redis-cli ping".to_string(),
};
match hc {
HealthCheck::Command { command } => assert!(command.contains("ping")),
_ => panic!("expected Command"),
}
}
#[test]
fn test_health_check_none() {
let hc = HealthCheck::None;
assert_eq!(hc, HealthCheck::None);
}
#[test]
fn test_dependency_full() {
let dep = Dependency {
name: "whatsmeow-bridge".to_string(),
kind: DepKind::Binary {
repo: "qhkm/whatsmeow-rs".to_string(),
asset_pattern: "whatsmeow-bridge-{os}-{arch}".to_string(),
version: "v0.1.0".to_string(),
},
health_check: HealthCheck::WebSocket {
url: "ws://localhost:3001".to_string(),
},
env: HashMap::from([("PORT".to_string(), "3001".to_string())]),
args: vec!["--port".to_string(), "3001".to_string()],
};
assert_eq!(dep.name, "whatsmeow-bridge");
assert_eq!(dep.env.get("PORT"), Some(&"3001".to_string()));
assert_eq!(dep.args.len(), 2);
}
#[test]
fn test_dependency_empty_env_and_args() {
let dep = Dependency {
name: "test".to_string(),
kind: DepKind::DockerImage {
image: "redis".to_string(),
tag: "latest".to_string(),
ports: vec![],
},
health_check: HealthCheck::None,
env: HashMap::new(),
args: vec![],
};
assert!(dep.env.is_empty());
assert!(dep.args.is_empty());
}
struct NoDeps;
impl HasDependencies for NoDeps {}
#[test]
fn test_has_dependencies_default_is_empty() {
let c = NoDeps;
assert!(c.dependencies().is_empty());
}
#[test]
fn test_dep_kind_serde_roundtrip() {
let kind = DepKind::Binary {
repo: "owner/repo".to_string(),
asset_pattern: "bin-{os}-{arch}".to_string(),
version: "v1.0.0".to_string(),
};
let json = serde_json::to_string(&kind).unwrap();
let deserialized: DepKind = serde_json::from_str(&json).unwrap();
assert_eq!(kind, deserialized);
}
#[test]
fn test_health_check_serde_roundtrip() {
let hc = HealthCheck::TcpPort { port: 5432 };
let json = serde_json::to_string(&hc).unwrap();
let deserialized: HealthCheck = serde_json::from_str(&json).unwrap();
assert_eq!(hc, deserialized);
}
#[test]
fn test_dependency_serde_roundtrip() {
let dep = Dependency {
name: "test-dep".to_string(),
kind: DepKind::NpmPackage {
package: "pkg".to_string(),
version: "1.0".to_string(),
entry_point: "cmd".to_string(),
},
health_check: HealthCheck::Http {
url: "http://localhost:3000".to_string(),
},
env: HashMap::new(),
args: vec![],
};
let json = serde_json::to_string(&dep).unwrap();
let deserialized: Dependency = serde_json::from_str(&json).unwrap();
assert_eq!(dep.name, deserialized.name);
}
#[test]
fn test_current_platform_returns_known_values() {
let (os, arch) = current_platform();
assert!(["darwin", "linux", "windows", "unknown"].contains(&os));
assert!(["amd64", "arm64", "unknown"].contains(&arch));
}
#[test]
fn test_resolve_asset_pattern() {
let (os, arch) = current_platform();
let result = resolve_asset_pattern("binary-{os}-{arch}.tar.gz");
assert_eq!(result, format!("binary-{}-{}.tar.gz", os, arch));
}
#[test]
fn test_resolve_asset_pattern_no_placeholders() {
let result = resolve_asset_pattern("static-binary");
assert_eq!(result, "static-binary");
}
}