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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct ResourceType(pub Cow<'static, str>);
17
18impl ResourceType {
19 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 "function",
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
81pub trait ResourceDefinition: Debug + Send + Sync + 'static {
85 fn get_resource_type(&self) -> ResourceType;
87
88 fn id(&self) -> &str;
90
91 fn get_dependencies(&self) -> Vec<ResourceRef>;
93
94 fn get_permissions(&self) -> Option<&str> {
102 None
103 }
104
105 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()>;
107
108 fn as_any(&self) -> &dyn Any;
110
111 fn as_any_mut(&mut self) -> &mut dyn Any;
113
114 fn box_clone(&self) -> Box<dyn ResourceDefinition>;
116
117 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool;
119
120 fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
122}
123
124impl 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>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
138 let mut v = self.inner.to_json_value().map_err(serde::ser::Error::custom)?;
139 v.as_object_mut()
140 .ok_or_else(|| serde::ser::Error::custom("resource must serialize as object"))?
141 .insert("type".into(), serde_json::Value::String(self.inner.get_resource_type().0.into_owned()));
142 v.serialize(serializer)
143 }
144}
145
146impl<'de> Deserialize<'de> for Resource {
147 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
148 let mut value = serde_json::Value::deserialize(deserializer)?;
149 let type_tag = value.get("type").and_then(|v| v.as_str())
150 .ok_or_else(|| serde::de::Error::missing_field("type"))?
151 .to_string();
152
153 if let Some(obj) = value.as_object_mut() {
156 obj.remove("type");
157 }
158
159 let inner: Box<dyn ResourceDefinition> = match type_tag.as_str() {
160 "vault" => Box::new(serde_json::from_value::<crate::resources::Vault>(value).map_err(serde::de::Error::custom)?),
161 "function" => Box::new(serde_json::from_value::<crate::resources::Function>(value).map_err(serde::de::Error::custom)?),
162 "container" => Box::new(serde_json::from_value::<crate::resources::Container>(value).map_err(serde::de::Error::custom)?),
163 "container-cluster" => Box::new(serde_json::from_value::<crate::resources::ContainerCluster>(value).map_err(serde::de::Error::custom)?),
164 "storage" => Box::new(serde_json::from_value::<crate::resources::Storage>(value).map_err(serde::de::Error::custom)?),
165 "queue" => Box::new(serde_json::from_value::<crate::resources::Queue>(value).map_err(serde::de::Error::custom)?),
166 "kv" => Box::new(serde_json::from_value::<crate::resources::Kv>(value).map_err(serde::de::Error::custom)?),
167 "network" => Box::new(serde_json::from_value::<crate::resources::Network>(value).map_err(serde::de::Error::custom)?),
168 "build" => Box::new(serde_json::from_value::<crate::resources::Build>(value).map_err(serde::de::Error::custom)?),
169 "service-account" => Box::new(serde_json::from_value::<crate::resources::ServiceAccount>(value).map_err(serde::de::Error::custom)?),
170 "artifact-registry" => Box::new(serde_json::from_value::<crate::resources::ArtifactRegistry>(value).map_err(serde::de::Error::custom)?),
171 "service_activation" => Box::new(serde_json::from_value::<crate::resources::ServiceActivation>(value).map_err(serde::de::Error::custom)?),
172 "remote-stack-management" => Box::new(serde_json::from_value::<crate::resources::RemoteStackManagement>(value).map_err(serde::de::Error::custom)?),
173 "azure_resource_group" => Box::new(serde_json::from_value::<crate::resources::AzureResourceGroup>(value).map_err(serde::de::Error::custom)?),
174 "azure_storage_account" => Box::new(serde_json::from_value::<crate::resources::AzureStorageAccount>(value).map_err(serde::de::Error::custom)?),
175 "azure_container_apps_environment" => Box::new(serde_json::from_value::<crate::resources::AzureContainerAppsEnvironment>(value).map_err(serde::de::Error::custom)?),
176 "azure_service_bus_namespace" => Box::new(serde_json::from_value::<crate::resources::AzureServiceBusNamespace>(value).map_err(serde::de::Error::custom)?),
177 other => return Err(serde::de::Error::unknown_variant(other, &[
178 "vault", "function", "container", "container-cluster", "storage", "queue", "kv",
179 "network", "build", "service-account", "artifact-registry", "service_activation",
180 "remote-stack-management", "azure_resource_group", "azure_storage_account",
181 "azure_container_apps_environment", "azure_service_bus_namespace",
182 ])),
183 };
184
185 Ok(Resource { inner })
186 }
187}
188
189impl Resource {
190 pub fn new<T: ResourceDefinition>(resource: T) -> Self {
192 Self {
193 inner: Box::new(resource),
194 }
195 }
196
197 pub fn from_boxed(boxed_resource: Box<dyn ResourceDefinition>) -> Self {
199 Self {
200 inner: boxed_resource,
201 }
202 }
203
204 pub fn resource_type(&self) -> ResourceType {
206 self.inner.get_resource_type()
207 }
208
209 pub fn id(&self) -> &str {
211 self.inner.id()
212 }
213
214 pub fn get_dependencies(&self) -> Vec<ResourceRef> {
216 self.inner.get_dependencies()
217 }
218
219 pub fn get_permissions(&self) -> Option<&str> {
221 self.inner.get_permissions()
222 }
223
224 pub fn validate_update(&self, new_config: &Resource) -> Result<()> {
226 self.inner.validate_update(new_config.inner.as_ref())
227 }
228
229 pub fn as_resource_definition(&self) -> &dyn ResourceDefinition {
231 self.inner.as_ref()
232 }
233
234 pub fn downcast_ref<T: ResourceDefinition + 'static>(&self) -> Option<&T> {
236 self.inner.as_any().downcast_ref::<T>()
237 }
238
239 pub fn downcast_mut<T: ResourceDefinition + 'static>(&mut self) -> Option<&mut T> {
241 self.inner.as_any_mut().downcast_mut::<T>()
242 }
243}
244
245impl PartialEq for Resource {
246 fn eq(&self, other: &Self) -> bool {
247 self.inner.resource_eq(other.inner.as_ref())
248 }
249}
250
251impl Eq for Resource {}
252
253#[cfg(feature = "openapi")]
275impl PartialSchema for Resource {
276 fn schema() -> RefOr<Schema> {
277 RefOr::T(Schema::Object(
278 ObjectBuilder::new()
279 .schema_type(Type::Object)
280 .property("type", Ref::from_schema_name("ResourceType"))
281 .property("id",
282 ObjectBuilder::new()
283 .schema_type(Type::String)
284 .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."))
285 .build()
286 )
287 .required("type")
288 .required("id")
289 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
290 .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."))
291 .build()
292 ))
293 }
294}
295
296#[cfg(feature = "openapi")]
297impl ToSchema for Resource {
298 fn name() -> std::borrow::Cow<'static, str> {
299 std::borrow::Cow::Borrowed("BaseResource")
300 }
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
306#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
307#[serde(rename_all = "camelCase")]
308pub struct ResourceRef {
309 #[serde(rename = "type")]
310 pub resource_type: ResourceType,
311 pub id: String,
312}
313
314impl ResourceRef {
315 pub fn new(resource_type: ResourceType, id: impl Into<String>) -> Self {
317 Self {
318 resource_type,
319 id: id.into(),
320 }
321 }
322
323 pub fn resource_type(&self) -> &ResourceType {
325 &self.resource_type
326 }
327
328 pub fn id(&self) -> &str {
330 &self.id
331 }
332}
333
334impl<T: ResourceDefinition> From<&T> for ResourceRef {
335 fn from(resource: &T) -> Self {
336 Self::new(resource.get_resource_type(), resource.id())
337 }
338}
339
340impl From<&Resource> for ResourceRef {
341 fn from(resource: &Resource) -> Self {
342 Self::new(resource.resource_type(), resource.id())
343 }
344}
345
346pub trait ResourceOutputsDefinition: Debug + Send + Sync + 'static {
350 fn get_resource_type(&self) -> ResourceType;
352
353 fn as_any(&self) -> &dyn Any;
355
356 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition>;
358
359 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool;
361
362 fn to_json_value(&self) -> serde_json::Result<serde_json::Value>;
364}
365
366impl Clone for Box<dyn ResourceOutputsDefinition> {
368 fn clone(&self) -> Self {
369 self.box_clone()
370 }
371}
372
373#[derive(Debug, Clone)]
376pub struct ResourceOutputs {
377 inner: Box<dyn ResourceOutputsDefinition>,
378}
379
380impl Serialize for ResourceOutputs {
381 fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
382 let mut v = self.inner.to_json_value().map_err(serde::ser::Error::custom)?;
383 v.as_object_mut()
384 .ok_or_else(|| serde::ser::Error::custom("resource outputs must serialize as object"))?
385 .insert("type".into(), serde_json::Value::String(self.inner.get_resource_type().0.into_owned()));
386 v.serialize(serializer)
387 }
388}
389
390impl<'de> Deserialize<'de> for ResourceOutputs {
391 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
392 let mut value = serde_json::Value::deserialize(deserializer)?;
393 let type_tag = value.get("type").and_then(|v| v.as_str())
394 .ok_or_else(|| serde::de::Error::missing_field("type"))?
395 .to_string();
396
397 if let Some(obj) = value.as_object_mut() {
400 obj.remove("type");
401 }
402
403 let inner: Box<dyn ResourceOutputsDefinition> = match type_tag.as_str() {
404 "vault" => Box::new(serde_json::from_value::<crate::resources::VaultOutputs>(value).map_err(serde::de::Error::custom)?),
405 "function" => Box::new(serde_json::from_value::<crate::resources::FunctionOutputs>(value).map_err(serde::de::Error::custom)?),
406 "container" => Box::new(serde_json::from_value::<crate::resources::ContainerOutputs>(value).map_err(serde::de::Error::custom)?),
407 "container-cluster" => Box::new(serde_json::from_value::<crate::resources::ContainerClusterOutputs>(value).map_err(serde::de::Error::custom)?),
408 "storage" => Box::new(serde_json::from_value::<crate::resources::StorageOutputs>(value).map_err(serde::de::Error::custom)?),
409 "queue" => Box::new(serde_json::from_value::<crate::resources::QueueOutputs>(value).map_err(serde::de::Error::custom)?),
410 "kv" => Box::new(serde_json::from_value::<crate::resources::KvOutputs>(value).map_err(serde::de::Error::custom)?),
411 "network" => Box::new(serde_json::from_value::<crate::resources::NetworkOutputs>(value).map_err(serde::de::Error::custom)?),
412 "build" => Box::new(serde_json::from_value::<crate::resources::BuildOutputs>(value).map_err(serde::de::Error::custom)?),
413 "service-account" => Box::new(serde_json::from_value::<crate::resources::ServiceAccountOutputs>(value).map_err(serde::de::Error::custom)?),
414 "artifact-registry" => Box::new(serde_json::from_value::<crate::resources::ArtifactRegistryOutputs>(value).map_err(serde::de::Error::custom)?),
415 "service_activation" => Box::new(serde_json::from_value::<crate::resources::ServiceActivationOutputs>(value).map_err(serde::de::Error::custom)?),
416 "remote-stack-management" => Box::new(serde_json::from_value::<crate::resources::RemoteStackManagementOutputs>(value).map_err(serde::de::Error::custom)?),
417 "azure_resource_group" => Box::new(serde_json::from_value::<crate::resources::AzureResourceGroupOutputs>(value).map_err(serde::de::Error::custom)?),
418 "azure_storage_account" => Box::new(serde_json::from_value::<crate::resources::AzureStorageAccountOutputs>(value).map_err(serde::de::Error::custom)?),
419 "azure_container_apps_environment" => Box::new(serde_json::from_value::<crate::resources::AzureContainerAppsEnvironmentOutputs>(value).map_err(serde::de::Error::custom)?),
420 "azure_service_bus_namespace" => Box::new(serde_json::from_value::<crate::resources::AzureServiceBusNamespaceOutputs>(value).map_err(serde::de::Error::custom)?),
421 other => return Err(serde::de::Error::unknown_variant(other, &[
422 "vault", "function", "container", "container-cluster", "storage", "queue", "kv",
423 "network", "build", "service-account", "artifact-registry", "service_activation",
424 "remote-stack-management", "azure_resource_group", "azure_storage_account",
425 "azure_container_apps_environment", "azure_service_bus_namespace",
426 ])),
427 };
428
429 Ok(ResourceOutputs { inner })
430 }
431}
432
433impl ResourceOutputs {
434 pub fn new<T: ResourceOutputsDefinition>(outputs: T) -> Self {
436 Self {
437 inner: Box::new(outputs),
438 }
439 }
440
441 pub fn as_resource_outputs(&self) -> &dyn ResourceOutputsDefinition {
443 self.inner.as_ref()
444 }
445
446 pub fn downcast_ref<T: ResourceOutputsDefinition + 'static>(&self) -> Option<&T> {
448 self.inner.as_any().downcast_ref::<T>()
449 }
450}
451
452impl PartialEq for ResourceOutputs {
453 fn eq(&self, other: &Self) -> bool {
454 self.inner.outputs_eq(other.inner.as_ref())
455 }
456}
457
458impl Eq for ResourceOutputs {}
459
460#[cfg(feature = "openapi")]
479impl PartialSchema for ResourceOutputs {
480 fn schema() -> RefOr<Schema> {
481 RefOr::T(Schema::Object(
482 ObjectBuilder::new()
483 .schema_type(Type::Object)
484 .property("type", Ref::from_schema_name("ResourceType"))
485 .required("type")
486 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
487 .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."))
488 .build()
489 ))
490 }
491}
492
493#[cfg(feature = "openapi")]
494impl ToSchema for ResourceOutputs {
495 fn name() -> std::borrow::Cow<'static, str> {
496 std::borrow::Cow::Borrowed("BaseResourceOutputs")
497 }
498}
499
500#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
502#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
503#[serde(rename_all = "kebab-case")]
504pub enum ResourceStatus {
505 Pending, Provisioning, ProvisionFailed,
508 Running, Updating,
510 UpdateFailed,
511 Deleting, DeleteFailed,
513 Deleted, RefreshFailed, }
516
517impl ResourceStatus {
518 pub fn is_terminal(&self) -> bool {
519 match self {
520 ResourceStatus::Deleted => true,
521 ResourceStatus::ProvisionFailed => true,
522 ResourceStatus::UpdateFailed => true,
523 ResourceStatus::DeleteFailed => true,
524 ResourceStatus::RefreshFailed => true,
525 _ => false, }
527 }
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Hash, Deserialize)]
532#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
533#[serde(rename_all = "kebab-case")]
534pub enum ResourceLifecycle {
535 Frozen,
539
540 Live,
545
546 LiveOnSetup,
551}
552
553impl ResourceLifecycle {
554 pub fn is_live(&self) -> bool {
556 match self {
557 ResourceLifecycle::Frozen => false,
558 ResourceLifecycle::Live | ResourceLifecycle::LiveOnSetup => true,
559 }
560 }
561
562 pub fn initial_setup(&self) -> bool {
565 match self {
566 ResourceLifecycle::Frozen | ResourceLifecycle::LiveOnSetup => true,
567 ResourceLifecycle::Live => false,
568 }
569 }
570}