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, 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 Ai(ExternalAiBinding),
45}
46
47#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
51#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
52#[serde(transparent)]
53pub struct ExternalBindings(pub HashMap<String, ExternalBinding>);
54
55impl ExternalBindings {
56 pub fn new() -> Self {
58 Self(HashMap::new())
59 }
60
61 pub fn is_empty(&self) -> bool {
63 self.0.is_empty()
64 }
65
66 pub fn has(&self, resource_id: &str) -> bool {
68 self.0.contains_key(resource_id)
69 }
70
71 pub fn get(&self, resource_id: &str) -> Option<&ExternalBinding> {
73 self.0.get(resource_id)
74 }
75
76 pub fn get_storage(&self, id: &str) -> crate::error::Result<Option<&StorageBinding>> {
79 match self.0.get(id) {
80 Some(ExternalBinding::Storage(b)) => Ok(Some(b)),
81 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
82 resource_id: id.to_string(),
83 expected: "storage".to_string(),
84 actual: other.binding_type().to_string(),
85 })),
86 None => Ok(None),
87 }
88 }
89
90 pub fn get_queue(&self, id: &str) -> crate::error::Result<Option<&QueueBinding>> {
93 match self.0.get(id) {
94 Some(ExternalBinding::Queue(b)) => Ok(Some(b)),
95 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
96 resource_id: id.to_string(),
97 expected: "queue".to_string(),
98 actual: other.binding_type().to_string(),
99 })),
100 None => Ok(None),
101 }
102 }
103
104 pub fn get_kv(&self, id: &str) -> crate::error::Result<Option<&KvBinding>> {
107 match self.0.get(id) {
108 Some(ExternalBinding::Kv(b)) => Ok(Some(b)),
109 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
110 resource_id: id.to_string(),
111 expected: "kv".to_string(),
112 actual: other.binding_type().to_string(),
113 })),
114 None => Ok(None),
115 }
116 }
117
118 pub fn get_artifact_registry(
121 &self,
122 id: &str,
123 ) -> crate::error::Result<Option<&ArtifactRegistryBinding>> {
124 match self.0.get(id) {
125 Some(ExternalBinding::ArtifactRegistry(b)) => Ok(Some(b)),
126 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
127 resource_id: id.to_string(),
128 expected: "artifact_registry".to_string(),
129 actual: other.binding_type().to_string(),
130 })),
131 None => Ok(None),
132 }
133 }
134
135 pub fn get_vault(&self, id: &str) -> crate::error::Result<Option<&VaultBinding>> {
138 match self.0.get(id) {
139 Some(ExternalBinding::Vault(b)) => Ok(Some(b)),
140 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
141 resource_id: id.to_string(),
142 expected: "vault".to_string(),
143 actual: other.binding_type().to_string(),
144 })),
145 None => Ok(None),
146 }
147 }
148
149 pub fn get_ai(&self, id: &str) -> crate::error::Result<Option<&ExternalAiBinding>> {
152 match self.0.get(id) {
153 Some(ExternalBinding::Ai(b)) => Ok(Some(b)),
154 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
155 resource_id: id.to_string(),
156 expected: "ai".to_string(),
157 actual: other.binding_type().to_string(),
158 })),
159 None => Ok(None),
160 }
161 }
162
163 pub fn get_container_apps_environment(
166 &self,
167 id: &str,
168 ) -> crate::error::Result<Option<&ContainerAppsEnvironmentBinding>> {
169 match self.0.get(id) {
170 Some(ExternalBinding::ContainerAppsEnvironment(b)) => Ok(Some(b)),
171 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
172 resource_id: id.to_string(),
173 expected: "azure_container_apps_environment".to_string(),
174 actual: other.binding_type().to_string(),
175 })),
176 None => Ok(None),
177 }
178 }
179
180 pub fn get_postgres(&self, id: &str) -> crate::error::Result<Option<&PostgresBinding>> {
182 match self.0.get(id) {
183 Some(ExternalBinding::Postgres(b)) => Ok(Some(b)),
184 Some(other) => Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
185 resource_id: id.to_string(),
186 expected: "postgres".to_string(),
187 actual: other.binding_type().to_string(),
188 })),
189 None => Ok(None),
190 }
191 }
192
193 pub fn insert(&mut self, resource_id: impl Into<String>, binding: ExternalBinding) {
195 self.0.insert(resource_id.into(), binding);
196 }
197}
198
199impl ExternalBinding {
200 pub fn binding_type(&self) -> &'static str {
202 match self {
203 ExternalBinding::Storage(_) => "storage",
204 ExternalBinding::Queue(_) => "queue",
205 ExternalBinding::Kv(_) => "kv",
206 ExternalBinding::ArtifactRegistry(_) => "artifact_registry",
207 ExternalBinding::Vault(_) => "vault",
208 ExternalBinding::ContainerAppsEnvironment(_) => "azure_container_apps_environment",
209 ExternalBinding::Postgres(_) => "postgres",
210 ExternalBinding::Ai(_) => "ai",
211 }
212 }
213
214 pub fn to_resource_outputs(&self) -> Option<ResourceOutputs> {
223 match self {
224 ExternalBinding::ContainerAppsEnvironment(binding) => {
225 let environment_name = match &binding.environment_name {
228 BindingValue::Value(v) => v.clone(),
229 _ => return None,
230 };
231 let resource_id = match &binding.resource_id {
232 BindingValue::Value(v) => v.clone(),
233 _ => return None,
234 };
235 let resource_group_name = match &binding.resource_group_name {
236 BindingValue::Value(v) => v.clone(),
237 _ => return None,
238 };
239 let default_domain = match &binding.default_domain {
240 BindingValue::Value(v) => v.clone(),
241 _ => return None,
242 };
243 let static_ip = binding.static_ip.as_ref().and_then(|v| match v {
244 BindingValue::Value(v) => Some(v.clone()),
245 _ => None,
246 });
247
248 Some(ResourceOutputs::new(AzureContainerAppsEnvironmentOutputs {
249 environment_name,
250 resource_id,
251 resource_group_name,
252 default_domain,
253 static_ip,
254 custom_domain_verification_id: None,
255 }))
256 }
257 _ => None,
259 }
260 }
261
262 pub fn to_env_binding_value(&self) -> serde_json::Result<serde_json::Value> {
270 match self {
271 ExternalBinding::Storage(b) => serde_json::to_value(b),
272 ExternalBinding::Queue(b) => serde_json::to_value(b),
273 ExternalBinding::Kv(b) => serde_json::to_value(b),
274 ExternalBinding::ArtifactRegistry(b) => serde_json::to_value(b),
275 ExternalBinding::Vault(b) => serde_json::to_value(b),
276 ExternalBinding::ContainerAppsEnvironment(b) => serde_json::to_value(b),
277 ExternalBinding::Postgres(b) => serde_json::to_value(b),
278 ExternalBinding::Ai(b) => {
279 serde_json::to_value(crate::bindings::AiBinding::External(b.clone()))
280 }
281 }
282 }
283}
284
285pub fn validate_binding_type(
287 resource: &Resource,
288 binding: &ExternalBinding,
289) -> crate::error::Result<()> {
290 let resource_type = resource.resource_type();
291 let resource_type_str = resource_type.as_ref();
292
293 let valid = match (resource_type_str, binding) {
294 ("storage", ExternalBinding::Storage(_)) => true,
295 ("queue", ExternalBinding::Queue(_)) => true,
296 ("kv", ExternalBinding::Kv(_)) => true,
297 ("artifact_registry", ExternalBinding::ArtifactRegistry(_)) => true,
298 ("vault", ExternalBinding::Vault(_)) => true,
299 ("azure_container_apps_environment", ExternalBinding::ContainerAppsEnvironment(_)) => true,
300 ("postgres", ExternalBinding::Postgres(_)) => true,
301 ("ai", ExternalBinding::Ai(_)) => true,
302 _ => false,
303 };
304
305 if !valid {
306 return Err(AlienError::new(ErrorData::ExternalBindingTypeMismatch {
307 resource_id: resource.id().to_string(),
308 expected: resource_type_str.to_string(),
309 actual: binding.binding_type().to_string(),
310 }));
311 }
312 Ok(())
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::bindings::{KvBinding, StorageBinding};
319
320 #[test]
321 fn test_external_bindings_storage() {
322 let mut bindings = ExternalBindings::new();
323 bindings.insert(
324 "data-storage",
325 ExternalBinding::Storage(StorageBinding::s3("my-bucket")),
326 );
327
328 assert!(bindings.has("data-storage"));
329 assert!(bindings.get_storage("data-storage").unwrap().is_some());
330 assert!(bindings.get_queue("data-storage").is_err()); }
332
333 #[test]
334 fn test_external_bindings_kv() {
335 let mut bindings = ExternalBindings::new();
336 bindings.insert(
337 "cache",
338 ExternalBinding::Kv(KvBinding::redis("redis://localhost:6379")),
339 );
340
341 assert!(bindings.has("cache"));
342 assert!(bindings.get_kv("cache").unwrap().is_some());
343 assert!(bindings.get_storage("cache").is_err()); }
345
346 #[test]
347 fn test_external_bindings_ai() {
348 use crate::bindings::ExternalAiBinding;
349
350 let mut bindings = ExternalBindings::new();
351 bindings.insert(
352 "llm",
353 ExternalBinding::Ai(ExternalAiBinding {
354 provider: "openai".to_string(),
355 api_key: "sk-test".into(),
356 }),
357 );
358
359 assert!(bindings.has("llm"));
360 assert_eq!(bindings.get("llm").unwrap().binding_type(), "ai");
361 assert_eq!(bindings.get_ai("llm").unwrap().unwrap().provider, "openai");
362 assert!(bindings.get_kv("llm").is_err()); }
364
365 #[test]
366 fn test_external_bindings_serialization() {
367 let mut bindings = ExternalBindings::new();
368 bindings.insert(
369 "data",
370 ExternalBinding::Storage(StorageBinding::s3("test-bucket")),
371 );
372
373 let json = serde_json::to_string(&bindings).unwrap();
374 let deserialized: ExternalBindings = serde_json::from_str(&json).unwrap();
375 assert_eq!(bindings, deserialized);
376 }
377}