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