Skip to main content

rusty_cdk_core/s3/
builder.rs

1use crate::custom_resource::{BucketNotificationBuilder, BUCKET_NOTIFICATION_HANDLER_CODE};
2use crate::iam::{
3    CustomPermission, Effect, Permission, PolicyDocument, PolicyDocumentBuilder, PrincipalBuilder, Statement, StatementBuilder,
4};
5use crate::intrinsic::join;
6use crate::lambda::{Architecture, Runtime};
7use crate::lambda::{Code, FunctionBuilder, FunctionRef, PermissionBuilder};
8use crate::s3::{dto, AccelerateConfiguration, BucketPolicyType, BucketType, IntelligentTieringConfiguration, InventoryTableConfiguration, JournalTableConfiguration, MetadataConfiguration, MetadataDestination, RecordExpiration, TagFilter, Tiering};
9use crate::s3::{
10    Bucket, BucketEncryption, BucketPolicy, BucketPolicyRef, BucketProperties, BucketRef, CorsConfiguration, CorsRule,
11    LifecycleConfiguration, LifecycleRule, LifecycleRuleTransition, NonCurrentVersionTransition, PublicAccessBlockConfiguration,
12    RedirectAllRequestsTo, S3BucketPolicyProperties, ServerSideEncryptionByDefault, ServerSideEncryptionRule, WebsiteConfiguration,
13};
14use crate::shared::{DeletionPolicy, Id, UpdateDeletePolicyDTO, UpdateReplacePolicy, QUEUE_POLICY_ID_SUFFIX, TOPIC_POLICY_ID_SUFFIX};
15use crate::shared::{HttpMethod, Protocol};
16use crate::sns::{TopicPolicyBuilder, TopicRef};
17use crate::sqs::{QueuePolicyBuilder, QueueRef};
18use crate::stack::{Resource, StackBuilder};
19use crate::type_state;
20use crate::wrappers::{
21    BucketName, BucketTiering, IamAction, LambdaPermissionAction, LifecycleTransitionInDays, Memory, PolicyName, RecordExpirationDays,
22    S3LifecycleObjectSizes, Timeout,
23};
24use serde_json::{json, Value};
25use std::marker::PhantomData;
26use std::time::Duration;
27
28/// Builder for S3 bucket policies.
29///
30/// Creates a policy document that controls access to an S3 bucket.
31///
32/// # Example
33///
34/// ```rust,no_run
35/// use serde_json::Value;
36/// use rusty_cdk_core::stack::StackBuilder;
37/// use rusty_cdk_core::s3::BucketPolicyBuilder;
38/// use rusty_cdk_core::iam::{PolicyDocumentBuilder, StatementBuilder, Effect, PrincipalBuilder};
39/// use rusty_cdk_core::wrappers::*;
40/// use rusty_cdk_core::s3::BucketBuilder;
41/// use rusty_cdk_macros::iam_action;
42///
43/// let mut stack_builder = StackBuilder::new();
44/// let bucket = unimplemented!("create a bucket");
45///
46/// let resources = vec![Value::String("*".to_string())];
47/// let statement = StatementBuilder::new(
48///         vec![iam_action!("s3:GetObject")],
49///         Effect::Allow
50///     )
51///     .principal(PrincipalBuilder::new().normal("*").build())
52///     .resources(resources)
53///     .build();
54///
55/// let policy_doc = PolicyDocumentBuilder::new(vec![statement]).build();
56/// let policy = BucketPolicyBuilder::new("bucket-policy", &bucket, policy_doc)
57///     .build(&mut stack_builder);
58/// ```
59pub struct BucketPolicyBuilder {
60    id: Id,
61    bucket_name: Value,
62    policy_document: PolicyDocument,
63}
64
65impl BucketPolicyBuilder {
66    /// Creates a new S3 bucket policy builder.
67    ///
68    /// # Arguments
69    /// * `id` - Unique identifier for the bucket policy
70    /// * `bucket` - Reference to the S3 bucket
71    /// * `policy_document` - IAM policy document controlling access
72    pub fn new(id: &str, bucket: &BucketRef, policy_document: PolicyDocument) -> Self {
73        Self {
74            id: Id(id.to_string()),
75            bucket_name: bucket.get_ref(),
76            policy_document,
77        }
78    }
79
80    pub(crate) fn new_with_bucket_ref(id: &str, bucket_name: Value, policy_document: PolicyDocument) -> Self {
81        Self {
82            id: Id(id.to_string()),
83            bucket_name,
84            policy_document,
85        }
86    }
87
88    pub(crate) fn raw_build(self) -> (String, BucketPolicy) {
89        let resource_id = Resource::generate_id("S3BucketPolicy");
90        let policy = BucketPolicy {
91            id: self.id,
92            resource_id: resource_id.to_string(),
93            r#type: BucketPolicyType::BucketPolicyType,
94            properties: S3BucketPolicyProperties {
95                bucket_name: self.bucket_name,
96                policy_document: self.policy_document,
97            },
98        };
99        (resource_id, policy)
100    }
101
102    pub fn build(self, stack_builder: &mut StackBuilder) -> BucketPolicyRef {
103        let (resource_id, policy) = self.raw_build();
104        stack_builder.add_resource(policy);
105        BucketPolicyRef::internal_new(resource_id)
106    }
107}
108
109#[derive(Debug, Clone)]
110pub enum VersioningConfiguration {
111    Enabled,
112    Suspended,
113}
114
115impl From<VersioningConfiguration> for String {
116    fn from(value: VersioningConfiguration) -> Self {
117        match value {
118            VersioningConfiguration::Enabled => "Enabled".to_string(),
119            VersioningConfiguration::Suspended => "Suspended".to_string(),
120        }
121    }
122}
123
124#[derive(Debug, Clone)]
125pub enum Encryption {
126    S3Managed,
127    KmsManaged,
128    DsseManaged,
129    // KMS, => add, this requires creating a kms key and passing it to the bucket
130    // DSSE, => add, similar
131}
132
133impl From<Encryption> for String {
134    fn from(value: Encryption) -> Self {
135        match value {
136            Encryption::S3Managed => "AES256".to_string(),
137            Encryption::KmsManaged => "aws:kms".to_string(),
138            Encryption::DsseManaged => "aws:kms:dsse".to_string(),
139        }
140    }
141}
142
143pub enum NotificationDestination<'a> {
144    Lambda(&'a FunctionRef, NotificationEventType),
145    Sns(&'a TopicRef, NotificationEventType),
146    Sqs(&'a QueueRef, NotificationEventType),
147}
148
149#[derive(Debug, Clone)]
150pub enum NotificationEventType {
151    ObjectCreated,
152    ObjectCreatedPut,
153    ObjectCreatedPost,
154    ObjectCreatedCopy,
155    ObjectCreatedCompleteMultipartUpload,
156    ObjectRemoved,
157    ObjectRemovedDelete,
158    ObjectRemovedDeleteMarkerCreated,
159    ObjectRestorePost,
160    ObjectRestoreCompleted,
161    ObjectRestoreDelete,
162    ReducedRedundancyLostObject,
163    ReplicationOperationFailedReplication,
164    ReplicationOperationMissedThreshold,
165    ReplicationOperationReplicatedAfterThreshold,
166    ReplicationOperationNotTracked,
167    LifecycleExpiration,
168    LifecycleExpirationDelete,
169    LifecycleExpirationDeleteMarkerCreated,
170    LifecycleTransition,
171    IntelligentTiering,
172    ObjectTagging,
173    ObjectTaggingPut,
174    ObjectTaggingDelete,
175    ObjectAclPut,
176    ObjectRestore,
177    REPLICATION,
178}
179
180#[derive(Debug, Clone)]
181pub enum AbacStatus {
182    Enabled,
183    Disabled,
184}
185
186impl From<AbacStatus> for String {
187    fn from(value: AbacStatus) -> Self {
188        match value {
189            AbacStatus::Enabled => "Enabled".to_string(),
190            AbacStatus::Disabled => "Disabled".to_string(),
191        }
192    }
193}
194#[derive(Debug, Clone)]
195pub enum AccelerationStatus {
196    Enabled,
197    Suspended,
198}
199
200impl From<AccelerationStatus> for String {
201    fn from(value: AccelerationStatus) -> Self {
202        match value {
203            AccelerationStatus::Enabled => "Enabled".to_string(),
204            AccelerationStatus::Suspended => "Suspended".to_string(),
205        }
206    }
207}
208
209impl From<NotificationEventType> for String {
210    fn from(value: NotificationEventType) -> Self {
211        match value {
212            NotificationEventType::ObjectCreated => "s3:ObjectCreated:*".to_string(),
213            NotificationEventType::ObjectCreatedPut => "s3:ObjectCreated:Put".to_string(),
214            NotificationEventType::ObjectCreatedPost => "s3:ObjectCreated:Post".to_string(),
215            NotificationEventType::ObjectCreatedCopy => "s3:ObjectCreated:Copy".to_string(),
216            NotificationEventType::ObjectCreatedCompleteMultipartUpload => "s3:ObjectCreated:CompleteMultipartUpload".to_string(),
217            NotificationEventType::ObjectRemoved => "s3:ObjectRemoved:*".to_string(),
218            NotificationEventType::ObjectRemovedDelete => "s3:ObjectRemoved:Delete".to_string(),
219            NotificationEventType::ObjectRemovedDeleteMarkerCreated => "s3:ObjectRemoved:DeleteMarkerCreated".to_string(),
220            NotificationEventType::ObjectRestorePost => "s3:ObjectRestore:Post".to_string(),
221            NotificationEventType::ObjectRestoreCompleted => "s3:ObjectRestore:Completed".to_string(),
222            NotificationEventType::ObjectRestoreDelete => "s3:ObjectRestore:Delete".to_string(),
223            NotificationEventType::ReducedRedundancyLostObject => "s3:ReducedRedundancyLostObject".to_string(),
224            NotificationEventType::ReplicationOperationFailedReplication => "s3:Replication:OperationFailedReplication".to_string(),
225            NotificationEventType::ReplicationOperationMissedThreshold => "s3:Replication:OperationMissedThreshold".to_string(),
226            NotificationEventType::ReplicationOperationReplicatedAfterThreshold => {
227                "s3:Replication:OperationReplicatedAfterThreshold".to_string()
228            }
229            NotificationEventType::ReplicationOperationNotTracked => "s3:Replication:OperationNotTracked".to_string(),
230            NotificationEventType::LifecycleExpiration => "s3:LifecycleExpiration:*".to_string(),
231            NotificationEventType::LifecycleExpirationDelete => "s3:LifecycleExpiration:Delete".to_string(),
232            NotificationEventType::LifecycleExpirationDeleteMarkerCreated => "s3:LifecycleExpiration:DeleteMarkerCreated".to_string(),
233            NotificationEventType::LifecycleTransition => "s3:LifecycleTransition".to_string(),
234            NotificationEventType::IntelligentTiering => "s3:IntelligentTiering".to_string(),
235            NotificationEventType::ObjectTagging => "s3:ObjectTagging:*".to_string(),
236            NotificationEventType::ObjectTaggingPut => "s3:ObjectTagging:Put".to_string(),
237            NotificationEventType::ObjectTaggingDelete => "s3:ObjectTagging:Delete".to_string(),
238            NotificationEventType::ObjectAclPut => "s3:ObjectAcl:Put".to_string(),
239            NotificationEventType::ObjectRestore => "s3:ObjectRestore:*".to_string(),
240            NotificationEventType::REPLICATION => "s3:Replication:*".to_string(),
241        }
242    }
243}
244
245type_state!(BucketBuilderState, StartState, WebsiteState,);
246
247/// Builder for S3 buckets.
248///
249/// Provides configuration for S3 buckets including versioning, lifecycle rules, encryption, CORS, and static website hosting.
250///
251/// # Example
252///
253/// ```rust,compile_fail
254/// use rusty_cdk_core::stack::StackBuilder;
255/// use rusty_cdk_core::s3::{BucketBuilder, VersioningConfig, Encryption, VersioningConfiguration};
256/// use rusty_cdk_core::wrappers::*;
257/// use rusty_cdk_macros::bucket_name;
258///
259/// let mut stack_builder = StackBuilder::new();
260///
261/// // Create a simple bucket
262/// let bucket = BucketBuilder::new("my-bucket")
263///     .name(bucket_name!("my-unique-bucket"))
264///     .versioning_configuration(VersioningConfiguration::Enabled)
265///     .encryption(Encryption::S3Managed)
266///     .build(&mut stack_builder);
267///
268/// // Create a website bucket
269/// let (website_bucket, policy) = BucketBuilder::new("website-bucket")
270///     .website("index.html")
271///     .error_document("error.html")
272///     .build(&mut stack_builder);
273/// ```
274pub struct BucketBuilder<T: BucketBuilderState> {
275    phantom_data: PhantomData<T>,
276    id: Id,
277    abac_status: Option<String>,
278    acceleration_status: Option<String>,
279    name: Option<String>,
280    access: Option<PublicAccessBlockConfiguration>,
281    intelligent_tiering_configurations: Option<Vec<IntelligentTieringConfiguration>>,
282    metadata_configuration: Option<MetadataConfiguration>,
283    versioning_configuration: Option<VersioningConfiguration>,
284    lifecycle_configuration: Option<LifecycleConfiguration>,
285    index_document: Option<String>,
286    error_document: Option<String>,
287    redirect_all_requests_to: Option<(String, Option<Protocol>)>,
288    cors_config: Option<CorsConfiguration>,
289    bucket_encryption: Option<Encryption>,
290    bucket_notification_lambda_destinations: Vec<(Value, String)>,
291    bucket_notification_sns_destinations: Vec<(Id, Value, String)>,
292    bucket_notification_sqs_destinations: Vec<(Id, Value, Value, String)>,
293    deletion_policy: Option<String>,
294    update_replace_policy: Option<String>,
295    additional_website_policy_statements: Option<Vec<Statement>>,
296}
297
298impl BucketBuilder<StartState> {
299    /// Creates a new S3 bucket builder.
300    ///
301    /// # Arguments
302    /// * `id` - Unique identifier for the bucket
303    pub fn new(id: &str) -> Self {
304        Self {
305            id: Id(id.to_string()),
306            phantom_data: Default::default(),
307            abac_status: None,
308            acceleration_status: None,
309            name: None,
310            access: None,
311            intelligent_tiering_configurations: None,
312            metadata_configuration: None,
313            versioning_configuration: None,
314            lifecycle_configuration: None,
315            index_document: None,
316            error_document: None,
317            redirect_all_requests_to: None,
318            cors_config: None,
319            bucket_encryption: None,
320            bucket_notification_lambda_destinations: vec![],
321            bucket_notification_sns_destinations: vec![],
322            bucket_notification_sqs_destinations: vec![],
323            deletion_policy: None,
324            update_replace_policy: None,
325            additional_website_policy_statements: None,
326        }
327    }
328
329    pub fn build(self, stack_builder: &mut StackBuilder) -> BucketRef {
330        let (bucket, _) = self.build_internal(false, stack_builder);
331        bucket
332    }
333}
334
335impl<T: BucketBuilderState> BucketBuilder<T> {
336    /// Sets the name of the bucket.
337    ///
338    /// The bucket name must be globally unique.
339    pub fn name(self, name: BucketName) -> Self {
340        Self {
341            name: Some(name.0),
342            ..self
343        }
344    }
345
346    /// Enables or disables attribute-based access control (ABAC) for the bucket.
347    pub fn abac_status(self, abac_status: AbacStatus) -> Self {
348        Self {
349            abac_status: Some(abac_status.into()),
350            ..self
351        }
352    }
353
354    /// Enables or disables S3 Transfer Acceleration for the bucket.
355    pub fn acceleration_status(self, acceleration_status: AccelerationStatus) -> Self {
356        Self {
357            acceleration_status: Some(acceleration_status.into()),
358            ..self
359        }
360    }
361
362    /// Configures metadata for the bucket.
363    pub fn metadata_configuration(self, config: MetadataConfiguration) -> Self {
364        Self {
365            metadata_configuration: Some(config),
366            ..self
367        }
368    }
369
370    /// Configures versioning for the bucket.
371    ///
372    /// Once enabled, versioning cannot be disabled, only suspended.
373    pub fn versioning_configuration(self, config: VersioningConfiguration) -> Self {
374        Self {
375            versioning_configuration: Some(config),
376            ..self
377        }
378    }
379
380    /// Configures lifecycle rules for the bucket.
381    pub fn lifecycle_configuration(self, config: LifecycleConfiguration) -> Self {
382        Self {
383            lifecycle_configuration: Some(config),
384            ..self
385        }
386    }
387
388    /// Configures the public access block for the bucket.
389    pub fn public_access_block_configuration(self, access: PublicAccessBlockConfiguration) -> Self {
390        Self {
391            access: Some(access),
392            ..self
393        }
394    }
395
396    /// Configures server-side encryption for the bucket.
397    pub fn encryption(self, encryption: Encryption) -> Self {
398        Self {
399            bucket_encryption: Some(encryption),
400            ..self
401        }
402    }
403
404    /// Sets the update replace policy and deletion policy for the bucket
405    pub fn update_replace_and_deletion_policy(self, update_replace_policy: UpdateReplacePolicy, deletion_policy: DeletionPolicy) -> Self {
406        Self {
407            deletion_policy: Some(deletion_policy.into()),
408            update_replace_policy: Some(update_replace_policy.into()),
409            ..self
410        }
411    }
412
413    /// Adds an intelligent tiering configuration to the S3 bucket
414    pub fn add_intelligent_tiering(mut self, tiering: IntelligentTieringConfiguration) -> Self {
415        if let Some(mut config) = self.intelligent_tiering_configurations {
416            config.push(tiering);
417            self.intelligent_tiering_configurations = Some(config);
418        } else {
419            self.intelligent_tiering_configurations = Some(vec![tiering]);
420        }
421        self
422    }
423
424    /// Adds a notification destination to the S3 bucket
425    pub fn add_notification(mut self, destination: NotificationDestination) -> Self {
426        match destination {
427            NotificationDestination::Lambda(l, e) => self.bucket_notification_lambda_destinations.push((l.get_arn(), e.into())),
428            NotificationDestination::Sns(s, e) => {
429                self.bucket_notification_sns_destinations
430                    .push((s.get_id().clone(), s.get_ref(), e.into()))
431            }
432            NotificationDestination::Sqs(q, e) => {
433                self.bucket_notification_sqs_destinations
434                    .push((q.get_id().clone(), q.get_ref(), q.get_arn(), e.into()))
435            }
436        }
437        self
438    }
439
440    /// Configures the bucket for static website hosting.
441    ///
442    /// Automatically disables public access blocks and creates a bucket policy
443    /// allowing public GetObject access.
444    pub fn website<I: Into<String>>(self, index_document: I) -> BucketBuilder<WebsiteState> {
445        BucketBuilder {
446            phantom_data: Default::default(),
447            id: self.id,
448            abac_status: self.abac_status,
449            acceleration_status: self.acceleration_status,
450            name: self.name,
451            access: self.access,
452            intelligent_tiering_configurations: self.intelligent_tiering_configurations,
453            metadata_configuration: self.metadata_configuration,
454            versioning_configuration: self.versioning_configuration,
455            lifecycle_configuration: self.lifecycle_configuration,
456            index_document: Some(index_document.into()),
457            error_document: self.error_document,
458            redirect_all_requests_to: self.redirect_all_requests_to,
459            cors_config: self.cors_config,
460            bucket_encryption: self.bucket_encryption,
461            bucket_notification_lambda_destinations: self.bucket_notification_lambda_destinations,
462            bucket_notification_sns_destinations: self.bucket_notification_sns_destinations,
463            bucket_notification_sqs_destinations: self.bucket_notification_sqs_destinations,
464            deletion_policy: self.deletion_policy,
465            update_replace_policy: self.update_replace_policy,
466            additional_website_policy_statements: self.additional_website_policy_statements,
467        }
468    }
469
470    fn build_internal(self, website: bool, stack_builder: &mut StackBuilder) -> (BucketRef, Option<BucketPolicyRef>) {
471        let resource_id = Resource::generate_id("S3Bucket");
472
473        let versioning_configuration = self.versioning_configuration.map(|c| dto::VersioningConfig { status: c.into() });
474
475        let website_configuration = if website {
476            let redirect_all_requests_to = self.redirect_all_requests_to.map(|r| RedirectAllRequestsTo {
477                host_name: r.0,
478                protocol: r.1.map(Into::into),
479            });
480
481            Some(WebsiteConfiguration {
482                index_document: self.index_document,
483                error_document: self.error_document,
484                redirect_all_requests_to,
485            })
486        } else {
487            None
488        };
489
490        let access = if self.access.is_none() && website {
491            // required for an S3 website
492            Some(PublicAccessBlockConfiguration {
493                block_public_policy: Some(false),
494                restrict_public_buckets: Some(false),
495                block_public_acls: Some(true),
496                ignore_public_acls: Some(true),
497            })
498        } else {
499            self.access
500        };
501
502        let encryption = self.bucket_encryption.map(|v| {
503            let rule = ServerSideEncryptionRule {
504                server_side_encryption_by_default: ServerSideEncryptionByDefault {
505                    sse_algorithm: v.into(),
506                    kms_master_key_id: None,
507                },
508                bucket_key_enabled: None,
509            };
510
511            BucketEncryption {
512                server_side_encryption_configuration: vec![rule],
513            }
514        });
515
516        let properties = BucketProperties {
517            abac_status: self.abac_status,
518            accelerate_configuration: self.acceleration_status.map(|v| AccelerateConfiguration { acceleration_status: v }),
519            bucket_name: self.name,
520            cors_configuration: self.cors_config,
521            intelligent_tiering_configurations: self.intelligent_tiering_configurations,
522            lifecycle_configuration: self.lifecycle_configuration,
523            public_access_block_configuration: access,
524            versioning_configuration,
525            website_configuration,
526            bucket_encryption: encryption,
527            metadata_configuration: self.metadata_configuration,
528        };
529
530        stack_builder.add_resource(Bucket {
531            id: self.id.clone(),
532            resource_id: resource_id.clone(),
533            r#type: BucketType::BucketType,
534            properties,
535            update_delete_policy_dto: UpdateDeletePolicyDTO {
536                deletion_policy: self.deletion_policy,
537                update_replace_policy: self.update_replace_policy,
538            },
539        });
540
541        let bucket = BucketRef::internal_new(resource_id);
542
543        let policy = if website {
544            // website needs a policy to allow GETs
545            let bucket_resource = vec![join("", vec![bucket.get_arn(), Value::String("/*".to_string())])];
546            let statement = StatementBuilder::new(vec![IamAction("s3:GetObject".to_string())], Effect::Allow)
547                .resources(bucket_resource)
548                .principal(PrincipalBuilder::new().normal("*").build())
549                .build();
550            let mut statements = vec![statement];
551
552            if let Some(additional) = self.additional_website_policy_statements {
553                statements.extend(additional);
554            }
555
556            let policy_doc = PolicyDocumentBuilder::new(statements).build();
557            let bucket_policy_id = Id::generate_id(&self.id, "S3Policy");
558            let s3_policy = BucketPolicyBuilder::new(&bucket_policy_id, &bucket, policy_doc).build(stack_builder);
559            Some(s3_policy)
560        } else {
561            None
562        };
563
564        for (i, (arn, event)) in self.bucket_notification_lambda_destinations.into_iter().enumerate() {
565            let permission_id = Id::generate_id(&self.id, format!("LambdaDestPerm{}", i).as_str());
566            let permission = PermissionBuilder::new(
567                &permission_id,
568                LambdaPermissionAction("lambda:InvokeFunction".to_string()),
569                arn.clone(),
570                "s3.amazonaws.com",
571            )
572            .source_arn(bucket.get_arn())
573            .current_account()
574            .build(stack_builder);
575            let handler = Self::notification_handler(&self.id, "Lambda", i, stack_builder);
576            let notification_id = Id::generate_id(&self.id, &format!("LambdaNotification{}", i));
577            BucketNotificationBuilder::new(
578                &notification_id,
579                handler.get_arn(),
580                bucket.get_ref(),
581                event,
582                Some(permission.get_id().clone()),
583            )
584            .lambda(arn)
585            .build(stack_builder);
586        }
587
588        for (i, (id, reference, event)) in self.bucket_notification_sns_destinations.into_iter().enumerate() {
589            let handler = Self::notification_handler(&self.id, "SNS", i, stack_builder);
590
591            let bucket_arn = bucket.get_arn();
592            let condition = json!({
593                "ArnLike": {
594                    "aws:SourceArn": bucket_arn
595                }
596            });
597            let principal = PrincipalBuilder::new().service("s3.amazonaws.com".to_string()).build();
598            let statement = StatementBuilder::new(vec![IamAction("sns:Publish".to_string())], Effect::Allow)
599                .principal(principal)
600                .condition(condition)
601                .resources(vec![reference.clone()])
602                .build();
603
604            let topic_policy_id = Id::generate_id(&id, TOPIC_POLICY_ID_SUFFIX);
605            let topic_policy_ref_id = match stack_builder.get_resource(&topic_policy_id) {
606                None => {
607                    // there's no queue policy. add ours
608                    let doc = PolicyDocumentBuilder::new(vec![statement]).build();
609                    let topic_policy_id = Id::generate_id(&self.id, &format!("SNSDestinationPolicy{}", i));
610                    TopicPolicyBuilder::new_with_values(topic_policy_id.clone(), doc, vec![reference.clone()]).build(stack_builder);
611                    topic_policy_id
612                }
613                Some(Resource::TopicPolicy(pol)) => {
614                    // there's a policy, add the required permissions
615                    pol.properties.doc.statements.push(statement);
616                    topic_policy_id
617                }
618                _ => unreachable!("topic policy id should point to optional topic policy"),
619            };
620
621            let notification_id = Id::generate_id(&self.id, &format!("SNSNotification{}", i));
622            BucketNotificationBuilder::new(
623                &notification_id,
624                handler.get_arn(),
625                bucket.get_ref(),
626                event,
627                Some(topic_policy_ref_id),
628            )
629            .sns(reference)
630            .build(stack_builder);
631        }
632
633        for (i, (id, reference, arn, event)) in self.bucket_notification_sqs_destinations.into_iter().enumerate() {
634            let handler = Self::notification_handler(&self.id, "SQS", i, stack_builder);
635
636            let bucket_arn = bucket.get_arn();
637            let condition = json!({
638                "ArnLike": {
639                    "aws:SourceArn": bucket_arn
640                }
641            });
642            let principal = PrincipalBuilder::new().service("s3.amazonaws.com".to_string()).build();
643            let statement = StatementBuilder::new(
644                vec![
645                    IamAction("sqs:GetQueueAttributes".to_string()),
646                    IamAction("sqs:GetQueueUrl".to_string()),
647                    IamAction("sqs:SendMessage".to_string()),
648                ],
649                Effect::Allow,
650            )
651            .principal(principal)
652            .condition(condition)
653            .resources(vec![arn.clone()])
654            .build();
655
656            let queue_policy_id = Id::generate_id(&id, QUEUE_POLICY_ID_SUFFIX);
657
658            let queue_policy_ref_id = match stack_builder.get_resource(&queue_policy_id) {
659                None => {
660                    // there's no queue policy. add ours
661                    let doc = PolicyDocumentBuilder::new(vec![statement]).build();
662                    let queue_policy_id = Id::generate_id(&self.id, &format!("SQSDestinationPolicy{}", i));
663                    QueuePolicyBuilder::new_with_values(queue_policy_id.clone(), doc, vec![reference.clone()]).build(stack_builder);
664                    queue_policy_id
665                }
666                Some(Resource::QueuePolicy(pol)) => {
667                    // there's a policy, add the required permissions
668                    pol.properties.doc.statements.push(statement);
669                    queue_policy_id
670                }
671                _ => unreachable!("queue policy id should point to optional queue policy"),
672            };
673
674            let notification_id = Id::generate_id(&self.id, format!("SQSNotification{}", i).as_str());
675            BucketNotificationBuilder::new(
676                &notification_id,
677                handler.get_arn(),
678                bucket.get_ref(),
679                event,
680                Some(queue_policy_ref_id),
681            )
682            .sqs(arn)
683            .build(stack_builder);
684        }
685
686        (bucket, policy)
687    }
688
689    fn notification_handler(id: &Id, target: &str, num: usize, stack_builder: &mut StackBuilder) -> FunctionRef {
690        let handler_id = Id::generate_id(id, &format!("{}Handler{}", target, num));
691        let (handler, ..) = FunctionBuilder::new(&handler_id, Architecture::X86_64, Memory(128), Timeout(300))
692            .code(Code::Inline(BUCKET_NOTIFICATION_HANDLER_CODE.to_string()))
693            .handler("index.handler")
694            .runtime(Runtime::Python313)
695            .add_permission(Permission::Custom(CustomPermission::new(
696                PolicyName("NotificationPermission".to_string()),
697                StatementBuilder::new(vec![IamAction("s3:PutBucketNotification".to_string())], Effect::Allow)
698                    .all_resources()
699                    .build(),
700            )))
701            .build(stack_builder);
702        handler
703    }
704}
705
706impl BucketBuilder<WebsiteState> {
707    pub fn error_document<I: Into<String>>(self, error: I) -> Self {
708        Self {
709            error_document: Some(error.into()),
710            ..self
711        }
712    }
713
714    pub fn redirect_all<I: Into<String>>(self, hostname: I, protocol: Option<Protocol>) -> Self {
715        Self {
716            redirect_all_requests_to: Some((hostname.into(), protocol)),
717            ..self
718        }
719    }
720
721    pub fn cors_config(self, config: CorsConfiguration) -> Self {
722        Self {
723            cors_config: Some(config),
724            ..self
725        }
726    }
727
728    /// Additional statements that will be added to the bucket policy.
729    ///
730    /// The bucket policy will by default allow GETs from anywhere.
731    pub fn custom_bucket_policy_statements(self, statements: Vec<Statement>) -> Self {
732        Self {
733            additional_website_policy_statements: Some(statements),
734            ..self
735        }
736    }
737
738    /// Builds the website bucket and adds it to the stack.
739    ///
740    /// Returns both the bucket and the automatically created bucket policy that allows public read access.
741    pub fn build(self, stack_builder: &mut StackBuilder) -> (BucketRef, BucketPolicyRef) {
742        let (bucket, policy) = self.build_internal(true, stack_builder);
743        (bucket, policy.expect("for website, bucket policy should always be present"))
744    }
745}
746
747/// Builder for S3 CORS configuration 
748/// 
749/// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-corsconfiguration.html
750pub struct CorsConfigurationBuilder {
751    rules: Vec<CorsRule>,
752}
753
754impl CorsConfigurationBuilder {
755    pub fn new(rules: Vec<CorsRule>) -> CorsConfigurationBuilder {
756        CorsConfigurationBuilder { rules }
757    }
758
759    pub fn build(self) -> CorsConfiguration {
760        CorsConfiguration { cors_rules: self.rules }
761    }
762}
763
764/// Builder for individual CORS rules.
765/// 
766/// See https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-s3-bucket-corsrule.html
767pub struct CorsRuleBuilder {
768    allow_origins: Vec<String>,
769    allow_methods: Vec<HttpMethod>,
770    allow_headers: Option<Vec<String>>,
771    expose_headers: Option<Vec<String>>,
772    max_age: Option<u64>,
773}
774
775impl CorsRuleBuilder {
776    pub fn new<T: Into<String>>(allow_origins: Vec<T>, allow_methods: Vec<HttpMethod>) -> Self {
777        Self {
778            allow_origins: allow_origins.into_iter().map(Into::into).collect(),
779            allow_methods,
780            allow_headers: None,
781            expose_headers: None,
782            max_age: None,
783        }
784    }
785
786    pub fn allow_headers(self, headers: Vec<String>) -> Self {
787        Self {
788            allow_headers: Some(headers),
789            ..self
790        }
791    }
792
793    pub fn expose_headers(self, headers: Vec<String>) -> Self {
794        Self {
795            expose_headers: Some(headers),
796            ..self
797        }
798    }
799
800    pub fn max_age(self, age: Duration) -> Self {
801        Self {
802            max_age: Some(age.as_secs()),
803            ..self
804        }
805    }
806
807    #[must_use]
808    pub fn build(self) -> CorsRule {
809        CorsRule {
810            allowed_headers: self.allow_headers,
811            allowed_methods: self.allow_methods.into_iter().map(Into::into).collect(),
812            allowed_origins: self.allow_origins,
813            exposed_headers: self.expose_headers,
814            max_age: self.max_age,
815        }
816    }
817}
818
819#[derive(Debug, Clone)]
820pub enum TransitionDefaultMinimumObjectSize {
821    VariesByStorageClass,
822    AllStorageClasses128k,
823}
824
825impl From<TransitionDefaultMinimumObjectSize> for String {
826    fn from(value: TransitionDefaultMinimumObjectSize) -> Self {
827        match value {
828            TransitionDefaultMinimumObjectSize::VariesByStorageClass => "varies_by_storage_class".to_string(),
829            TransitionDefaultMinimumObjectSize::AllStorageClasses128k => "all_storage_classes_128K".to_string(),
830        }
831    }
832}
833
834#[derive(Debug, Clone)]
835pub enum LifecycleStorageClass {
836    IntelligentTiering,
837    OneZoneIA,
838    StandardIA,
839    GlacierDeepArchive,
840    Glacier,
841    GlacierInstantRetrieval,
842}
843
844impl From<LifecycleStorageClass> for String {
845    fn from(value: LifecycleStorageClass) -> Self {
846        match value {
847            LifecycleStorageClass::GlacierDeepArchive => "DEEP_ARCHIVE".to_string(),
848            LifecycleStorageClass::Glacier => "GLACIER".to_string(),
849            LifecycleStorageClass::GlacierInstantRetrieval => "GLACIER_IR".to_string(),
850            LifecycleStorageClass::IntelligentTiering => "INTELLIGENT_TIERING".to_string(),
851            LifecycleStorageClass::OneZoneIA => "ONEZONE_IA".to_string(),
852            LifecycleStorageClass::StandardIA => "STANDARD_IA".to_string(),
853        }
854    }
855}
856
857/// Builder for S3 lifecycle rule transitions.
858///
859/// Configures automatic transitions of objects to different storage classes.
860pub struct LifecycleRuleTransitionBuilder {
861    storage_class: LifecycleStorageClass,
862    transition_in_days: Option<u16>,
863}
864
865impl LifecycleRuleTransitionBuilder {
866    pub fn new(storage_class: LifecycleStorageClass) -> Self {
867        Self {
868            storage_class,
869            transition_in_days: None,
870        }
871    }
872
873    pub fn transition_in_days(self, days: LifecycleTransitionInDays) -> Self {
874        Self {
875            transition_in_days: Some(days.0),
876            ..self
877        }
878    }
879
880    #[must_use]
881    pub fn build(self) -> LifecycleRuleTransition {
882        LifecycleRuleTransition {
883            storage_class: self.storage_class.into(),
884            transition_in_days: self.transition_in_days.unwrap_or(0),
885        }
886    }
887}
888
889/// Builder for non-current version transitions in versioned buckets.
890///
891/// Configures automatic transitions for previous versions of objects.
892pub struct NonCurrentVersionTransitionBuilder {
893    storage_class: LifecycleStorageClass,
894    transition_in_days: u32,
895    newer_non_current_versions: Option<u32>,
896}
897
898impl NonCurrentVersionTransitionBuilder {
899    pub fn new(storage_class: LifecycleStorageClass, transition_in_days: u32) -> Self {
900        Self {
901            storage_class,
902            transition_in_days,
903            newer_non_current_versions: None,
904        }
905    }
906
907    pub fn newer_non_current_versions(self, versions: u32) -> Self {
908        Self {
909            newer_non_current_versions: Some(versions),
910            ..self
911        }
912    }
913
914    #[must_use]
915    pub fn build(self) -> NonCurrentVersionTransition {
916        NonCurrentVersionTransition {
917            storage_class: self.storage_class.into(),
918            transition_in_days: self.transition_in_days,
919            newer_non_current_versions: self.newer_non_current_versions,
920        }
921    }
922}
923
924#[derive(Debug, Clone)]
925pub enum LifecycleRuleStatus {
926    Enabled,
927    Disabled,
928}
929
930impl From<LifecycleRuleStatus> for String {
931    fn from(value: LifecycleRuleStatus) -> Self {
932        match value {
933            LifecycleRuleStatus::Enabled => "Enabled".to_string(),
934            LifecycleRuleStatus::Disabled => "Disabled".to_string(),
935        }
936    }
937}
938
939/// Builder for S3 lifecycle rules.
940///
941/// Defines rules for automatic object expiration and transitions between storage classes.
942pub struct LifecycleRuleBuilder {
943    id: Option<String>,
944    status: LifecycleRuleStatus,
945    expiration_in_days: Option<u16>, // expiration must be > than expiration in transition (ow boy...)
946    prefix: Option<String>,
947    object_size_greater_than: Option<u32>,
948    object_size_less_than: Option<u32>,
949    abort_incomplete_multipart_upload: Option<u16>,
950    non_current_version_expiration: Option<u16>,
951    transitions: Option<Vec<LifecycleRuleTransition>>,
952    non_current_version_transitions: Option<Vec<NonCurrentVersionTransition>>,
953}
954
955impl LifecycleRuleBuilder {
956    pub fn new(status: LifecycleRuleStatus) -> Self {
957        Self {
958            status,
959            id: None,
960            expiration_in_days: None,
961            prefix: None,
962            object_size_greater_than: None,
963            object_size_less_than: None,
964            abort_incomplete_multipart_upload: None,
965            non_current_version_expiration: None,
966            transitions: None,
967            non_current_version_transitions: None,
968        }
969    }
970
971    pub fn id<T: Into<String>>(self, id: T) -> Self {
972        Self {
973            id: Some(id.into()),
974            ..self
975        }
976    }
977
978    pub fn expiration_in_days(self, days: u16) -> Self {
979        Self {
980            expiration_in_days: Some(days),
981            ..self
982        }
983    }
984
985    pub fn prefix<T: Into<String>>(self, prefix: T) -> Self {
986        Self {
987            prefix: Some(prefix.into()),
988            ..self
989        }
990    }
991
992    pub fn object_size(self, sizes: S3LifecycleObjectSizes) -> Self {
993        Self {
994            object_size_less_than: sizes.0,
995            object_size_greater_than: sizes.1,
996            ..self
997        }
998    }
999
1000    pub fn abort_incomplete_multipart_upload(self, days: u16) -> Self {
1001        Self {
1002            abort_incomplete_multipart_upload: Some(days),
1003            ..self
1004        }
1005    }
1006
1007    pub fn non_current_version_expiration(self, days: u16) -> Self {
1008        Self {
1009            non_current_version_expiration: Some(days),
1010            ..self
1011        }
1012    }
1013
1014    pub fn add_transition(mut self, transition: LifecycleRuleTransition) -> Self {
1015        if let Some(mut transitions) = self.transitions {
1016            transitions.push(transition);
1017            self.transitions = Some(transitions);
1018        } else {
1019            self.transitions = Some(vec![transition]);
1020        }
1021
1022        Self { ..self }
1023    }
1024
1025    pub fn add_non_current_version_transitions(mut self, transition: NonCurrentVersionTransition) -> Self {
1026        if let Some(mut transitions) = self.non_current_version_transitions {
1027            transitions.push(transition);
1028            self.non_current_version_transitions = Some(transitions);
1029        } else {
1030            self.non_current_version_transitions = Some(vec![transition]);
1031        }
1032
1033        Self { ..self }
1034    }
1035
1036    pub fn build(self) -> LifecycleRule {
1037        LifecycleRule {
1038            id: self.id,
1039            status: self.status.into(),
1040            expiration_in_days: self.expiration_in_days,
1041            prefix: self.prefix,
1042            object_size_greater_than: self.object_size_greater_than,
1043            object_size_less_than: self.object_size_less_than,
1044            transitions: self.transitions,
1045            abort_incomplete_multipart_upload: self.abort_incomplete_multipart_upload,
1046            non_current_version_expiration: self.non_current_version_expiration,
1047            non_current_version_transitions: self.non_current_version_transitions,
1048        }
1049    }
1050}
1051
1052/// Builder for S3 lifecycle configuration.
1053///
1054/// Combines multiple lifecycle rules into a configuration for a bucket.
1055pub struct LifecycleConfigurationBuilder {
1056    rules: Vec<LifecycleRule>,
1057    transition_minimum_size: Option<TransitionDefaultMinimumObjectSize>,
1058}
1059
1060impl Default for LifecycleConfigurationBuilder {
1061    fn default() -> Self {
1062        Self::new()
1063    }
1064}
1065
1066impl LifecycleConfigurationBuilder {
1067    pub fn new() -> Self {
1068        Self {
1069            rules: vec![],
1070            transition_minimum_size: None,
1071        }
1072    }
1073
1074    pub fn transition_minimum_size(self, size: TransitionDefaultMinimumObjectSize) -> Self {
1075        Self {
1076            transition_minimum_size: Some(size),
1077            ..self
1078        }
1079    }
1080
1081    pub fn add_rule(mut self, rule: LifecycleRule) -> Self {
1082        self.rules.push(rule);
1083        self
1084    }
1085
1086    #[must_use]
1087    pub fn build(self) -> LifecycleConfiguration {
1088        LifecycleConfiguration {
1089            rules: self.rules,
1090            transition_minimum_size: self.transition_minimum_size.map(|v| v.into()),
1091        }
1092    }
1093}
1094
1095/// Builder for S3 public access block configuration.
1096///
1097/// Controls public access to the bucket at the bucket level.
1098pub struct PublicAccessBlockConfigurationBuilder {
1099    block_public_acls: Option<bool>,
1100    block_public_policy: Option<bool>,
1101    ignore_public_acls: Option<bool>,
1102    restrict_public_buckets: Option<bool>,
1103}
1104
1105impl Default for PublicAccessBlockConfigurationBuilder {
1106    fn default() -> Self {
1107        Self::new()
1108    }
1109}
1110
1111impl PublicAccessBlockConfigurationBuilder {
1112    pub fn new() -> Self {
1113        Self {
1114            block_public_acls: None,
1115            block_public_policy: None,
1116            ignore_public_acls: None,
1117            restrict_public_buckets: None,
1118        }
1119    }
1120
1121    pub fn block_public_acls(self, config: bool) -> Self {
1122        Self {
1123            block_public_acls: Some(config),
1124            ..self
1125        }
1126    }
1127
1128    pub fn block_public_policy(self, config: bool) -> Self {
1129        Self {
1130            block_public_policy: Some(config),
1131            ..self
1132        }
1133    }
1134
1135    pub fn ignore_public_acls(self, config: bool) -> Self {
1136        Self {
1137            ignore_public_acls: Some(config),
1138            ..self
1139        }
1140    }
1141
1142    pub fn restrict_public_buckets(self, config: bool) -> Self {
1143        Self {
1144            restrict_public_buckets: Some(config),
1145            ..self
1146        }
1147    }
1148
1149    #[must_use]
1150    pub fn build(self) -> PublicAccessBlockConfiguration {
1151        PublicAccessBlockConfiguration {
1152            block_public_acls: self.block_public_acls,
1153            block_public_policy: self.block_public_policy,
1154            ignore_public_acls: self.ignore_public_acls,
1155            restrict_public_buckets: self.restrict_public_buckets,
1156        }
1157    }
1158}
1159
1160pub enum IntelligentTieringStatus {
1161    Enabled,
1162    Disabled,
1163}
1164
1165impl From<IntelligentTieringStatus> for String {
1166    fn from(value: IntelligentTieringStatus) -> String {
1167        match value {
1168            IntelligentTieringStatus::Enabled => "Enabled".to_string(),
1169            IntelligentTieringStatus::Disabled => "Disabled".to_string(),
1170        }
1171    }
1172}
1173
1174/// Builder for TagFilters of IntelligentTiering
1175/// 
1176/// See https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-s3-bucket-tagfilter.html
1177pub struct TagFilterBuilder {
1178    key: String,
1179    value: String,
1180}
1181
1182impl TagFilterBuilder {
1183    pub fn new<T: Into<String>>(key: T, value: T) -> Self {
1184        Self {
1185            key: key.into(),
1186            value: value.into(),
1187        }
1188    }
1189
1190    pub fn build(self) -> TagFilter {
1191        TagFilter {
1192            key: self.key,
1193            value: self.value,
1194        }
1195    }
1196}
1197
1198/// Builder for IntelligentTiering Configuration
1199/// 
1200/// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-intelligenttieringconfiguration.html
1201pub struct IntelligentTieringConfigurationBuilder {
1202    id: String,
1203    status: String,
1204    prefix: Option<String>,
1205    tag_filters: Option<Vec<TagFilter>>,
1206    tierings: Vec<Tiering>,
1207}
1208
1209impl IntelligentTieringConfigurationBuilder {
1210    pub fn new(id: &str, status: IntelligentTieringStatus, tierings: Vec<BucketTiering>) -> Self {
1211        IntelligentTieringConfigurationBuilder {
1212            id: id.to_string(),
1213            status: status.into(),
1214            prefix: None,
1215            tag_filters: None,
1216            tierings: tierings
1217                .into_iter()
1218                .map(|t| Tiering {
1219                    access_tier: t.0,
1220                    days: t.1,
1221                })
1222                .collect(),
1223        }
1224    }
1225
1226    pub fn prefix<T: Into<String>>(self, prefix: T) -> Self {
1227        Self {
1228            prefix: Some(prefix.into()),
1229            ..self
1230        }
1231    }
1232
1233    pub fn add_tag_filter(mut self, tag_filter: TagFilter) -> Self {
1234        if let Some(mut filters) = self.tag_filters {
1235            filters.push(tag_filter);
1236            self.tag_filters = Some(filters);
1237        } else {
1238            self.tag_filters = Some(vec![tag_filter]);
1239        }
1240
1241        self
1242    }
1243
1244    pub fn build(self) -> IntelligentTieringConfiguration {
1245        IntelligentTieringConfiguration {
1246            id: self.id,
1247            prefix: self.prefix,
1248            status: self.status,
1249            tag_filters: self.tag_filters,
1250            tierings: self.tierings,
1251        }
1252    }
1253}
1254
1255pub enum TableBucketType {
1256    Aws,
1257    Customer,
1258}
1259
1260impl From<TableBucketType> for String {
1261    fn from(value: TableBucketType) -> String {
1262        match value {
1263            TableBucketType::Aws => "aws".to_string(),
1264            TableBucketType::Customer => "customer".to_string(),
1265        }
1266    }
1267}
1268
1269pub enum Expiration {
1270    Enabled,
1271    Disabled,
1272}
1273
1274impl From<Expiration> for String {
1275    fn from(value: Expiration) -> String {
1276        match value {
1277            Expiration::Enabled => "ENABLED".to_string(),
1278            Expiration::Disabled => "DISABLED".to_string(),
1279        }
1280    }
1281}
1282
1283/// Builder for the Record Expiration of Bucket Metadataconfiguration
1284/// 
1285/// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-metadataconfiguration-journaltableconfiguration.html
1286pub struct RecordExpirationBuilder {
1287    days: Option<u32>,
1288    expiration: String,
1289}
1290
1291impl RecordExpirationBuilder {
1292    pub fn new(expiration: Expiration) -> Self {
1293        Self {
1294            expiration: expiration.into(),
1295            days: None,
1296        }
1297    }
1298
1299    pub fn days(self, days: RecordExpirationDays) -> Self {
1300        Self {
1301            days: Some(days.0),
1302            ..self
1303        }
1304    }
1305
1306    pub fn build(self) -> RecordExpiration {
1307        RecordExpiration {
1308            days: self.days,
1309            expiration: self.expiration,
1310        }
1311    }
1312}
1313
1314pub struct JournalTableConfigurationBuilder {
1315    record_expiration: RecordExpiration,
1316    table_arn: Option<Value>,
1317    table_name: Option<String>,
1318}
1319
1320impl JournalTableConfigurationBuilder {
1321    pub fn new(record_expiration: RecordExpiration) -> Self {
1322        Self {
1323            record_expiration,
1324            table_arn: None,
1325            table_name: None,
1326        }
1327    }
1328
1329    pub fn table_name<T: Into<String>>(self, name: T) -> Self {
1330        Self {
1331            table_name: Some(name.into()),
1332            ..self
1333        }
1334    }
1335
1336    pub fn table_arn(self, arn: Value) -> Self {
1337        Self {
1338            table_arn: Some(arn),
1339            ..self
1340        }
1341    }
1342
1343    pub fn build(self) -> JournalTableConfiguration {
1344        JournalTableConfiguration {
1345            record_expiration: self.record_expiration,
1346            table_arn: self.table_arn,
1347            table_name: self.table_name,
1348        }
1349    }
1350}
1351
1352/// Builder for the metadata destination of Bucket Metadataconfiguration
1353/// 
1354/// See https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-s3-bucket-metadatadestination.html
1355pub struct MetadataDestinationBuilder {
1356    table_bucket_type: String,
1357    table_bucket_arn: Option<Value>,
1358    table_namespace: Option<String>,
1359}
1360
1361impl MetadataDestinationBuilder {
1362    pub fn new(table_bucket_type: TableBucketType) -> Self {
1363        Self {
1364            table_bucket_type: table_bucket_type.into(),
1365            table_bucket_arn: None,
1366            table_namespace: None,
1367        }
1368    }
1369
1370    pub fn table_bucket_arn(self, table_bucket_arn: Value) -> Self {
1371        Self {
1372            table_bucket_arn: Some(table_bucket_arn),
1373            ..self
1374        }
1375    }
1376
1377    pub fn table_namespace<T: Into<String>>(self, table_namespace: T) -> Self {
1378        Self {
1379            table_namespace: Some(table_namespace.into()),
1380            ..self
1381        }
1382    }
1383
1384    pub fn build(self) -> MetadataDestination {
1385        MetadataDestination {
1386            table_bucket_arn: self.table_bucket_arn,
1387            table_bucket_type: self.table_bucket_type,
1388            table_namespace: self.table_namespace,
1389        }
1390    }
1391}
1392
1393pub enum ConfigurationState {
1394    Enabled,
1395    Disabled,
1396}
1397
1398impl From<ConfigurationState> for String {
1399    fn from(value: ConfigurationState) -> String {
1400        match value {
1401            ConfigurationState::Enabled => "ENABLED".to_string(),
1402            ConfigurationState::Disabled => "DISABLED".to_string(),
1403        }
1404    }
1405}
1406
1407/// Builder for the InventoryTable Config
1408/// 
1409/// See https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-s3-bucket-inventorytableconfiguration.html
1410pub struct InventoryTableConfigurationBuilder {
1411    configuration_state: String,
1412    table_arn: Option<Value>,
1413    table_name: Option<String>,
1414}
1415
1416impl InventoryTableConfigurationBuilder {
1417    pub fn new(configuration_state: ConfigurationState) -> Self {
1418        Self {
1419            configuration_state: configuration_state.into(),
1420            table_arn: None,
1421            table_name: None,
1422        }
1423    }
1424
1425    pub fn table_arn(self, table_arn: Value) -> Self {
1426        Self {
1427            table_arn: Some(table_arn),
1428            ..self
1429        }
1430    }
1431
1432    pub fn table_name<T: Into<String>>(self, table_name: T) -> Self {
1433        Self {
1434            table_name: Some(table_name.into()),
1435            ..self
1436        }
1437    }
1438
1439    pub fn build(self) -> InventoryTableConfiguration {
1440        InventoryTableConfiguration {
1441            configuration_state: self.configuration_state,
1442            table_arn: self.table_arn,
1443            table_name: self.table_name,
1444        }
1445    }
1446}
1447
1448pub struct MetadataConfigurationBuilder {
1449    destination: Option<MetadataDestination>,
1450    inventory_table_configuration: Option<InventoryTableConfiguration>,
1451    journal_table_configuration: JournalTableConfiguration,
1452}
1453
1454impl MetadataConfigurationBuilder {
1455    pub fn new(journal_table_configuration: JournalTableConfiguration) -> Self {
1456        MetadataConfigurationBuilder {
1457            journal_table_configuration,
1458            destination: None,
1459            inventory_table_configuration: None,
1460        }
1461    }
1462
1463    pub fn destination(self, destination: MetadataDestination) -> Self {
1464        Self {
1465            destination: Some(destination),
1466            ..self
1467        }
1468    }
1469
1470    pub fn inventory_table_configuration(self, inventory_table_configuration: InventoryTableConfiguration) -> Self {
1471        Self {
1472            inventory_table_configuration: Some(inventory_table_configuration),
1473            ..self
1474        }
1475    }
1476
1477    pub fn build(self) -> MetadataConfiguration {
1478        MetadataConfiguration {
1479            destination: self.destination,
1480            inventory_table_configuration: self.inventory_table_configuration,
1481            journal_table_configuration: self.journal_table_configuration,
1482        }
1483    }
1484}