seaplane_cli/ops/formation/
endpoint.rs

1use 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    // TODO: might allow eliding destination port
168    #[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}