1use crate::permissions::{ManagementPermissions, PermissionProfile, PermissionsConfig};
2use crate::{Platform, Resource, ResourceLifecycle, ResourceRef};
3use bon::Builder;
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
9#[serde(rename_all = "camelCase")]
10pub struct ResourceEntry {
11 pub config: Resource,
13 pub lifecycle: ResourceLifecycle,
15 pub dependencies: Vec<ResourceRef>,
18 #[serde(default)]
22 pub remote_access: bool,
23}
24
25#[derive(Builder, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
28#[serde(rename_all = "camelCase")]
29#[builder(start_fn = new)]
30pub struct Stack {
31 #[builder(start_fn)]
33 pub id: String,
34 #[builder(field)]
36 pub resources: IndexMap<String, ResourceEntry>,
37 #[builder(field)]
39 #[serde(default)]
40 pub permissions: PermissionsConfig,
41 #[builder(field)]
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub supported_platforms: Option<Vec<Platform>>,
45}
46
47impl Stack {
48 pub fn resources(&self) -> impl Iterator<Item = (&String, &ResourceEntry)> {
50 self.resources.iter()
51 }
52
53 pub fn resources_mut(&mut self) -> impl Iterator<Item = (&String, &mut ResourceEntry)> {
55 self.resources.iter_mut()
56 }
57
58 pub fn id(&self) -> &str {
59 &self.id
60 }
61
62 pub fn current() -> StackRef {
64 StackRef::Current
65 }
66
67 pub fn permissions(&self) -> &PermissionsConfig {
69 &self.permissions
70 }
71
72 pub fn permission_profiles(&self) -> &IndexMap<String, PermissionProfile> {
74 &self.permissions.profiles
75 }
76
77 pub fn management(&self) -> &ManagementPermissions {
79 &self.permissions.management
80 }
81
82 pub fn supported_platforms(&self) -> Option<&[Platform]> {
84 self.supported_platforms.as_deref()
85 }
86
87 pub fn supports_platform(&self, platform: &Platform) -> bool {
90 match &self.supported_platforms {
91 Some(platforms) => platforms.contains(platform),
92 None => true,
93 }
94 }
95}
96
97impl StackBuilder {
98 pub fn add<T: crate::ResourceDefinition>(
102 self,
103 resource: T,
104 lifecycle: ResourceLifecycle,
105 ) -> Self {
106 self.add_with_dependencies(resource, lifecycle, vec![])
107 }
108
109 pub fn add_with_dependencies<T: crate::ResourceDefinition>(
112 mut self,
113 resource: T,
114 lifecycle: ResourceLifecycle,
115 additional_dependencies: Vec<ResourceRef>,
116 ) -> Self {
117 let resource = Resource::new(resource);
118 self.resources.insert(
119 resource.id().to_string(),
120 ResourceEntry {
121 config: resource,
122 lifecycle,
123 dependencies: additional_dependencies,
124 remote_access: false,
125 },
126 );
127 self
128 }
129
130 pub fn add_with_remote_access<T: crate::ResourceDefinition>(
133 mut self,
134 resource: T,
135 lifecycle: ResourceLifecycle,
136 ) -> Self {
137 let resource = Resource::new(resource);
138 self.resources.insert(
139 resource.id().to_string(),
140 ResourceEntry {
141 config: resource,
142 lifecycle,
143 dependencies: vec![],
144 remote_access: true,
145 },
146 );
147 self
148 }
149
150 pub fn permissions(mut self, permissions: PermissionsConfig) -> Self {
153 self.permissions = permissions;
154 self
155 }
156
157 pub fn permission(mut self, name: impl Into<String>, profile: PermissionProfile) -> Self {
169 self.permissions.profiles.insert(name.into(), profile);
170 self
171 }
172
173 pub fn platforms(mut self, platforms: Vec<Platform>) -> Self {
175 self.supported_platforms = Some(platforms);
176 self
177 }
178
179 pub fn management(mut self, management: ManagementPermissions) -> Self {
205 self.permissions.management = management;
206 self
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
213#[serde(rename_all = "camelCase")]
214pub enum StackRef {
215 Current,
217 External(String),
219}
220
221impl StackRef {
222 pub fn from_stack(stack: &Stack) -> Self {
224 StackRef::External(stack.id().to_string())
225 }
226}
227
228impl From<&Stack> for StackRef {
229 fn from(stack: &Stack) -> Self {
230 StackRef::External(stack.id().to_string())
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::resource::ResourceLifecycle;
238 use crate::{PermissionSetReference, Storage, Worker};
239 use insta::assert_json_snapshot;
240
241 #[test]
242 fn test_stack_serialization() {
243 use crate::WorkerCode;
244
245 let storage = Storage::new("my-bucket".to_string())
246 .public_read(true)
247 .build();
248
249 let worker = Worker::new("my-worker".to_string())
250 .code(WorkerCode::Image {
251 image: "rust:latest".to_string(),
252 })
253 .permissions("execution".to_string())
254 .link(&storage)
255 .build();
256
257 let mut permissions = IndexMap::new();
259 let mut execution_profile = PermissionProfile::new();
260 execution_profile.0.insert(
261 "*".to_string(),
262 vec![
263 PermissionSetReference::from_name("storage/data-read"),
264 PermissionSetReference::from_name("storage/data-write"),
265 ],
266 );
267 permissions.insert("execution".to_string(), execution_profile);
268
269 let stack_builder = Stack::new("test-stack".to_string())
270 .add(storage, ResourceLifecycle::Frozen)
271 .add(worker.clone(), ResourceLifecycle::Live);
272
273 let stack = stack_builder
274 .permissions(PermissionsConfig {
275 profiles: permissions,
276 management: ManagementPermissions::Auto,
277 })
278 .build();
279
280 let serialized_stack =
282 serde_json::to_string_pretty(&stack).expect("Failed to serialize stack");
283 let deserialized_stack: Stack =
284 serde_json::from_str(&serialized_stack).expect("Failed to deserialize stack");
285
286 assert_eq!(
288 stack, deserialized_stack,
289 "Original and deserialized stacks do not match."
290 );
291
292 let mut settings = insta::Settings::clone_current();
294 settings.set_sort_maps(true);
295 settings.bind(|| {
296 assert_json_snapshot!("stack_serialization_account_managed", stack);
297 });
298 }
299
300 #[test]
301 fn test_empty_stack_serialization() {
302 let stack_builder = Stack::new("empty-test-stack".to_string());
303
304 let stack = stack_builder
305 .permissions(PermissionsConfig::new()) .build();
307
308 let serialized_stack =
310 serde_json::to_string_pretty(&stack).expect("Failed to serialize empty stack");
311 let deserialized_stack: Stack =
312 serde_json::from_str(&serialized_stack).expect("Failed to deserialize empty stack");
313
314 assert_eq!(
316 stack, deserialized_stack,
317 "Original and deserialized empty stacks do not match."
318 );
319
320 let mut settings = insta::Settings::clone_current();
322 settings.set_sort_maps(true);
323 settings.bind(|| {
324 assert_json_snapshot!("empty_stack_serialization_account", stack);
325 });
326 }
327
328 #[test]
329 fn test_stack_with_permissions() {
330 use crate::permissions::PermissionProfile;
331 use indexmap::IndexMap;
332
333 let storage = Storage::new("test-storage".to_string()).build();
335
336 let mut permission_profile = PermissionProfile::new();
338 permission_profile.0.insert(
339 "*".to_string(),
340 vec![PermissionSetReference::from_name("storage/data-read")],
341 );
342
343 let mut permissions = IndexMap::new();
344 permissions.insert("reader".to_string(), permission_profile);
345
346 let stack = Stack::new("test-permissions-stack".to_string())
347 .add(storage, ResourceLifecycle::Frozen)
348 .permissions(PermissionsConfig {
349 profiles: permissions,
350 management: ManagementPermissions::Auto,
351 })
352 .build();
353
354 assert_eq!(stack.permission_profiles().len(), 1);
356 assert!(stack.permission_profiles().contains_key("reader"));
357
358 let reader_profile = stack.permission_profiles().get("reader").unwrap();
359 assert_eq!(reader_profile.0.len(), 1);
360 assert!(reader_profile.0.contains_key("*"));
361
362 let global_permissions = reader_profile.0.get("*").unwrap();
363 assert_eq!(
364 global_permissions,
365 &vec![PermissionSetReference::from_name("storage/data-read")]
366 );
367
368 let serialized = serde_json::to_string_pretty(&stack).expect("Failed to serialize");
370 let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
371 assert_eq!(stack, deserialized);
372 }
373
374 #[test]
375 fn test_stack_with_management_permissions() {
376 use crate::permissions::{ManagementPermissions, PermissionProfile};
377
378 let storage = Storage::new("test-storage".to_string()).build();
380
381 let mut management_profile = PermissionProfile::new();
383 management_profile.0.insert(
384 "*".to_string(),
385 vec![PermissionSetReference::from_name("vault/data-write")],
386 );
387
388 let stack_auto = Stack::new("test-auto-management-stack".to_string())
390 .add(storage.clone(), ResourceLifecycle::Frozen)
391 .management(ManagementPermissions::auto())
392 .build();
393
394 assert!(stack_auto.management().is_auto());
395 assert!(stack_auto.management().profile().is_none());
396
397 let stack_extend = Stack::new("test-extend-management-stack".to_string())
399 .add(storage.clone(), ResourceLifecycle::Frozen)
400 .management(ManagementPermissions::extend(management_profile.clone()))
401 .build();
402
403 assert!(stack_extend.management().is_extend());
404 assert_eq!(
405 stack_extend.management().profile().unwrap(),
406 &management_profile
407 );
408
409 let stack_override = Stack::new("test-override-management-stack".to_string())
411 .add(storage.clone(), ResourceLifecycle::Frozen)
412 .management(ManagementPermissions::override_(management_profile.clone()))
413 .build();
414
415 assert!(stack_override.management().is_override());
416 assert_eq!(
417 stack_override.management().profile().unwrap(),
418 &management_profile
419 );
420
421 let stack_default = Stack::new("test-default-management-stack".to_string())
423 .add(storage, ResourceLifecycle::Frozen)
424 .build();
425
426 assert!(stack_default.management().is_auto());
427
428 let serialized = serde_json::to_string_pretty(&stack_extend).expect("Failed to serialize");
430 let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
431 assert_eq!(stack_extend, deserialized);
432 }
433}