1use super::BindingValue;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(tag = "service", rename_all = "lowercase")]
14pub enum ContainerBinding {
15 Horizon(HorizonContainerBinding),
17 Kubernetes(KubernetesContainerBinding),
19 Local(LocalContainerBinding),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct HorizonContainerBinding {
27 pub container_name: BindingValue<String>,
29 pub internal_url: BindingValue<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub public_url: Option<BindingValue<String>>,
34 #[serde(
36 default,
37 skip_serializing_if = "HashMap::is_empty",
38 deserialize_with = "deserialize_internal_urls"
39 )]
40 pub internal_urls: HashMap<u16, BindingValue<String>>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct KubernetesContainerBinding {
47 pub name: BindingValue<String>,
49 pub namespace: BindingValue<String>,
51 pub service_name: BindingValue<String>,
53 pub service_port: BindingValue<u16>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub public_url: Option<BindingValue<String>>,
58 #[serde(
60 default,
61 skip_serializing_if = "HashMap::is_empty",
62 deserialize_with = "deserialize_internal_urls"
63 )]
64 pub internal_urls: HashMap<u16, BindingValue<String>>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct LocalContainerBinding {
71 pub container_name: BindingValue<String>,
73 pub internal_url: BindingValue<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub public_url: Option<BindingValue<String>>,
78 #[serde(
80 default,
81 skip_serializing_if = "HashMap::is_empty",
82 deserialize_with = "deserialize_internal_urls"
83 )]
84 pub internal_urls: HashMap<u16, BindingValue<String>>,
85}
86
87fn deserialize_internal_urls<'de, D>(
89 deserializer: D,
90) -> Result<HashMap<u16, BindingValue<String>>, D::Error>
91where
92 D: serde::Deserializer<'de>,
93{
94 HashMap::<String, BindingValue<String>>::deserialize(deserializer)?
95 .into_iter()
96 .map(|(port, url)| {
97 port.parse::<u16>()
98 .map(|port| (port, url))
99 .map_err(serde::de::Error::custom)
100 })
101 .collect()
102}
103
104impl ContainerBinding {
105 pub fn horizon(
107 container_name: impl Into<BindingValue<String>>,
108 internal_url: impl Into<BindingValue<String>>,
109 ) -> Self {
110 Self::Horizon(HorizonContainerBinding {
111 container_name: container_name.into(),
112 internal_url: internal_url.into(),
113 public_url: None,
114 internal_urls: HashMap::new(),
115 })
116 }
117
118 pub fn horizon_with_public_url(
120 container_name: impl Into<BindingValue<String>>,
121 internal_url: impl Into<BindingValue<String>>,
122 public_url: impl Into<BindingValue<String>>,
123 ) -> Self {
124 Self::Horizon(HorizonContainerBinding {
125 container_name: container_name.into(),
126 internal_url: internal_url.into(),
127 public_url: Some(public_url.into()),
128 internal_urls: HashMap::new(),
129 })
130 }
131
132 pub fn local(
134 container_name: impl Into<BindingValue<String>>,
135 internal_url: impl Into<BindingValue<String>>,
136 ) -> Self {
137 Self::Local(LocalContainerBinding {
138 container_name: container_name.into(),
139 internal_url: internal_url.into(),
140 public_url: None,
141 internal_urls: HashMap::new(),
142 })
143 }
144
145 pub fn local_with_public_url(
147 container_name: impl Into<BindingValue<String>>,
148 internal_url: impl Into<BindingValue<String>>,
149 public_url: impl Into<BindingValue<String>>,
150 ) -> Self {
151 Self::Local(LocalContainerBinding {
152 container_name: container_name.into(),
153 internal_url: internal_url.into(),
154 public_url: Some(public_url.into()),
155 internal_urls: HashMap::new(),
156 })
157 }
158
159 pub fn kubernetes(
161 name: impl Into<BindingValue<String>>,
162 namespace: impl Into<BindingValue<String>>,
163 service_name: impl Into<BindingValue<String>>,
164 service_port: impl Into<BindingValue<u16>>,
165 ) -> Self {
166 Self::Kubernetes(KubernetesContainerBinding {
167 name: name.into(),
168 namespace: namespace.into(),
169 service_name: service_name.into(),
170 service_port: service_port.into(),
171 public_url: None,
172 internal_urls: HashMap::new(),
173 })
174 }
175
176 pub fn kubernetes_with_public_url(
178 name: impl Into<BindingValue<String>>,
179 namespace: impl Into<BindingValue<String>>,
180 service_name: impl Into<BindingValue<String>>,
181 service_port: impl Into<BindingValue<u16>>,
182 public_url: impl Into<BindingValue<String>>,
183 ) -> Self {
184 Self::Kubernetes(KubernetesContainerBinding {
185 name: name.into(),
186 namespace: namespace.into(),
187 service_name: service_name.into(),
188 service_port: service_port.into(),
189 public_url: Some(public_url.into()),
190 internal_urls: HashMap::new(),
191 })
192 }
193
194 pub fn internal_urls(&self) -> &HashMap<u16, BindingValue<String>> {
196 match self {
197 Self::Horizon(binding) => &binding.internal_urls,
198 Self::Kubernetes(binding) => &binding.internal_urls,
199 Self::Local(binding) => &binding.internal_urls,
200 }
201 }
202
203 pub fn with_internal_urls(mut self, endpoints: HashMap<u16, BindingValue<String>>) -> Self {
205 match &mut self {
206 Self::Horizon(binding) => binding.internal_urls = endpoints,
207 Self::Kubernetes(binding) => binding.internal_urls = endpoints,
208 Self::Local(binding) => binding.internal_urls = endpoints,
209 }
210 self
211 }
212
213 pub fn get_internal_url(&self) -> Option<String> {
216 match self {
217 ContainerBinding::Horizon(binding) => {
218 if let BindingValue::Value(url) = &binding.internal_url {
220 Some(url.clone())
221 } else {
222 None
223 }
224 }
225 ContainerBinding::Kubernetes(binding) => {
226 if let (
228 BindingValue::Value(service_name),
229 BindingValue::Value(namespace),
230 BindingValue::Value(port),
231 ) = (
232 &binding.service_name,
233 &binding.namespace,
234 &binding.service_port,
235 ) {
236 Some(format!(
237 "http://{}.{}.svc.cluster.local:{}",
238 service_name, namespace, port
239 ))
240 } else {
241 None
242 }
243 }
244 ContainerBinding::Local(binding) => {
245 if let BindingValue::Value(url) = &binding.internal_url {
246 Some(url.clone())
247 } else {
248 None
249 }
250 }
251 }
252 }
253
254 pub fn get_public_url(&self) -> Option<&BindingValue<String>> {
256 match self {
257 ContainerBinding::Horizon(binding) => binding.public_url.as_ref(),
258 ContainerBinding::Kubernetes(binding) => binding.public_url.as_ref(),
259 ContainerBinding::Local(binding) => binding.public_url.as_ref(),
260 }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_horizon_binding() {
270 let binding = ContainerBinding::horizon("api", "http://api.svc:8080");
271
272 if let ContainerBinding::Horizon(horizon_binding) = binding {
273 assert_eq!(
274 horizon_binding.container_name,
275 BindingValue::Value("api".to_string())
276 );
277 assert_eq!(
278 horizon_binding.internal_url,
279 BindingValue::Value("http://api.svc:8080".to_string())
280 );
281 assert!(horizon_binding.public_url.is_none());
282 } else {
283 panic!("Expected Horizon binding");
284 }
285 }
286
287 #[test]
288 fn test_horizon_binding_with_public_url() {
289 let binding = ContainerBinding::horizon_with_public_url(
290 "api",
291 "http://api.svc:8080",
292 "https://api.example.com",
293 );
294
295 if let ContainerBinding::Horizon(horizon_binding) = binding {
296 assert_eq!(
297 horizon_binding.public_url,
298 Some(BindingValue::Value("https://api.example.com".to_string()))
299 );
300 } else {
301 panic!("Expected Horizon binding");
302 }
303 }
304
305 #[test]
306 fn test_local_binding() {
307 let binding = ContainerBinding::local("my-container", "http://my-container.svc:8080");
308
309 if let ContainerBinding::Local(local_binding) = binding {
310 assert_eq!(
311 local_binding.container_name,
312 BindingValue::Value("my-container".to_string())
313 );
314 assert_eq!(
315 local_binding.internal_url,
316 BindingValue::Value("http://my-container.svc:8080".to_string())
317 );
318 assert!(local_binding.public_url.is_none());
319 } else {
320 panic!("Expected Local binding");
321 }
322 }
323
324 #[test]
325 fn test_local_binding_with_public_url() {
326 let binding = ContainerBinding::local_with_public_url(
327 "my-container",
328 "http://my-container.svc:8080",
329 "http://localhost:62844",
330 );
331
332 if let ContainerBinding::Local(local_binding) = binding {
333 assert_eq!(
334 local_binding.public_url,
335 Some(BindingValue::Value("http://localhost:62844".to_string()))
336 );
337 } else {
338 panic!("Expected Local binding");
339 }
340 }
341
342 #[test]
343 fn test_get_internal_url() {
344 let horizon = ContainerBinding::horizon("api", "http://api.svc:8080");
345 assert_eq!(
346 horizon.get_internal_url(),
347 Some("http://api.svc:8080".to_string())
348 );
349
350 let local = ContainerBinding::local("api", "http://api.svc:3000");
351 assert_eq!(
352 local.get_internal_url(),
353 Some("http://api.svc:3000".to_string())
354 );
355 }
356
357 #[test]
358 fn test_serialization_roundtrip() {
359 let binding = ContainerBinding::horizon_with_public_url(
360 "api",
361 "http://api.svc:8080",
362 "https://api.example.com",
363 );
364
365 let serialized = serde_json::to_string(&binding).unwrap();
366 let deserialized: ContainerBinding = serde_json::from_str(&serialized).unwrap();
367 assert_eq!(binding, deserialized);
368 }
369}