1use std::collections::HashMap;
8
9use alien_error::AlienError;
10use serde::{Deserialize, Serialize};
11
12use crate::bindings::{
13 ArtifactRegistryBinding, BindingValue, ContainerAppsEnvironmentBinding, ExternalAiBinding,
14 KvBinding, PostgresBinding, QueueBinding, RedisBinding, StorageBinding, VaultBinding,
15};
16use crate::error::ErrorData;
17use crate::resource::ResourceOutputs;
18use crate::resources::AzureContainerAppsEnvironmentOutputs;
19use crate::Resource;
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
27#[serde(tag = "type", rename_all = "snake_case")]
28pub enum ExternalBinding {
29 Storage(StorageBinding),
31 Queue(QueueBinding),
33 Kv(KvBinding),
35 ArtifactRegistry(ArtifactRegistryBinding),
37 Vault(VaultBinding),
39 ContainerAppsEnvironment(ContainerAppsEnvironmentBinding),
41 Postgres(PostgresBinding),
43 Redis(RedisBinding),
45 Ai(ExternalAiBinding),
47}
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
53#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
54#[serde(transparent)]
55pub struct ExternalBindings(pub HashMap<String, ExternalBinding>);
56
57impl ExternalBindings {
58 pub fn new() -> Self {
60 Self(HashMap::new())
61 }
62
63 pub fn is_empty(&self) -> bool {
65 self.0.is_empty()
66 }
67
68 pub fn has(&self, resource_id: &str) -> bool {
70 self.0.contains_key(resource_id)
71 }
72
73 pub fn get(&self, resource_id: &str) -> Option<&ExternalBinding> {
75 self.0.get(resource_id)
76 }
77
78 pub fn get_storage(&self, id: &str) -> crate::error::Result<Option<&StorageBinding>> {
81 match self.0.get(id) {
82 Some(ExternalBinding::Storage(b)) => Ok(Some(b)),
83 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
84 resource_id: id.to_string(),
85 expected: "storage".to_string(),
86 actual: other.binding_type().to_string(),
87 })),
88 None => Ok(None),
89 }
90 }
91
92 pub fn get_queue(&self, id: &str) -> crate::error::Result<Option<&QueueBinding>> {
95 match self.0.get(id) {
96 Some(ExternalBinding::Queue(b)) => Ok(Some(b)),
97 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
98 resource_id: id.to_string(),
99 expected: "queue".to_string(),
100 actual: other.binding_type().to_string(),
101 })),
102 None => Ok(None),
103 }
104 }
105
106 pub fn get_kv(&self, id: &str) -> crate::error::Result<Option<&KvBinding>> {
109 match self.0.get(id) {
110 Some(ExternalBinding::Kv(b)) => Ok(Some(b)),
111 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
112 resource_id: id.to_string(),
113 expected: "kv".to_string(),
114 actual: other.binding_type().to_string(),
115 })),
116 None => Ok(None),
117 }
118 }
119
120 pub fn get_artifact_registry(
123 &self,
124 id: &str,
125 ) -> crate::error::Result<Option<&ArtifactRegistryBinding>> {
126 match self.0.get(id) {
127 Some(ExternalBinding::ArtifactRegistry(b)) => Ok(Some(b)),
128 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
129 resource_id: id.to_string(),
130 expected: "artifact_registry".to_string(),
131 actual: other.binding_type().to_string(),
132 })),
133 None => Ok(None),
134 }
135 }
136
137 pub fn get_vault(&self, id: &str) -> crate::error::Result<Option<&VaultBinding>> {
140 match self.0.get(id) {
141 Some(ExternalBinding::Vault(b)) => Ok(Some(b)),
142 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
143 resource_id: id.to_string(),
144 expected: "vault".to_string(),
145 actual: other.binding_type().to_string(),
146 })),
147 None => Ok(None),
148 }
149 }
150
151 pub fn get_ai(&self, id: &str) -> crate::error::Result<Option<&ExternalAiBinding>> {
154 match self.0.get(id) {
155 Some(ExternalBinding::Ai(b)) => Ok(Some(b)),
156 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
157 resource_id: id.to_string(),
158 expected: "ai".to_string(),
159 actual: other.binding_type().to_string(),
160 })),
161 None => Ok(None),
162 }
163 }
164
165 pub fn get_container_apps_environment(
168 &self,
169 id: &str,
170 ) -> crate::error::Result<Option<&ContainerAppsEnvironmentBinding>> {
171 match self.0.get(id) {
172 Some(ExternalBinding::ContainerAppsEnvironment(b)) => Ok(Some(b)),
173 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
174 resource_id: id.to_string(),
175 expected: "azure_container_apps_environment".to_string(),
176 actual: other.binding_type().to_string(),
177 })),
178 None => Ok(None),
179 }
180 }
181
182 pub fn get_postgres(&self, id: &str) -> crate::error::Result<Option<&PostgresBinding>> {
184 match self.0.get(id) {
185 Some(ExternalBinding::Postgres(b)) => Ok(Some(b)),
186 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
187 resource_id: id.to_string(),
188 expected: "postgres".to_string(),
189 actual: other.binding_type().to_string(),
190 })),
191 None => Ok(None),
192 }
193 }
194
195 pub fn get_redis(&self, id: &str) -> crate::error::Result<Option<&RedisBinding>> {
197 match self.0.get(id) {
198 Some(ExternalBinding::Redis(b)) => Ok(Some(b)),
199 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
200 resource_id: id.to_string(),
201 expected: "redis".to_string(),
202 actual: other.binding_type().to_string(),
203 })),
204 None => Ok(None),
205 }
206 }
207
208 pub fn insert(&mut self, resource_id: impl Into<String>, binding: ExternalBinding) {
210 self.0.insert(resource_id.into(), binding);
211 }
212}
213
214impl ExternalBinding {
215 pub fn binding_type(&self) -> &'static str {
217 match self {
218 ExternalBinding::Storage(_) => "storage",
219 ExternalBinding::Queue(_) => "queue",
220 ExternalBinding::Kv(_) => "kv",
221 ExternalBinding::ArtifactRegistry(_) => "artifact_registry",
222 ExternalBinding::Vault(_) => "vault",
223 ExternalBinding::ContainerAppsEnvironment(_) => "azure_container_apps_environment",
224 ExternalBinding::Postgres(_) => "postgres",
225 ExternalBinding::Redis(_) => "redis",
226 ExternalBinding::Ai(_) => "ai",
227 }
228 }
229
230 pub fn to_resource_outputs(&self) -> Option<ResourceOutputs> {
239 match self {
240 ExternalBinding::ContainerAppsEnvironment(binding) => {
241 let environment_name = match &binding.environment_name {
244 BindingValue::Value(v) => v.clone(),
245 _ => return None,
246 };
247 let resource_id = match &binding.resource_id {
248 BindingValue::Value(v) => v.clone(),
249 _ => return None,
250 };
251 let resource_group_name = match &binding.resource_group_name {
252 BindingValue::Value(v) => v.clone(),
253 _ => return None,
254 };
255 let default_domain = match &binding.default_domain {
256 BindingValue::Value(v) => v.clone(),
257 _ => return None,
258 };
259 let static_ip = binding.static_ip.as_ref().and_then(|v| match v {
260 BindingValue::Value(v) => Some(v.clone()),
261 _ => None,
262 });
263
264 Some(ResourceOutputs::new(AzureContainerAppsEnvironmentOutputs {
265 environment_name,
266 resource_id,
267 resource_group_name,
268 default_domain,
269 static_ip,
270 custom_domain_verification_id: None,
271 }))
272 }
273 _ => None,
275 }
276 }
277
278 pub fn to_env_binding_value(&self) -> serde_json::Result<serde_json::Value> {
286 match self {
287 ExternalBinding::Storage(b) => serde_json::to_value(b),
288 ExternalBinding::Queue(b) => serde_json::to_value(b),
289 ExternalBinding::Kv(b) => serde_json::to_value(b),
290 ExternalBinding::ArtifactRegistry(b) => serde_json::to_value(b),
291 ExternalBinding::Vault(b) => serde_json::to_value(b),
292 ExternalBinding::ContainerAppsEnvironment(b) => serde_json::to_value(b),
293 ExternalBinding::Postgres(b) => serde_json::to_value(b),
294 ExternalBinding::Redis(b) => serde_json::to_value(b),
295 ExternalBinding::Ai(b) => {
296 serde_json::to_value(crate::bindings::AiBinding::External(b.clone()))
297 }
298 }
299 }
300}
301
302pub fn validate_binding_type(
304 resource: &Resource,
305 binding: &ExternalBinding,
306) -> crate::error::Result<()> {
307 let resource_type = resource.resource_type();
308 let resource_type_str = resource_type.as_ref();
309
310 let valid = match (resource_type_str, binding) {
311 ("storage", ExternalBinding::Storage(_)) => true,
312 ("queue", ExternalBinding::Queue(_)) => true,
313 ("kv", ExternalBinding::Kv(_)) => true,
314 ("artifact_registry", ExternalBinding::ArtifactRegistry(_)) => true,
315 ("vault", ExternalBinding::Vault(_)) => true,
316 ("azure_container_apps_environment", ExternalBinding::ContainerAppsEnvironment(_)) => true,
317 ("postgres", ExternalBinding::Postgres(_)) => true,
318 ("redis", ExternalBinding::Redis(_)) => true,
319 ("ai", ExternalBinding::Ai(_)) => true,
320 _ => false,
321 };
322
323 if !valid {
324 return Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
325 resource_id: resource.id().to_string(),
326 expected: resource_type_str.to_string(),
327 actual: binding.binding_type().to_string(),
328 }));
329 }
330 Ok(())
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::bindings::{KvBinding, StorageBinding};
337
338 #[test]
339 fn test_external_bindings_storage() {
340 let mut bindings = ExternalBindings::new();
341 bindings.insert(
342 "data-storage",
343 ExternalBinding::Storage(StorageBinding::s3("my-bucket")),
344 );
345
346 assert!(bindings.has("data-storage"));
347 assert!(bindings.get_storage("data-storage").unwrap().is_some());
348 assert!(bindings.get_queue("data-storage").is_err()); }
350
351 #[test]
352 fn test_external_bindings_kv() {
353 let mut bindings = ExternalBindings::new();
354 bindings.insert(
355 "cache",
356 ExternalBinding::Kv(KvBinding::redis("redis://localhost:6379")),
357 );
358
359 assert!(bindings.has("cache"));
360 assert!(bindings.get_kv("cache").unwrap().is_some());
361 assert!(bindings.get_storage("cache").is_err()); }
363
364 #[test]
365 fn test_external_bindings_ai() {
366 use crate::bindings::ExternalAiBinding;
367
368 let mut bindings = ExternalBindings::new();
369 bindings.insert(
370 "llm",
371 ExternalBinding::Ai(ExternalAiBinding {
372 provider: "openai".to_string(),
373 api_key: "sk-test".into(),
374 }),
375 );
376
377 assert!(bindings.has("llm"));
378 assert_eq!(bindings.get("llm").unwrap().binding_type(), "ai");
379 assert_eq!(bindings.get_ai("llm").unwrap().unwrap().provider, "openai");
380 assert!(bindings.get_kv("llm").is_err()); }
382
383 #[test]
384 fn test_external_bindings_serialization() {
385 let mut bindings = ExternalBindings::new();
386 bindings.insert(
387 "data",
388 ExternalBinding::Storage(StorageBinding::s3("test-bucket")),
389 );
390
391 let json = serde_json::to_string(&bindings).unwrap();
392 let deserialized: ExternalBindings = serde_json::from_str(&json).unwrap();
393 assert_eq!(bindings, deserialized);
394 }
395}