1use crate::{
2 error::{ErrorData, Result},
3 resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef},
4 ResourceType,
5};
6use alien_error::AlienError;
7use bon::Builder;
8use serde::{Deserialize, Serialize};
9use std::{any::Any, fmt::Debug};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15pub struct LifecycleRule {
16 pub days: u32,
18 #[serde(default)]
20 pub prefix: Option<String>,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
26#[serde(rename_all = "camelCase")]
27pub enum StorageNotificationEvent {
28 Created,
30}
31
32fn default_notification_events() -> Vec<StorageNotificationEvent> {
33 vec![StorageNotificationEvent::Created]
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
40#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
41#[serde(rename_all = "camelCase", deny_unknown_fields)]
42pub struct StorageNotification {
43 pub queue: ResourceRef,
45 #[serde(default = "default_notification_events")]
47 #[builder(default = default_notification_events())]
48 pub events: Vec<StorageNotificationEvent>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub prefix: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub suffix: Option<String>,
55}
56
57impl StorageNotification {
58 pub fn matches_key(&self, key: &str) -> bool {
60 self.prefix
61 .as_ref()
62 .is_none_or(|prefix| key.starts_with(prefix))
63 && self
64 .suffix
65 .as_ref()
66 .is_none_or(|suffix| key.ends_with(suffix))
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
72#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
73#[serde(rename_all = "camelCase", deny_unknown_fields)]
74#[builder(start_fn = new)]
75pub struct Storage {
76 #[builder(start_fn)]
79 pub id: String,
80
81 #[serde(default)]
84 #[builder(default)]
85 pub public_read: bool,
86
87 #[serde(default)]
90 #[builder(default)]
91 pub versioning: bool,
92
93 #[serde(default)]
96 #[builder(default)]
97 pub lifecycle_rules: Vec<LifecycleRule>,
98
99 #[serde(default)]
106 #[builder(default)]
107 pub cors_allowed_origins: Vec<String>,
108
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 #[builder(default)]
113 #[cfg_attr(feature = "openapi", schema(max_items = 10))]
114 pub notifications: Vec<StorageNotification>,
115
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub encryption_key: Option<ResourceRef>,
119}
120
121impl Storage {
122 pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("storage");
124
125 pub fn id(&self) -> &str {
127 &self.id
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
134#[serde(rename_all = "camelCase")]
135pub struct StorageOutputs {
136 pub bucket_name: String,
138 }
140
141impl ResourceOutputsDefinition for StorageOutputs {
142 fn get_resource_type(&self) -> ResourceType {
143 Storage::RESOURCE_TYPE.clone()
144 }
145
146 fn as_any(&self) -> &dyn Any {
147 self
148 }
149
150 fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
151 Box::new(self.clone())
152 }
153
154 fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
155 other.as_any().downcast_ref::<StorageOutputs>() == Some(self)
156 }
157
158 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
159 serde_json::to_value(self)
160 }
161}
162
163impl ResourceDefinition for Storage {
165 fn get_resource_type(&self) -> ResourceType {
166 Self::RESOURCE_TYPE
167 }
168
169 fn id(&self) -> &str {
170 &self.id
171 }
172
173 fn replace_after_failed_create_is_safe(&self) -> bool {
174 false
176 }
177
178 fn get_dependencies(&self) -> Vec<ResourceRef> {
179 let mut dependencies: Vec<_> = self.encryption_key.iter().cloned().collect();
180 for notification in &self.notifications {
181 if !dependencies.contains(¬ification.queue) {
182 dependencies.push(notification.queue.clone());
183 }
184 }
185 dependencies
186 }
187
188 fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
189 let new_storage = new_config
191 .as_any()
192 .downcast_ref::<Storage>()
193 .ok_or_else(|| {
194 AlienError::new(ErrorData::UnexpectedResourceType {
195 resource_id: self.id.clone(),
196 expected: Self::RESOURCE_TYPE,
197 actual: new_config.get_resource_type(),
198 })
199 })?;
200
201 if self.id != new_storage.id {
202 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
203 resource_id: self.id.clone(),
204 reason: "the 'id' field is immutable".to_string(),
205 }));
206 }
207 if self.encryption_key != new_storage.encryption_key {
208 return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
209 resource_id: self.id.clone(),
210 reason: "the 'encryptionKey' field is immutable once the resource exists"
211 .to_string(),
212 }));
213 }
214 Ok(())
215 }
216
217 fn as_any(&self) -> &dyn Any {
218 self
219 }
220
221 fn as_any_mut(&mut self) -> &mut dyn Any {
222 self
223 }
224
225 fn box_clone(&self) -> Box<dyn ResourceDefinition> {
226 Box::new(self.clone())
227 }
228
229 fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
230 other.as_any().downcast_ref::<Storage>() == Some(self)
231 }
232
233 fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
234 serde_json::to_value(self)
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn encryption_key_is_an_implicit_dependency() {
244 let key = ResourceRef::new(crate::Key::RESOURCE_TYPE, "customer-key");
245 let storage = Storage::new("customer-data".to_string())
246 .encryption_key(key.clone())
247 .build();
248
249 assert_eq!(storage.get_dependencies(), vec![key]);
250 }
251
252 #[test]
253 fn notifications_preserve_existing_serialization_and_deduplicate_dependencies() {
254 let original: Storage = serde_json::from_value(serde_json::json!({
255 "id": "objects", "publicRead": false, "versioning": false,
256 "lifecycleRules": [], "corsAllowedOrigins": []
257 }))
258 .unwrap();
259 assert!(original.notifications.is_empty());
260 assert_eq!(
261 serde_json::to_value(&original).unwrap(),
262 serde_json::json!({
263 "id": "objects", "publicRead": false, "versioning": false,
264 "lifecycleRules": [], "corsAllowedOrigins": []
265 })
266 );
267 let key = ResourceRef::new(crate::Key::RESOURCE_TYPE, "key");
268 let queue = ResourceRef::new(crate::Queue::RESOURCE_TYPE, "work");
269 let rule = StorageNotification::builder().queue(queue.clone()).build();
270 let storage = Storage::new("objects".to_string())
271 .encryption_key(key.clone())
272 .notifications(vec![rule.clone(), rule])
273 .build();
274 assert_eq!(storage.get_dependencies(), vec![key, queue]);
275 let encoded = serde_json::to_value(&storage).unwrap();
276 assert_eq!(
277 encoded["notifications"][0]["events"],
278 serde_json::json!(["created"])
279 );
280 assert_eq!(serde_json::from_value::<Storage>(encoded).unwrap(), storage);
281 }
282
283 #[test]
284 fn notification_filters_are_literal_case_sensitive_and_combined() {
285 let rule = StorageNotification::builder()
286 .queue(ResourceRef::new(crate::Queue::RESOURCE_TYPE, "work"))
287 .prefix("in/*/é/".to_string())
288 .suffix(".json".to_string())
289 .build();
290 assert!(rule.matches_key("in/*/é/object.json"));
291 for key in [
292 "in/a/é/object.json",
293 "in/*/e/object.json",
294 "in/*/é/object.JSON",
295 "other/object.json",
296 "in/*/é/object.json.tmp",
297 ] {
298 assert!(!rule.matches_key(key), "{key}");
299 }
300 let mut unfiltered = rule.clone();
301 unfiltered.prefix = None;
302 unfiltered.suffix = None;
303 assert!(unfiltered.matches_key("anything"));
304 unfiltered.suffix = Some(".json".to_string());
305 assert!(unfiltered.matches_key("any/path.json"));
306 assert!(!unfiltered.matches_key("any/path.txt"));
307 unfiltered.prefix = Some("same".to_string());
308 unfiltered.suffix = Some("same".to_string());
309 assert!(unfiltered.matches_key("same"));
310 }
311
312 #[test]
313 fn notification_events_default_to_created_and_reject_unknown_events() {
314 let rule: StorageNotification = serde_json::from_value(serde_json::json!({
315 "queue": { "type": "queue", "id": "work" }
316 }))
317 .unwrap();
318 assert_eq!(rule.events, vec![StorageNotificationEvent::Created]);
319 assert!(
320 serde_json::from_value::<StorageNotification>(serde_json::json!({
321 "queue": { "type": "queue", "id": "work" }, "events": ["deleted"]
322 }))
323 .is_err()
324 );
325 }
326}