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 let mut settings = insta::Settings::clone_current();
270 settings.set_sort_maps(true);
271 settings.bind(|| {
272 assert_json_snapshot!("stack_serialization_account_managed", stack);
273 });
274 }
275
276 #[test]
277 fn test_empty_stack_serialization() {
278 let stack_builder = Stack::new("empty-test-stack".to_string());
279
280 let stack = stack_builder
281 .permissions(PermissionsConfig::new()) .build();
283
284 let serialized_stack =
286 serde_json::to_string_pretty(&stack).expect("Failed to serialize empty stack");
287 let deserialized_stack: Stack =
288 serde_json::from_str(&serialized_stack).expect("Failed to deserialize empty stack");
289
290 assert_eq!(
292 stack, deserialized_stack,
293 "Original and deserialized empty stacks do not match."
294 );
295
296 let mut settings = insta::Settings::clone_current();
298 settings.set_sort_maps(true);
299 settings.bind(|| {
300 assert_json_snapshot!("empty_stack_serialization_account", stack);
301 });
302 }
303
304 #[test]
305 fn test_stack_with_permissions() {
306 use crate::permissions::PermissionProfile;
307 use indexmap::IndexMap;
308
309 let storage = Storage::new("test-storage".to_string()).build();
311
312 let mut permission_profile = PermissionProfile::new();
314 permission_profile.0.insert(
315 "*".to_string(),
316 vec![PermissionSetReference::from_name("storage/data-read")],
317 );
318
319 let mut permissions = IndexMap::new();
320 permissions.insert("reader".to_string(), permission_profile);
321
322 let stack = Stack::new("test-permissions-stack".to_string())
323 .add(storage, ResourceLifecycle::Frozen)
324 .permissions(PermissionsConfig {
325 profiles: permissions,
326 management: ManagementPermissions::Auto,
327 })
328 .build();
329
330 assert_eq!(stack.permission_profiles().len(), 1);
332 assert!(stack.permission_profiles().contains_key("reader"));
333
334 let reader_profile = stack.permission_profiles().get("reader").unwrap();
335 assert_eq!(reader_profile.0.len(), 1);
336 assert!(reader_profile.0.contains_key("*"));
337
338 let global_permissions = reader_profile.0.get("*").unwrap();
339 assert_eq!(
340 global_permissions,
341 &vec![PermissionSetReference::from_name("storage/data-read")]
342 );
343
344 let serialized = serde_json::to_string_pretty(&stack).expect("Failed to serialize");
346 let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
347 assert_eq!(stack, deserialized);
348 }
349
350 #[test]
351 fn test_stack_with_management_permissions() {
352 use crate::permissions::{ManagementPermissions, PermissionProfile};
353
354 let storage = Storage::new("test-storage".to_string()).build();
356
357 let mut management_profile = PermissionProfile::new();
359 management_profile.0.insert(
360 "*".to_string(),
361 vec![PermissionSetReference::from_name("vault/data-write")],
362 );
363
364 let stack_auto = Stack::new("test-auto-management-stack".to_string())
366 .add(storage.clone(), ResourceLifecycle::Frozen)
367 .management(ManagementPermissions::auto())
368 .build();
369
370 assert!(stack_auto.management().is_auto());
371 assert!(stack_auto.management().profile().is_none());
372
373 let stack_extend = Stack::new("test-extend-management-stack".to_string())
375 .add(storage.clone(), ResourceLifecycle::Frozen)
376 .management(ManagementPermissions::extend(management_profile.clone()))
377 .build();
378
379 assert!(stack_extend.management().is_extend());
380 assert_eq!(
381 stack_extend.management().profile().unwrap(),
382 &management_profile
383 );
384
385 let stack_override = Stack::new("test-override-management-stack".to_string())
387 .add(storage.clone(), ResourceLifecycle::Frozen)
388 .management(ManagementPermissions::override_(management_profile.clone()))
389 .build();
390
391 assert!(stack_override.management().is_override());
392 assert_eq!(
393 stack_override.management().profile().unwrap(),
394 &management_profile
395 );
396
397 let stack_default = Stack::new("test-default-management-stack".to_string())
399 .add(storage, ResourceLifecycle::Frozen)
400 .build();
401
402 assert!(stack_default.management().is_auto());
403
404 let serialized = serde_json::to_string_pretty(&stack_extend).expect("Failed to serialize");
406 let deserialized: Stack = serde_json::from_str(&serialized).expect("Failed to deserialize");
407 assert_eq!(stack_extend, deserialized);
408 }
409}