seaplane_cli/ops/formation/
endpoint.rs1use std::{result::Result as StdResult, str::FromStr};
2
3use seaplane::api::compute::v1::{
4 EndpointKey as EndpointKeyModel, EndpointValue as EndpointValueModel,
5};
6
7use crate::cli::validator::validate_flight_name;
8
9#[derive(Debug, PartialEq, Eq, Clone)]
10pub struct Endpoint {
11 src: EndpointSrc,
12 dst: EndpointDst,
13}
14
15impl Endpoint {
16 pub fn key(&self) -> EndpointKeyModel {
17 match &self.src {
18 EndpointSrc::Http(p) => EndpointKeyModel::Http { path: p.to_owned() },
19 EndpointSrc::Tcp(p) => EndpointKeyModel::Tcp { port: *p },
20 EndpointSrc::Udp(p) => EndpointKeyModel::Udp { port: *p },
21 }
22 }
23 pub fn value(&self) -> EndpointValueModel {
24 EndpointValueModel { flight_name: self.dst.flight.clone(), port: self.dst.port }
25 }
26}
27
28impl FromStr for Endpoint {
29 type Err = String;
30
31 fn from_str(s: &str) -> StdResult<Self, Self::Err> {
32 let mut parts = s.split('=');
33 Ok(Self {
34 src: parts
35 .next()
36 .ok_or_else(|| String::from("invalid endpoint source"))?
37 .parse()?,
38 dst: parts
39 .next()
40 .ok_or_else(|| String::from("invalid endpoint destination"))?
41 .parse()?,
42 })
43 }
44}
45
46#[derive(Debug, PartialEq, Eq, Clone)]
47pub enum EndpointSrc {
48 Http(String),
49 Tcp(u16),
50 Udp(u16),
51}
52
53impl FromStr for EndpointSrc {
54 type Err = String;
55
56 fn from_str(s: &str) -> StdResult<Self, Self::Err> {
57 let mut parts = s.split(':');
58 let proto = parts.next().ok_or_else(|| String::from("http"))?;
59 let ep = match &*proto.to_ascii_lowercase() {
60 "http" | "https" => EndpointSrc::Http(if let Some(route) = parts.next() {
61 if route.is_empty() {
62 return Err(String::from("missing http route"));
63 } else if !route.starts_with('/') {
64 return Err(String::from("route must start with a leady slash ('/')"));
65 }
66 route.to_string()
67 } else {
68 return Err(String::from("missing http route"));
69 }),
70 "tcp" => EndpointSrc::Tcp(
71 parts
72 .next()
73 .ok_or_else(|| String::from("missing network port number"))?
74 .parse::<u16>()
75 .map_err(|_| String::from("invalid network port number"))?,
76 ),
77 "udp" => EndpointSrc::Udp(
78 parts
79 .next()
80 .ok_or_else(|| String::from("missing network port number"))?
81 .parse::<u16>()
82 .map_err(|_| String::from("invalid network port number"))?,
83 ),
84 proto if proto.starts_with('/') => EndpointSrc::Http(proto.to_string()),
85 _ => {
86 return Err(format!(
87 "invalid protocol '{proto}' (valid options: http, https, tcp, udp)"
88 ))
89 }
90 };
91 Ok(ep)
92 }
93}
94
95#[derive(Debug, PartialEq, Eq, Clone)]
96pub struct EndpointDst {
97 flight: String,
98 port: u16,
99}
100
101impl FromStr for EndpointDst {
102 type Err = String;
103
104 fn from_str(s: &str) -> StdResult<Self, Self::Err> {
105 let mut parts = s.split(':');
106 let flight = parts
107 .next()
108 .ok_or_else(|| ("missing destinaion flight").to_string())?;
109 validate_flight_name(flight)?;
110 let port = parts
111 .next()
112 .ok_or_else(|| ("missing destination port number").to_string())?
113 .parse::<u16>()
114 .map_err(|_| ("invalid port number").to_string())?;
115
116 Ok(Self { flight: flight.to_string(), port })
117 }
118}
119
120#[cfg(test)]
121mod endpoint_test {
122 use super::*;
123
124 #[test]
125 fn endpoint_valid_http() {
126 let ep: Endpoint = "http:/foo/bar=baz:1234".parse().unwrap();
127 assert_eq!(
128 ep,
129 Endpoint {
130 src: EndpointSrc::Http("/foo/bar".into()),
131 dst: EndpointDst { flight: "baz".into(), port: 1234 }
132 }
133 )
134 }
135
136 #[test]
137 fn endpoint_valid_https() {
138 let ep: Endpoint = "https:/foo/bar=baz:1234".parse().unwrap();
139 assert_eq!(
140 ep,
141 Endpoint {
142 src: EndpointSrc::Http("/foo/bar".into()),
143 dst: EndpointDst { flight: "baz".into(), port: 1234 }
144 }
145 )
146 }
147
148 #[test]
149 fn endpoint_missing_dst_or_src() {
150 assert!("baz:1234".parse::<Endpoint>().is_err());
151 }
152
153 #[test]
154 fn endpoint_infer_http() {
155 assert!("/foo/bar=baz:1234".parse::<Endpoint>().is_ok());
156 }
157
158 #[test]
159 fn endpoint_http_missing_leading_slash() {
160 assert!("foo/bar=baz:1234".parse::<Endpoint>().is_err());
161 assert!(":foo/bar=baz:1234".parse::<Endpoint>().is_err());
162 assert!("http:foo/bar=baz:1234".parse::<Endpoint>().is_err());
163 assert!("https:foo/bar/=baz:1234".parse::<Endpoint>().is_err());
164 assert!("http:=baz:1234".parse::<Endpoint>().is_err(),);
165 }
166
167 #[test]
169 fn endpoint_missing_dst() {
170 assert!("tcp:1234=baz".parse::<Endpoint>().is_err());
171 assert!("udp:1234=:1234".parse::<Endpoint>().is_err());
172 assert!("http:/foo/bar=baz:".parse::<Endpoint>().is_err());
173 assert!("http:/foo/bar=".parse::<Endpoint>().is_err());
174 }
175
176 #[test]
177 fn endpoint_valid_tcp() {
178 let ep: Endpoint = "tcp:1234=baz:4321".parse().unwrap();
179 assert_eq!(
180 ep,
181 Endpoint {
182 src: EndpointSrc::Tcp(1234),
183 dst: EndpointDst { flight: "baz".into(), port: 4321 }
184 }
185 )
186 }
187
188 #[test]
189 fn endpoint_invalid_tcp_udp() {
190 assert!("udp:/foo/bar=baz:1234".parse::<Endpoint>().is_err());
191 assert!("udp:1234=baz:9999999".parse::<Endpoint>().is_err());
192 assert!("udp:1234=baz:/foo".parse::<Endpoint>().is_err());
193 }
194}