cli_shared/remote/
target.rs1use std::{
5 net::{SocketAddr, ToSocketAddrs},
6 path::PathBuf,
7};
8
9#[derive(Debug, Clone)]
11pub enum RemoteTarget {
12 Network {
14 addr: SocketAddr,
15 repo_path: Option<String>,
16 },
17 Local(PathBuf),
19}
20
21impl RemoteTarget {
22 pub fn parse(s: &str) -> Result<Self, String> {
29 if let Some(path) = s.strip_prefix("file://") {
31 return Ok(RemoteTarget::Local(PathBuf::from(path)));
32 }
33
34 if let Some((addr, repo_path)) = parse_network_with_repo_path(s) {
35 return Ok(RemoteTarget::Network { addr, repo_path });
36 }
37
38 let path = PathBuf::from(s);
40 if path.exists() && path.is_dir() {
41 return Ok(RemoteTarget::Local(path));
42 }
43
44 Err(format!(
45 "invalid remote url (expected file://path or host:port): {}",
46 s
47 ))
48 }
49
50 pub fn is_local(&self) -> bool {
52 matches!(self, RemoteTarget::Local(_))
53 }
54
55 pub fn is_network(&self) -> bool {
57 matches!(self, RemoteTarget::Network { .. })
58 }
59}
60
61impl std::fmt::Display for RemoteTarget {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 RemoteTarget::Network { addr, repo_path } => {
65 if let Some(repo_path) = repo_path {
66 write!(f, "heddle://{}/{}", addr, repo_path)
67 } else {
68 write!(f, "{}", addr)
69 }
70 }
71 RemoteTarget::Local(path) => write!(f, "file://{}", path.display()),
72 }
73 }
74}
75
76fn parse_network_with_repo_path(s: &str) -> Option<(SocketAddr, Option<String>)> {
77 if let Some(rest) = s.strip_prefix("heddle://") {
78 return parse_network_with_repo_path(rest);
79 }
80
81 if let Ok(addr) = s.parse::<SocketAddr>() {
82 return Some((addr, None));
83 }
84
85 if let Some(addr) = resolve_socket_addr(s) {
86 return Some((addr, None));
87 }
88
89 let slash = s.find('/')?;
90 let (addr_part, repo_part) = s.split_at(slash);
91 let addr = resolve_socket_addr(addr_part)?;
92 let repo_path = repo_part.trim_start_matches('/');
93 if repo_path.is_empty() {
94 return Some((addr, None));
95 }
96 Some((addr, Some(repo_path.to_string())))
97}
98
99fn resolve_socket_addr(addr: &str) -> Option<SocketAddr> {
100 if let Ok(parsed) = addr.parse::<SocketAddr>() {
101 return Some(parsed);
102 }
103
104 addr.to_socket_addrs().ok()?.next()
105}
106
107#[cfg(test)]
108mod tests {
109 use super::RemoteTarget;
110
111 #[test]
112 fn parses_hostname_without_repo_path() {
113 let target = RemoteTarget::parse("localhost:8421").expect("parse localhost");
114 match target {
115 RemoteTarget::Network { addr, repo_path } => {
116 assert_eq!(addr.port(), 8421);
117 assert!(addr.ip().is_loopback());
118 assert!(repo_path.is_none());
119 }
120 other => panic!("expected network target, got {other:?}"),
121 }
122 }
123
124 #[test]
125 fn parses_hostname_with_repo_path() {
126 let target =
127 RemoteTarget::parse("localhost:8421/acme/heddle").expect("parse localhost repo path");
128 match target {
129 RemoteTarget::Network { addr, repo_path } => {
130 assert_eq!(addr.port(), 8421);
131 assert!(addr.ip().is_loopback());
132 assert_eq!(repo_path.as_deref(), Some("acme/heddle"));
133 }
134 other => panic!("expected network target, got {other:?}"),
135 }
136 }
137}