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