1use crate::permissions::{ManagementPermissions, PermissionProfile, PermissionsConfig};
2use crate::{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", deny_unknown_fields)]
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", deny_unknown_fields)]
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}
42
43impl Stack {
44 pub fn resources(&self) -> impl Iterator<Item = (&String, &ResourceEntry)> {
46 self.resources.iter()
47 }
48
49 pub fn resources_mut(&mut self) -> impl Iterator<Item = (&String, &mut ResourceEntry)> {
51 self.resources.iter_mut()
52 }
53
54 pub fn id(&self) -> &str {
55 &self.id
56 }
57
58 pub fn current() -> StackRef {
60 StackRef::Current
61 }
62
63 pub fn permissions(&self) -> &PermissionsConfig {
65 &self.permissions
66 }
67
68 pub fn permission_profiles(&self) -> &IndexMap<String, PermissionProfile> {
70 &self.permissions.profiles
71 }
72
73 pub fn management(&self) -> &ManagementPermissions {
75 &self.permissions.management
76 }
77}
78
79impl StackBuilder {
80 pub fn add<T: crate::ResourceDefinition>(
84 self,
85 resource: T,
86 lifecycle: ResourceLifecycle,
87 ) -> Self {
88 self.add_with_dependencies(resource, lifecycle, vec![])
89 }
90
91 pub fn add_with_dependencies<T: crate::ResourceDefinition>(
94 mut self,
95 resource: T,
96 lifecycle: ResourceLifecycle,
97 additional_dependencies: Vec<ResourceRef>,
98 ) -> Self {
99 let resource = Resource::new(resource);
100 self.resources.insert(
101 resource.id().to_string(),
102 ResourceEntry {
103 config: resource,
104 lifecycle,
105 dependencies: additional_dependencies,
106 remote_access: false,
107 },
108 );
109 self
110 }
111
112 pub fn add_with_remote_access<T: crate::ResourceDefinition>(
115 mut self,
116 resource: T,
117 lifecycle: ResourceLifecycle,
118 ) -> Self {
119 let resource = Resource::new(resource);
120 self.resources.insert(
121 resource.id().to_string(),
122 ResourceEntry {
123 config: resource,
124 lifecycle,
125 dependencies: vec![],
126 remote_access: true,
127 },
128 );
129 self
130 }
131
132 pub fn permissions(mut self, permissions: PermissionsConfig) -> Self {
135 self.permissions = permissions;
136 self
137 }
138
139 pub fn permission(mut self, name: impl Into<String>, profile: PermissionProfile) -> Self {
151 self.permissions.profiles.insert(name.into(), profile);
152 self
153 }
154
155 pub fn management(mut self, management: ManagementPermissions) -> Self {
181 self.permissions.management = management;
182 self
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
189#[serde(rename_all = "camelCase")]
190pub enum StackRef {
191 Current,
193 External(String),
195}
196
197impl StackRef {
198 pub fn from_stack(stack: &Stack) -> Self {
200 StackRef::External(stack.id().to_string())
201 }
202}
203
204impl From<&Stack> for StackRef {
205 fn from(stack: &Stack) -> Self {
206 StackRef::External(stack.id().to_string())
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::resource::ResourceLifecycle;
214 use crate::{Function, PermissionSetReference, Storage};
215 use insta::assert_json_snapshot;
216
217 #[test]
218 fn test_stack_serialization() {
219 use crate::FunctionCode;
220
221 let storage = Storage::new("my-bucket".to_string())
222 .public_read(true)
223 .build();
224
225 let function = Function::new("my-function".to_string())
226 .code(FunctionCode::Image {
227 image: "rust:latest".to_string(),
228 })
229 .permissions("execution".to_string())
230 .link(&storage)
231 .build();
232
233 let mut permissions = IndexMap::new();
235 let mut execution_profile = PermissionProfile::new();
236 execution_profile.0.insert(
237 "*".to_string(),
238 vec![
239 PermissionSetReference::from_name("storage/data-read"),
240 PermissionSetReference::from_name("storage/data-write"),
241 ],
242 );
243 permissions.insert("execution".to_string(), execution_profile);
244
245 let stack_builder = Stack::new("test-stack".to_string())
246 .add(storage, ResourceLifecycle::Frozen)
247 .add(function.clone(), ResourceLifecycle::Live);
248
249 let stack = stack_builder
250 .permissions(PermissionsConfig {
251 profiles: permissions,
252 management: ManagementPermissions::Auto,
253 })
254 .build();
255
256 let serialized_stack =
258 serde_json::to_string_pretty(&stack).expect("Failed to serialize stack");
259 let deserialized_stack: Stack =
260 serde_json::from_str(&serialized_stack).expect("Failed to deserialize stack");
261
262 assert_eq!(
264 stack, deserialized_stack,
265 "Original and deserialized stacks do not match."
266 );
267
268 assert_json_snapshot!("stack_serialization_account_managed", stack);
270 }
271
272 #[test]
273 fn test_empty_stack_serialization() {
274 let stack_builder = Stack::new("empty-test-stack".to_string());
275
276 let stack = stack_builder
277 .permissions(PermissionsConfig::new()) .build();
279
280 let serialized_stack =
282 serde_json::to_string_pretty(&stack).expect("Failed to serialize empty stack");
283 let deserialized_stack: Stack =
284 serde_json::from_str(&serialized_stack).expect("Failed to deserialize empty stack");
285
286 assert_eq!(
288 stack, deserialized_stack,
289 "Original and deserialized empty stacks do not match."
290 );
291
292 assert_json_snapshot!("empty_stack_serialization_account", stack);
294 }
295
296 #[test]
297 fn test_stack_with_permissions() {
298 use crate::permissions::PermissionProfile;
299 use indexmap::IndexMap;
300
301 let storage = Storage::new("test-storage".to_string()).build();
303
304 let mut permission_profile = PermissionProfile::new();
306 permission_profile.0.insert(
307 "*".to_string(),
308 vec![PermissionSetReference::from_name("storage/data-read")],
309 );
310
311 let mut permissions = IndexMap::new();
312 permissions.insert("reader".to_string(), permission_profile);
313
314 let stack = Stack::new("test-permissions-stack".to_string())
315 .add(storage, ResourceLifecycle::Frozen)
316 .permissions(PermissionsConfig {
317 profiles: permissions,
318 management: ManagementPermissions::Auto,
319 })
320 .build();
321
322 assert_eq!(stack.permission_profiles().len(), 1);
324 assert!(stack.permission_profiles().contains_key("reader"));
325
326 let reader_profile = stack.permission_profiles().get("reader").unwrap();
327 assert_eq!(reader_profile.0.len(), 1);
328 assert!(reader_profile.0.contains_key("*"));
329
330 let global_permissions = reader_profile.0.get("*").unwrap();
331 assert_eq!(
332 global_permissions,
333 &vec![PermissionSetReference::from_name("storage/data-read")]
334 );
335
336 let serialized = serde_json::to_string_pretty(&stack).expect("Failed to serialize");
338 let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
339 assert_eq!(stack, deserialized);
340 }
341
342 #[test]
343 fn test_stack_with_management_permissions() {
344 use crate::permissions::{ManagementPermissions, PermissionProfile};
345
346 let storage = Storage::new("test-storage".to_string()).build();
348
349 let mut management_profile = PermissionProfile::new();
351 management_profile.0.insert(
352 "*".to_string(),
353 vec![PermissionSetReference::from_name("vault/data-write")],
354 );
355
356 let stack_auto = Stack::new("test-auto-management-stack".to_string())
358 .add(storage.clone(), ResourceLifecycle::Frozen)
359 .management(ManagementPermissions::auto())
360 .build();
361
362 assert!(stack_auto.management().is_auto());
363 assert!(stack_auto.management().profile().is_none());
364
365 let stack_extend = Stack::new("test-extend-management-stack".to_string())
367 .add(storage.clone(), ResourceLifecycle::Frozen)
368 .management(ManagementPermissions::extend(management_profile.clone()))
369 .build();
370
371 assert!(stack_extend.management().is_extend());
372 assert_eq!(
373 stack_extend.management().profile().unwrap(),
374 &management_profile
375 );
376
377 let stack_override = Stack::new("test-override-management-stack".to_string())
379 .add(storage.clone(), ResourceLifecycle::Frozen)
380 .management(ManagementPermissions::override_(management_profile.clone()))
381 .build();
382
383 assert!(stack_override.management().is_override());
384 assert_eq!(
385 stack_override.management().profile().unwrap(),
386 &management_profile
387 );
388
389 let stack_default = Stack::new("test-default-management-stack".to_string())
391 .add(storage, ResourceLifecycle::Frozen)
392 .build();
393
394 assert!(stack_default.management().is_auto());
395
396 let serialized = serde_json::to_string_pretty(&stack_extend).expect("Failed to serialize");
398 let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
399 assert_eq!(stack_extend, deserialized);
400 }
401}