1use super::BindingValue;
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, 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 serialize_with = "serialize_internal_urls",
39 deserialize_with = "deserialize_internal_urls"
40 )]
41 pub internal_urls: HashMap<u16, BindingValue<String>>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct KubernetesContainerBinding {
48 pub name: BindingValue<String>,
50 pub namespace: BindingValue<String>,
52 pub service_name: BindingValue<String>,
54 pub service_port: BindingValue<u16>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub public_url: Option<BindingValue<String>>,
59 #[serde(
61 default,
62 skip_serializing_if = "HashMap::is_empty",
63 serialize_with = "serialize_internal_urls",
64 deserialize_with = "deserialize_internal_urls"
65 )]
66 pub internal_urls: HashMap<u16, BindingValue<String>>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct LocalContainerBinding {
73 pub container_name: BindingValue<String>,
75 pub internal_url: BindingValue<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub public_url: Option<BindingValue<String>>,
80 #[serde(
82 default,
83 skip_serializing_if = "HashMap::is_empty",
84 serialize_with = "serialize_internal_urls",
85 deserialize_with = "deserialize_internal_urls"
86 )]
87 pub internal_urls: HashMap<u16, BindingValue<String>>,
88}
89
90fn serialize_internal_urls<S>(
93 urls: &HashMap<u16, BindingValue<String>>,
94 serializer: S,
95) -> Result<S::Ok, S::Error>
96where
97 S: serde::Serializer,
98{
99 urls.iter()
100 .collect::<BTreeMap<_, _>>()
101 .serialize(serializer)
102}
103
104fn deserialize_internal_urls<'de, D>(
106 deserializer: D,
107) -> Result<HashMap<u16, BindingValue<String>>, D::Error>
108where
109 D: serde::Deserializer<'de>,
110{
111 HashMap::<String, BindingValue<String>>::deserialize(deserializer)?
112 .into_iter()
113 .map(|(port, url)| {
114 port.parse::<u16>()
115 .map(|port| (port, url))
116 .map_err(serde::de::Error::custom)
117 })
118 .collect()
119}
120
121impl ContainerBinding {
122 pub fn horizon(
124 container_name: impl Into<BindingValue<String>>,
125 internal_url: impl Into<BindingValue<String>>,
126 ) -> Self {
127 Self::Horizon(HorizonContainerBinding {
128 container_name: container_name.into(),
129 internal_url: internal_url.into(),
130 public_url: None,
131 internal_urls: HashMap::new(),
132 })
133 }
134
135 pub fn horizon_with_public_url(
137 container_name: impl Into<BindingValue<String>>,
138 internal_url: impl Into<BindingValue<String>>,
139 public_url: impl Into<BindingValue<String>>,
140 ) -> Self {
141 Self::Horizon(HorizonContainerBinding {
142 container_name: container_name.into(),
143 internal_url: internal_url.into(),
144 public_url: Some(public_url.into()),
145 internal_urls: HashMap::new(),
146 })
147 }
148
149 pub fn local(
151 container_name: impl Into<BindingValue<String>>,
152 internal_url: impl Into<BindingValue<String>>,
153 ) -> Self {
154 Self::Local(LocalContainerBinding {
155 container_name: container_name.into(),
156 internal_url: internal_url.into(),
157 public_url: None,
158 internal_urls: HashMap::new(),
159 })
160 }
161
162 pub fn local_with_public_url(
164 container_name: impl Into<BindingValue<String>>,
165 internal_url: impl Into<BindingValue<String>>,
166 public_url: impl Into<BindingValue<String>>,
167 ) -> Self {
168 Self::Local(LocalContainerBinding {
169 container_name: container_name.into(),
170 internal_url: internal_url.into(),
171 public_url: Some(public_url.into()),
172 internal_urls: HashMap::new(),
173 })
174 }
175
176 pub fn kubernetes(
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 ) -> Self {
183 Self::Kubernetes(KubernetesContainerBinding {
184 name: name.into(),
185 namespace: namespace.into(),
186 service_name: service_name.into(),
187 service_port: service_port.into(),
188 public_url: None,
189 internal_urls: HashMap::new(),
190 })
191 }
192
193 pub fn kubernetes_with_public_url(
195 name: impl Into<BindingValue<String>>,
196 namespace: impl Into<BindingValue<String>>,
197 service_name: impl Into<BindingValue<String>>,
198 service_port: impl Into<BindingValue<u16>>,
199 public_url: impl Into<BindingValue<String>>,
200 ) -> Self {
201 Self::Kubernetes(KubernetesContainerBinding {
202 name: name.into(),
203 namespace: namespace.into(),
204 service_name: service_name.into(),
205 service_port: service_port.into(),
206 public_url: Some(public_url.into()),
207 internal_urls: HashMap::new(),
208 })
209 }
210
211 pub fn internal_urls(&self) -> &HashMap<u16, BindingValue<String>> {
213 match self {
214 Self::Horizon(binding) => &binding.internal_urls,
215 Self::Kubernetes(binding) => &binding.internal_urls,
216 Self::Local(binding) => &binding.internal_urls,
217 }
218 }
219
220 pub fn with_internal_urls(mut self, endpoints: HashMap<u16, BindingValue<String>>) -> Self {
222 match &mut self {
223 Self::Horizon(binding) => binding.internal_urls = endpoints,
224 Self::Kubernetes(binding) => binding.internal_urls = endpoints,
225 Self::Local(binding) => binding.internal_urls = endpoints,
226 }
227 self
228 }
229
230 pub fn get_internal_url(&self) -> Option<String> {
233 match self {
234 ContainerBinding::Horizon(binding) => {
235 if let BindingValue::Value(url) = &binding.internal_url {
237 Some(url.clone())
238 } else {
239 None
240 }
241 }
242 ContainerBinding::Kubernetes(binding) => {
243 if let (
245 BindingValue::Value(service_name),
246 BindingValue::Value(namespace),
247 BindingValue::Value(port),
248 ) = (
249 &binding.service_name,
250 &binding.namespace,
251 &binding.service_port,
252 ) {
253 Some(format!(
254 "http://{}.{}.svc.cluster.local:{}",
255 service_name, namespace, port
256 ))
257 } else {
258 None
259 }
260 }
261 ContainerBinding::Local(binding) => {
262 if let BindingValue::Value(url) = &binding.internal_url {
263 Some(url.clone())
264 } else {
265 None
266 }
267 }
268 }
269 }
270
271 pub fn get_public_url(&self) -> Option<&BindingValue<String>> {
273 match self {
274 ContainerBinding::Horizon(binding) => binding.public_url.as_ref(),
275 ContainerBinding::Kubernetes(binding) => binding.public_url.as_ref(),
276 ContainerBinding::Local(binding) => binding.public_url.as_ref(),
277 }
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_horizon_binding() {
287 let binding = ContainerBinding::horizon("api", "http://api.svc:8080");
288
289 if let ContainerBinding::Horizon(horizon_binding) = binding {
290 assert_eq!(
291 horizon_binding.container_name,
292 BindingValue::Value("api".to_string())
293 );
294 assert_eq!(
295 horizon_binding.internal_url,
296 BindingValue::Value("http://api.svc:8080".to_string())
297 );
298 assert!(horizon_binding.public_url.is_none());
299 } else {
300 panic!("Expected Horizon binding");
301 }
302 }
303
304 #[test]
305 fn test_horizon_binding_with_public_url() {
306 let binding = ContainerBinding::horizon_with_public_url(
307 "api",
308 "http://api.svc:8080",
309 "https://api.example.com",
310 );
311
312 if let ContainerBinding::Horizon(horizon_binding) = binding {
313 assert_eq!(
314 horizon_binding.public_url,
315 Some(BindingValue::Value("https://api.example.com".to_string()))
316 );
317 } else {
318 panic!("Expected Horizon binding");
319 }
320 }
321
322 #[test]
323 fn test_local_binding() {
324 let binding = ContainerBinding::local("my-container", "http://my-container.svc:8080");
325
326 if let ContainerBinding::Local(local_binding) = binding {
327 assert_eq!(
328 local_binding.container_name,
329 BindingValue::Value("my-container".to_string())
330 );
331 assert_eq!(
332 local_binding.internal_url,
333 BindingValue::Value("http://my-container.svc:8080".to_string())
334 );
335 assert!(local_binding.public_url.is_none());
336 } else {
337 panic!("Expected Local binding");
338 }
339 }
340
341 #[test]
342 fn test_local_binding_with_public_url() {
343 let binding = ContainerBinding::local_with_public_url(
344 "my-container",
345 "http://my-container.svc:8080",
346 "http://localhost:62844",
347 );
348
349 if let ContainerBinding::Local(local_binding) = binding {
350 assert_eq!(
351 local_binding.public_url,
352 Some(BindingValue::Value("http://localhost:62844".to_string()))
353 );
354 } else {
355 panic!("Expected Local binding");
356 }
357 }
358
359 #[test]
360 fn test_get_internal_url() {
361 let horizon = ContainerBinding::horizon("api", "http://api.svc:8080");
362 assert_eq!(
363 horizon.get_internal_url(),
364 Some("http://api.svc:8080".to_string())
365 );
366
367 let local = ContainerBinding::local("api", "http://api.svc:3000");
368 assert_eq!(
369 local.get_internal_url(),
370 Some("http://api.svc:3000".to_string())
371 );
372 }
373
374 #[test]
375 fn test_serialization_roundtrip() {
376 let binding = ContainerBinding::horizon_with_public_url(
377 "api",
378 "http://api.svc:8080",
379 "https://api.example.com",
380 );
381
382 let serialized = serde_json::to_string(&binding).unwrap();
383 let deserialized: ContainerBinding = serde_json::from_str(&serialized).unwrap();
384 assert_eq!(binding, deserialized);
385 }
386}