Skip to main content

alien_core/
resource.rs

1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::any::Any;
4use std::borrow::Cow;
5use std::fmt::Debug;
6#[cfg(feature = "openapi")]
7use utoipa::openapi::schema::AdditionalProperties;
8#[cfg(feature = "openapi")]
9use utoipa::openapi::{ObjectBuilder, Ref, RefOr, Schema, Type};
10#[cfg(feature = "openapi")]
11use utoipa::{PartialSchema, ToSchema};
12
13/// Type alias for resource type identifiers
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct ResourceType(pub Cow<'static, str>);
17
18impl ResourceType {
19    /// Create a new ResourceType from a static string (const-friendly)
20    pub const fn from_static(s: &'static str) -> Self {
21        Self(Cow::Borrowed(s))
22    }
23}
24
25impl From<String> for ResourceType {
26    fn from(s: String) -> Self {
27        Self(Cow::Owned(s))
28    }
29}
30
31impl From<&str> for ResourceType {
32    fn from(s: &str) -> Self {
33        Self(Cow::Owned(s.to_string()))
34    }
35}
36
37impl std::fmt::Display for ResourceType {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "{}", self.0)
40    }
41}
42
43impl From<ResourceType> for String {
44    fn from(val: ResourceType) -> Self {
45        val.0.into_owned()
46    }
47}
48
49impl AsRef<str> for ResourceType {
50    fn as_ref(&self) -> &str {
51        &self.0
52    }
53}
54
55#[cfg(feature = "openapi")]
56impl PartialSchema for ResourceType {
57    fn schema() -> RefOr<Schema> {
58        RefOr::T(Schema::Object(
59            ObjectBuilder::new()
60                .schema_type(Type::String)
61                .description(Some("Resource type identifier that determines the specific kind of resource. This field is used for polymorphic deserialization and resource-specific behavior."))
62                .examples([
63                    "worker",
64                    "storage",
65                    "queue",
66                    "redis",
67                    "postgres"
68                ])
69                .build()
70        ))
71    }
72}
73
74#[cfg(feature = "openapi")]
75impl ToSchema for ResourceType {
76    fn name() -> std::borrow::Cow<'static, str> {
77        std::borrow::Cow::Borrowed("ResourceType")
78    }
79}
80
81/// Trait that defines the interface for all resource types in the Alien system.
82/// This trait enables extensibility by allowing new resource types to be registered
83/// and managed alongside built-in resources.
84pub trait ResourceDefinition: Debug + Send + Sync + 'static {
85    /// Returns the resource type for this instance
86    fn get_resource_type(&self) -> ResourceType;
87
88    /// Returns the unique identifier for this specific resource instance
89    fn id(&self) -> &str;
90
91    /// Returns the list of other resources this resource depends on
92    fn get_dependencies(&self) -> Vec<ResourceRef>;
93
94    /// Returns the permission profile name for this resource, if it has one.
95    ///
96    /// Used by `ServiceAccountDependenciesMutation` to wire the corresponding
97    /// `{profile}-sa` service account as a declared dependency so the executor
98    /// enforces ordering and propagates SA changes automatically.
99    ///
100    /// Override in concrete types that carry a `permissions` field (Container, Worker).
101    fn get_permissions(&self) -> Option<&str> {
102        None
103    }
104
105    /// Validates if an update from the current configuration to a new configuration is allowed
106    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()>;
107
108    /// Provides access to the underlying concrete type for downcasting
109    fn as_any(&self) -> &dyn Any;
110
111    /// Provides mutable access to the underlying concrete type for downcasting
112    fn as_any_mut(&mut self) -> &mut dyn Any;
113
114    /// Creates a boxed clone of this resource definition
115    fn box_clone(&self) -> Box<dyn ResourceDefinition>;
116
117    /// For equality comparison between resource definitions
118    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool;
119
120    /// Serialize this resource to a JSON value (without the "type" tag - that's added by Resource)
121    fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
122}
123
124/// Clone implementation for boxed ResourceDefinition trait objects
125impl Clone for Box<dyn ResourceDefinition> {
126    fn clone(&self) -> Self {
127        self.box_clone()
128    }
129}
130
131#[derive(Debug, Clone)]
132pub struct Resource {
133    inner: Box<dyn ResourceDefinition>,
134}
135
136impl Serialize for Resource {
137    fn serialize<S: serde::Serializer>(
138        &self,
139        serializer: S,
140    ) -> std::result::Result<S::Ok, S::Error> {
141        let mut v = self
142            .inner
143            .to_json_value()
144            .map_err(serde::ser::Error::custom)?;
145        v.as_object_mut()
146            .ok_or_else(|| serde::ser::Error::custom("resource must serialize as object"))?
147            .insert(
148                "type".into(),
149                serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
150            );
151        v.serialize(serializer)
152    }
153}
154
155impl<'de> Deserialize<'de> for Resource {
156    fn deserialize<D: serde::Deserializer<'de>>(
157        deserializer: D,
158    ) -> std::result::Result<Self, D::Error> {
159        let mut value = serde_json::Value::deserialize(deserializer)?;
160        let type_tag = value
161            .get("type")
162            .and_then(|v| v.as_str())
163            .ok_or_else(|| serde::de::Error::missing_field("type"))?
164            .to_string();
165
166        // Remove the "type" tag before passing to concrete deserializer
167        // (structs with deny_unknown_fields would reject it)
168        if let Some(obj) = value.as_object_mut() {
169            obj.remove("type");
170        }
171
172        let inner: Box<dyn ResourceDefinition> = match type_tag.as_str() {
173            "vault" => Box::new(
174                serde_json::from_value::<crate::resources::Vault>(value)
175                    .map_err(serde::de::Error::custom)?,
176            ),
177            "worker" => Box::new(
178                serde_json::from_value::<crate::resources::Worker>(value)
179                    .map_err(serde::de::Error::custom)?,
180            ),
181            "daemon" => Box::new(
182                serde_json::from_value::<crate::resources::Daemon>(value)
183                    .map_err(serde::de::Error::custom)?,
184            ),
185            "container" => Box::new(
186                serde_json::from_value::<crate::resources::Container>(value)
187                    .map_err(serde::de::Error::custom)?,
188            ),
189            "compute-cluster" => Box::new(
190                serde_json::from_value::<crate::resources::ComputeCluster>(value)
191                    .map_err(serde::de::Error::custom)?,
192            ),
193            "kubernetes-cluster" => Box::new(
194                serde_json::from_value::<crate::resources::KubernetesCluster>(value)
195                    .map_err(serde::de::Error::custom)?,
196            ),
197            "storage" => Box::new(
198                serde_json::from_value::<crate::resources::Storage>(value)
199                    .map_err(serde::de::Error::custom)?,
200            ),
201            "key" => Box::new(
202                serde_json::from_value::<crate::resources::Key>(value)
203                    .map_err(serde::de::Error::custom)?,
204            ),
205            "queue" => Box::new(
206                serde_json::from_value::<crate::resources::Queue>(value)
207                    .map_err(serde::de::Error::custom)?,
208            ),
209            "sandbox" => Box::new(
210                serde_json::from_value::<crate::resources::Sandbox>(value)
211                    .map_err(serde::de::Error::custom)?,
212            ),
213            "email" => Box::new(
214                serde_json::from_value::<crate::resources::Email>(value)
215                    .map_err(serde::de::Error::custom)?,
216            ),
217            "kv" => Box::new(
218                serde_json::from_value::<crate::resources::Kv>(value)
219                    .map_err(serde::de::Error::custom)?,
220            ),
221            "postgres" => Box::new(
222                serde_json::from_value::<crate::resources::Postgres>(value)
223                    .map_err(serde::de::Error::custom)?,
224            ),
225            "ai" => Box::new(
226                serde_json::from_value::<crate::resources::Ai>(value)
227                    .map_err(serde::de::Error::custom)?,
228            ),
229            "network" => Box::new(
230                serde_json::from_value::<crate::resources::Network>(value)
231                    .map_err(serde::de::Error::custom)?,
232            ),
233            "build" => Box::new(
234                serde_json::from_value::<crate::resources::Build>(value)
235                    .map_err(serde::de::Error::custom)?,
236            ),
237            "service-account" => Box::new(
238                serde_json::from_value::<crate::resources::ServiceAccount>(value)
239                    .map_err(serde::de::Error::custom)?,
240            ),
241            "artifact-registry" => Box::new(
242                serde_json::from_value::<crate::resources::ArtifactRegistry>(value)
243                    .map_err(serde::de::Error::custom)?,
244            ),
245            "service_activation" => Box::new(
246                serde_json::from_value::<crate::resources::ServiceActivation>(value)
247                    .map_err(serde::de::Error::custom)?,
248            ),
249            "remote-stack-management" => Box::new(
250                serde_json::from_value::<crate::resources::RemoteStackManagement>(value)
251                    .map_err(serde::de::Error::custom)?,
252            ),
253            "resource-access" => Box::new(
254                serde_json::from_value::<crate::resources::RemoteBindings>(value)
255                    .map_err(serde::de::Error::custom)?,
256            ),
257            "azure_resource_group" => Box::new(
258                serde_json::from_value::<crate::resources::AzureResourceGroup>(value)
259                    .map_err(serde::de::Error::custom)?,
260            ),
261            "gcp_agent_platform_engine" => Box::new(
262                serde_json::from_value::<crate::resources::GcpAgentPlatformEngine>(value)
263                    .map_err(serde::de::Error::custom)?,
264            ),
265            "azure_storage_account" => Box::new(
266                serde_json::from_value::<crate::resources::AzureStorageAccount>(value)
267                    .map_err(serde::de::Error::custom)?,
268            ),
269            "azure_container_apps_environment" => Box::new(
270                serde_json::from_value::<crate::resources::AzureContainerAppsEnvironment>(value)
271                    .map_err(serde::de::Error::custom)?,
272            ),
273            "azure_service_bus_namespace" => Box::new(
274                serde_json::from_value::<crate::resources::AzureServiceBusNamespace>(value)
275                    .map_err(serde::de::Error::custom)?,
276            ),
277            "experimental/aws-opensearch" => Box::new(
278                serde_json::from_value::<crate::resources::AwsOpenSearch>(value)
279                    .map_err(serde::de::Error::custom)?,
280            ),
281            other => {
282                return Err(serde::de::Error::unknown_variant(
283                    other,
284                    &[
285                        "vault",
286                        "worker",
287                        "daemon",
288                        "container",
289                        "compute-cluster",
290                        "kubernetes-cluster",
291                        "storage",
292                        "key",
293                        "queue",
294                        "sandbox",
295                        "email",
296                        "kv",
297                        "postgres",
298                        "ai",
299                        "network",
300                        "build",
301                        "service-account",
302                        "artifact-registry",
303                        "service_activation",
304                        "remote-stack-management",
305                        "resource-access",
306                        "azure_resource_group",
307                        "gcp_agent_platform_engine",
308                        "azure_storage_account",
309                        "azure_container_apps_environment",
310                        "azure_service_bus_namespace",
311                        "experimental/aws-opensearch",
312                    ],
313                ))
314            }
315        };
316
317        Ok(Resource { inner })
318    }
319}
320
321impl Resource {
322    /// Creates a new Resource from any type that implements ResourceDefinition
323    pub fn new<T: ResourceDefinition>(resource: T) -> Self {
324        Self {
325            inner: Box::new(resource),
326        }
327    }
328
329    /// Creates a new Resource from a boxed ResourceDefinition
330    pub fn from_boxed(boxed_resource: Box<dyn ResourceDefinition>) -> Self {
331        Self {
332            inner: boxed_resource,
333        }
334    }
335
336    /// Returns the resource type identifier
337    pub fn resource_type(&self) -> ResourceType {
338        self.inner.get_resource_type()
339    }
340
341    /// Returns the unique identifier for this resource instance
342    pub fn id(&self) -> &str {
343        self.inner.id()
344    }
345
346    /// Returns the list of other resources this resource depends on
347    pub fn get_dependencies(&self) -> Vec<ResourceRef> {
348        self.inner.get_dependencies()
349    }
350
351    /// Returns the permission profile name for this resource, if it has one.
352    pub fn get_permissions(&self) -> Option<&str> {
353        self.inner.get_permissions()
354    }
355
356    /// Validates if an update from the current configuration to a new configuration is allowed
357    pub fn validate_update(&self, new_config: &Resource) -> Result<()> {
358        self.inner.validate_update(new_config.inner.as_ref())
359    }
360
361    /// Provides access to the underlying ResourceDefinition trait object
362    pub fn as_resource_definition(&self) -> &dyn ResourceDefinition {
363        self.inner.as_ref()
364    }
365
366    /// Generic downcasting for any type
367    pub fn downcast_ref<T: ResourceDefinition + 'static>(&self) -> Option<&T> {
368        self.inner.as_any().downcast_ref::<T>()
369    }
370
371    /// Generic mutable downcasting for any type
372    pub fn downcast_mut<T: ResourceDefinition + 'static>(&mut self) -> Option<&mut T> {
373        self.inner.as_any_mut().downcast_mut::<T>()
374    }
375}
376
377impl PartialEq for Resource {
378    fn eq(&self, other: &Self) -> bool {
379        self.inner.resource_eq(other.inner.as_ref())
380    }
381}
382
383impl Eq for Resource {}
384
385/// OpenAPI schema implementation for Resource.
386///
387/// The schema represents the flattened JSON structure of any resource type in the Alien system.
388/// All resources have a common base structure with `type` and `id` fields, plus type-specific
389/// additional properties that vary depending on the concrete resource implementation.
390///
391/// # Schema Structure
392/// - `type` (required): The resource type identifier (e.g., "worker", "storage", "queue")
393/// - `id` (required): The unique identifier for this specific resource instance
394/// - Additional properties: Type-specific fields that vary by resource type (e.g., Worker has `code`, `memory_mb`, etc.)
395///
396/// # Example JSON
397/// ```json
398/// {
399///   "type": "worker",
400///   "id": "my-function",
401///   "code": { "type": "image", "image": "my-image:latest" },
402///   "memoryMb": 512,
403///   "timeoutSeconds": 30
404/// }
405/// ```
406#[cfg(feature = "openapi")]
407impl PartialSchema for Resource {
408    fn schema() -> RefOr<Schema> {
409        RefOr::T(Schema::Object(
410            ObjectBuilder::new()
411                .schema_type(Type::Object)
412                .property("type", Ref::from_schema_name("ResourceType"))
413                .property("id",
414                    ObjectBuilder::new()
415                        .schema_type(Type::String)
416                        .description(Some("The unique identifier for this specific resource instance. Must contain only alphanumeric characters, hyphens, and underscores ([A-Za-z0-9-_]). Maximum 64 characters."))
417                        .build()
418                )
419                .required("type")
420                .required("id")
421                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
422                .description(Some("Resource that can hold any resource type in the Alien system. All resources share common 'type' and 'id' fields with additional type-specific properties."))
423                .build()
424        ))
425    }
426}
427
428#[cfg(feature = "openapi")]
429impl ToSchema for Resource {
430    fn name() -> std::borrow::Cow<'static, str> {
431        std::borrow::Cow::Borrowed("BaseResource")
432    }
433}
434
435/// Reference to a resource by its stable id and resource type.
436#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
437#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
438#[serde(rename_all = "camelCase")]
439pub struct ResourceRef {
440    #[serde(rename = "type")]
441    pub resource_type: ResourceType,
442    pub id: String,
443}
444
445impl ResourceRef {
446    /// Creates a new ResourceRef
447    pub fn new(resource_type: ResourceType, id: impl Into<String>) -> Self {
448        Self {
449            resource_type,
450            id: id.into(),
451        }
452    }
453
454    /// Returns the resource type
455    pub fn resource_type(&self) -> &ResourceType {
456        &self.resource_type
457    }
458
459    /// Returns the resource id
460    pub fn id(&self) -> &str {
461        &self.id
462    }
463}
464
465impl<T: ResourceDefinition> From<&T> for ResourceRef {
466    fn from(resource: &T) -> Self {
467        Self::new(resource.get_resource_type(), resource.id())
468    }
469}
470
471impl From<&Resource> for ResourceRef {
472    fn from(resource: &Resource) -> Self {
473        Self::new(resource.resource_type(), resource.id())
474    }
475}
476
477/// Trait that defines the interface for all resource output types in the Alien system.
478/// This trait enables extensibility by allowing new resource output types to be registered
479/// and managed alongside built-in resource outputs.
480pub trait ResourceOutputsDefinition: Debug + Send + Sync + 'static {
481    /// Returns the resource type for this instance
482    fn get_resource_type(&self) -> ResourceType;
483
484    /// Provides access to the underlying concrete type for downcasting
485    fn as_any(&self) -> &dyn Any;
486
487    /// Creates a boxed clone of this resource outputs
488    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition>;
489
490    /// For equality comparison between resource outputs
491    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool;
492
493    /// Serialize this resource outputs to a JSON value (without the "type" tag - that's added by ResourceOutputs)
494    fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
495}
496
497/// Clone implementation for boxed ResourceOutputsDefinition trait objects
498impl Clone for Box<dyn ResourceOutputsDefinition> {
499    fn clone(&self) -> Self {
500        self.box_clone()
501    }
502}
503
504/// New Resource outputs wrapper that can hold any ResourceOutputsDefinition.
505/// This replaces the old ResourceOutputs enum to enable runtime extensibility.
506#[derive(Debug, Clone)]
507pub struct ResourceOutputs {
508    inner: Box<dyn ResourceOutputsDefinition>,
509}
510
511impl Serialize for ResourceOutputs {
512    fn serialize<S: serde::Serializer>(
513        &self,
514        serializer: S,
515    ) -> std::result::Result<S::Ok, S::Error> {
516        let mut v = self
517            .inner
518            .to_json_value()
519            .map_err(serde::ser::Error::custom)?;
520        v.as_object_mut()
521            .ok_or_else(|| serde::ser::Error::custom("resource outputs must serialize as object"))?
522            .insert(
523                "type".into(),
524                serde_json::Value::String(self.inner.get_resource_type().0.into_owned()),
525            );
526        v.serialize(serializer)
527    }
528}
529
530impl<'de> Deserialize<'de> for ResourceOutputs {
531    fn deserialize<D: serde::Deserializer<'de>>(
532        deserializer: D,
533    ) -> std::result::Result<Self, D::Error> {
534        let mut value = serde_json::Value::deserialize(deserializer)?;
535        let type_tag = value
536            .get("type")
537            .and_then(|v| v.as_str())
538            .ok_or_else(|| serde::de::Error::missing_field("type"))?
539            .to_string();
540
541        // Remove the "type" tag before passing to concrete deserializer
542        // (structs with deny_unknown_fields would reject it)
543        if let Some(obj) = value.as_object_mut() {
544            obj.remove("type");
545        }
546
547        let inner: Box<dyn ResourceOutputsDefinition> = match type_tag.as_str() {
548            "vault" => Box::new(
549                serde_json::from_value::<crate::resources::VaultOutputs>(value)
550                    .map_err(serde::de::Error::custom)?,
551            ),
552            "worker" => Box::new(
553                serde_json::from_value::<crate::resources::WorkerOutputs>(value)
554                    .map_err(serde::de::Error::custom)?,
555            ),
556            "daemon" => Box::new(
557                serde_json::from_value::<crate::resources::DaemonOutputs>(value)
558                    .map_err(serde::de::Error::custom)?,
559            ),
560            "container" => Box::new(
561                serde_json::from_value::<crate::resources::ContainerOutputs>(value)
562                    .map_err(serde::de::Error::custom)?,
563            ),
564            "compute-cluster" => Box::new(
565                serde_json::from_value::<crate::resources::ComputeClusterOutputs>(value)
566                    .map_err(serde::de::Error::custom)?,
567            ),
568            "storage" => Box::new(
569                serde_json::from_value::<crate::resources::StorageOutputs>(value)
570                    .map_err(serde::de::Error::custom)?,
571            ),
572            "key" => Box::new(
573                serde_json::from_value::<crate::resources::KeyOutputs>(value)
574                    .map_err(serde::de::Error::custom)?,
575            ),
576            "queue" => Box::new(
577                serde_json::from_value::<crate::resources::QueueOutputs>(value)
578                    .map_err(serde::de::Error::custom)?,
579            ),
580            "sandbox" => Box::new(
581                serde_json::from_value::<crate::resources::SandboxOutputs>(value)
582                    .map_err(serde::de::Error::custom)?,
583            ),
584            "email" => Box::new(
585                serde_json::from_value::<crate::resources::EmailOutputs>(value)
586                    .map_err(serde::de::Error::custom)?,
587            ),
588            "kv" => Box::new(
589                serde_json::from_value::<crate::resources::KvOutputs>(value)
590                    .map_err(serde::de::Error::custom)?,
591            ),
592            "postgres" => Box::new(
593                serde_json::from_value::<crate::resources::PostgresOutputs>(value)
594                    .map_err(serde::de::Error::custom)?,
595            ),
596            "ai" => Box::new(
597                serde_json::from_value::<crate::resources::AiOutputs>(value)
598                    .map_err(serde::de::Error::custom)?,
599            ),
600            "network" => Box::new(
601                serde_json::from_value::<crate::resources::NetworkOutputs>(value)
602                    .map_err(serde::de::Error::custom)?,
603            ),
604            "build" => Box::new(
605                serde_json::from_value::<crate::resources::BuildOutputs>(value)
606                    .map_err(serde::de::Error::custom)?,
607            ),
608            "service-account" => Box::new(
609                serde_json::from_value::<crate::resources::ServiceAccountOutputs>(value)
610                    .map_err(serde::de::Error::custom)?,
611            ),
612            "artifact-registry" => Box::new(
613                serde_json::from_value::<crate::resources::ArtifactRegistryOutputs>(value)
614                    .map_err(serde::de::Error::custom)?,
615            ),
616            "service_activation" => Box::new(
617                serde_json::from_value::<crate::resources::ServiceActivationOutputs>(value)
618                    .map_err(serde::de::Error::custom)?,
619            ),
620            "remote-stack-management" => Box::new(
621                serde_json::from_value::<crate::resources::RemoteStackManagementOutputs>(value)
622                    .map_err(serde::de::Error::custom)?,
623            ),
624            "resource-access" => Box::new(
625                serde_json::from_value::<crate::resources::RemoteBindingsOutputs>(value)
626                    .map_err(serde::de::Error::custom)?,
627            ),
628            "kubernetes-cluster" => Box::new(
629                serde_json::from_value::<crate::resources::KubernetesClusterOutputs>(value)
630                    .map_err(serde::de::Error::custom)?,
631            ),
632            "azure_resource_group" => Box::new(
633                serde_json::from_value::<crate::resources::AzureResourceGroupOutputs>(value)
634                    .map_err(serde::de::Error::custom)?,
635            ),
636            "azure_storage_account" => Box::new(
637                serde_json::from_value::<crate::resources::AzureStorageAccountOutputs>(value)
638                    .map_err(serde::de::Error::custom)?,
639            ),
640            "azure_container_apps_environment" => Box::new(
641                serde_json::from_value::<crate::resources::AzureContainerAppsEnvironmentOutputs>(
642                    value,
643                )
644                .map_err(serde::de::Error::custom)?,
645            ),
646            "azure_service_bus_namespace" => Box::new(
647                serde_json::from_value::<crate::resources::AzureServiceBusNamespaceOutputs>(value)
648                    .map_err(serde::de::Error::custom)?,
649            ),
650            "experimental/aws-opensearch" => Box::new(
651                serde_json::from_value::<crate::resources::AwsOpenSearchOutputs>(value)
652                    .map_err(serde::de::Error::custom)?,
653            ),
654            other => {
655                return Err(serde::de::Error::unknown_variant(
656                    other,
657                    &[
658                        "vault",
659                        "worker",
660                        "daemon",
661                        "container",
662                        "compute-cluster",
663                        "storage",
664                        "key",
665                        "queue",
666                        "sandbox",
667                        "email",
668                        "kv",
669                        "postgres",
670                        "ai",
671                        "network",
672                        "build",
673                        "service-account",
674                        "artifact-registry",
675                        "service_activation",
676                        "remote-stack-management",
677                        "resource-access",
678                        "kubernetes-cluster",
679                        "azure_resource_group",
680                        "azure_storage_account",
681                        "azure_container_apps_environment",
682                        "azure_service_bus_namespace",
683                        "experimental/aws-opensearch",
684                    ],
685                ))
686            }
687        };
688
689        Ok(ResourceOutputs { inner })
690    }
691}
692
693impl ResourceOutputs {
694    /// Creates a new ResourceOutputs from any type that implements ResourceOutputsDefinition
695    pub fn new<T: ResourceOutputsDefinition>(outputs: T) -> Self {
696        Self {
697            inner: Box::new(outputs),
698        }
699    }
700
701    /// Provides access to the underlying ResourceOutputsDefinition trait object
702    pub fn as_resource_outputs(&self) -> &dyn ResourceOutputsDefinition {
703        self.inner.as_ref()
704    }
705
706    /// Generic downcasting for any type
707    pub fn downcast_ref<T: ResourceOutputsDefinition + 'static>(&self) -> Option<&T> {
708        self.inner.as_any().downcast_ref::<T>()
709    }
710}
711
712impl PartialEq for ResourceOutputs {
713    fn eq(&self, other: &Self) -> bool {
714        self.inner.outputs_eq(other.inner.as_ref())
715    }
716}
717
718impl Eq for ResourceOutputs {}
719
720/// OpenAPI schema implementation for ResourceOutputs.
721///
722/// The schema represents the flattened JSON structure of any resource outputs in the Alien system.
723/// All resource outputs have a common base structure with a `type` field, plus type-specific
724/// additional properties that vary depending on the concrete resource implementation.
725///
726/// # Schema Structure
727/// - `type` (required): The resource type identifier (e.g., "worker", "storage", "queue")
728/// - Additional properties: Type-specific output fields that vary by resource type
729///
730/// # Example JSON
731/// ```json
732/// {
733///   "type": "worker",
734///   "functionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-function",
735///   "functionUrl": "https://abc123.lambda-url.us-east-1.on.aws/"
736/// }
737/// ```
738#[cfg(feature = "openapi")]
739impl PartialSchema for ResourceOutputs {
740    fn schema() -> RefOr<Schema> {
741        RefOr::T(Schema::Object(
742            ObjectBuilder::new()
743                .schema_type(Type::Object)
744                .property("type", Ref::from_schema_name("ResourceType"))
745                .required("type")
746                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
747                .description(Some("Resource outputs that can hold output data for any resource type in the Alien system. All resource outputs share a common 'type' field with additional type-specific output properties."))
748                .build()
749        ))
750    }
751}
752
753#[cfg(feature = "openapi")]
754impl ToSchema for ResourceOutputs {
755    fn name() -> std::borrow::Cow<'static, str> {
756        std::borrow::Cow::Borrowed("BaseResourceOutputs")
757    }
758}
759
760/// Represents the high-level status of a resource during its lifecycle.
761#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
762#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
763#[serde(rename_all = "kebab-case")]
764pub enum ResourceStatus {
765    Pending,      // Initial state before any action starts
766    Provisioning, // Resource is being created or updated
767    ProvisionFailed,
768    Running, // Resource is active and configured as desired
769    Updating,
770    UpdateFailed,
771    Deleting, // Resource is being removed
772    DeleteFailed,
773    TeardownRequired, // Runtime-owned parts were removed; setup-owned parts remain
774    Deleted,          // Resource has been successfully removed (terminal state)
775    RefreshFailed,    // Resource heartbeat/health check failed
776}
777
778impl ResourceStatus {
779    pub fn is_terminal(&self) -> bool {
780        match self {
781            ResourceStatus::TeardownRequired => true,
782            ResourceStatus::Deleted => true,
783            ResourceStatus::ProvisionFailed => true,
784            ResourceStatus::UpdateFailed => true,
785            ResourceStatus::DeleteFailed => true,
786            ResourceStatus::RefreshFailed => true,
787            _ => false, // Pending, Provisioning, Updating, Deleting are not terminal
788        }
789    }
790}
791
792/// Describes the lifecycle of a resource within a stack, determining how it's managed and deployed.
793#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, Deserialize)]
794#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
795#[serde(rename_all = "kebab-case")]
796pub enum ResourceLifecycle {
797    /// Frozen resources are owned by setup. Setup creates, updates, and
798    /// deletes them. Alien may heartbeat them and may run explicit management
799    /// operations when setup granted management permissions.
800    Frozen,
801
802    /// Live resources are owned by Alien. Alien creates, updates, deletes, and
803    /// replaces them after setup, so Live resources require provision
804    /// permissions.
805    Live,
806}