rusty_cdk_core/s3/
builder.rs

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