Skip to main content

rusty_cdk_core/cloudfront/
builder.rs

1use crate::cloudfront::{CacheBehavior, CachePolicy, CachePolicyConfig, CachePolicyProperties, CachePolicyRef, CachePolicyType, CookiesConfig, CustomOriginConfig, DefaultCacheBehavior, Distribution, DistributionConfig, DistributionProperties, DistributionRef, DistributionType, HeadersConfig, Origin, OriginAccessControl, OriginAccessControlConfig, OriginAccessControlDtoType, OriginAccessControlRef, OriginControlProperties, OriginCustomHeader, ParametersInCacheKeyAndForwardedToOrigin, QueryStringsConfig, S3OriginConfig, ViewerCertificate, VpcOriginConfig};
2use crate::iam::{Effect, PolicyDocumentBuilder, PrincipalBuilder, StatementBuilder};
3use crate::intrinsic::{get_att, get_ref, join, AWS_ACCOUNT_PSEUDO_PARAM};
4use crate::s3::BucketPolicyBuilder;
5use crate::s3::BucketRef;
6use crate::shared::HttpMethod::{Delete, Get, Head, Options, Patch, Post, Put};
7use crate::shared::Id;
8use crate::stack::{Resource, StackBuilder};
9use crate::wrappers::{CfConnectionTimeout, ConnectionAttempts, DefaultRootObject, IamAction, OriginPath, S3OriginReadTimeout};
10use serde_json::{json, Value};
11use std::marker::PhantomData;
12use crate::type_state;
13
14#[derive(Debug, Clone)]
15pub enum SslSupportedMethod {
16    SniOnly,
17    Vip,
18    StaticIp,
19}
20
21impl From<SslSupportedMethod> for String {
22    fn from(value: SslSupportedMethod) -> Self {
23        match value {
24            SslSupportedMethod::SniOnly => "sni-only".to_string(),
25            SslSupportedMethod::Vip => "vip".to_string(),
26            SslSupportedMethod::StaticIp => "static-ip".to_string(),
27        }
28    }
29}
30
31#[derive(Debug, Clone)]
32pub enum MinProtocolVersion {
33    SSLV3,
34    TLSv1,
35    TLSv1_1,
36    TLSv1_2_2018,
37    TLSv1_2_2019,
38    TLSv1_2_2021,
39    TLSv1_2_2025,
40    TLSv1_3,
41}
42
43impl From<MinProtocolVersion> for String {
44    fn from(value: MinProtocolVersion) -> Self {
45        match value {
46            MinProtocolVersion::SSLV3 => "SSLv3".to_string(),
47            MinProtocolVersion::TLSv1 => "TLSv1".to_string(),
48            MinProtocolVersion::TLSv1_1 => "TLSv1.1_2016".to_string(),
49            MinProtocolVersion::TLSv1_2_2018 => "TLSv1.2_2018".to_string(),
50            MinProtocolVersion::TLSv1_2_2019 => "TLSv1.2_2019".to_string(),
51            MinProtocolVersion::TLSv1_2_2021 => "TLSv1.2_2021".to_string(),
52            MinProtocolVersion::TLSv1_2_2025 => "TLSv1.2_2025".to_string(),
53            MinProtocolVersion::TLSv1_3 => "TLSv1.3_2025".to_string(),
54        }
55    }
56}
57
58type_state!(
59    ViewerCertificateState,
60    ViewerCertificateStateStartState,
61    ViewerCertificateStateAcmOrIamState,
62    ViewerCertificateStateEndState,
63);
64
65/// Builder for CloudFront viewer certificates.
66pub struct ViewerCertificateBuilder<T: ViewerCertificateState> {
67    phantom_data: PhantomData<T>,
68    cloudfront_default_cert: Option<bool>,
69    acm_cert_arn: Option<String>,
70    iam_cert_id: Option<String>,
71    min_protocol_version: Option<String>,
72    ssl_support_method: Option<String>,
73}
74
75impl Default for ViewerCertificateBuilder<ViewerCertificateStateStartState> {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl ViewerCertificateBuilder<ViewerCertificateStateStartState> {
82    pub fn new() -> ViewerCertificateBuilder<ViewerCertificateStateStartState> {
83        ViewerCertificateBuilder {
84            phantom_data: Default::default(),
85            acm_cert_arn: None,
86            cloudfront_default_cert: None,
87            iam_cert_id: None,
88            min_protocol_version: None,
89            ssl_support_method: None,
90        }
91    }
92
93    pub fn cloudfront_default_cert(self) -> ViewerCertificateBuilder<ViewerCertificateStateEndState> {
94        ViewerCertificateBuilder {
95            phantom_data: Default::default(),
96            cloudfront_default_cert: Some(true),
97            acm_cert_arn: self.acm_cert_arn,
98            iam_cert_id: self.iam_cert_id,
99            min_protocol_version: self.min_protocol_version,
100            ssl_support_method: self.ssl_support_method,
101        }
102    }
103
104    pub fn iam_cert_id(self, id: String) -> ViewerCertificateBuilder<ViewerCertificateStateAcmOrIamState> {
105        ViewerCertificateBuilder {
106            phantom_data: Default::default(),
107            cloudfront_default_cert: Some(true),
108            acm_cert_arn: self.acm_cert_arn,
109            iam_cert_id: Some(id),
110            min_protocol_version: self.min_protocol_version,
111            ssl_support_method: self.ssl_support_method,
112        }
113    }
114
115    pub fn acm_cert_arn(self, id: String) -> ViewerCertificateBuilder<ViewerCertificateStateAcmOrIamState> {
116        ViewerCertificateBuilder {
117            phantom_data: Default::default(),
118            cloudfront_default_cert: Some(true),
119            acm_cert_arn: Some(id),
120            iam_cert_id: self.iam_cert_id,
121            min_protocol_version: self.min_protocol_version,
122            ssl_support_method: self.ssl_support_method,
123        }
124    }
125}
126
127impl<T: ViewerCertificateState> ViewerCertificateBuilder<T> {
128    fn build_internal(self) -> ViewerCertificate {
129        ViewerCertificate {
130            cloudfront_default_cert: self.cloudfront_default_cert,
131            acm_cert_arn: self.acm_cert_arn,
132            iam_cert_id: self.iam_cert_id,
133            min_protocol_version: self.min_protocol_version,
134            ssl_support_method: self.ssl_support_method,
135        }
136    }
137}
138
139impl ViewerCertificateBuilder<ViewerCertificateStateAcmOrIamState> {
140    pub fn min_protocol_version(self, protocol_version: MinProtocolVersion) -> Self {
141        Self {
142            phantom_data: Default::default(),
143            min_protocol_version: Some(protocol_version.into()),
144            cloudfront_default_cert: self.cloudfront_default_cert,
145            acm_cert_arn: self.acm_cert_arn,
146            iam_cert_id: self.iam_cert_id,
147            ssl_support_method: self.ssl_support_method,
148        }
149    }
150
151    pub fn ssl_support_method(self, ssl_support: SslSupportedMethod) -> Self {
152        Self {
153            phantom_data: Default::default(),
154            ssl_support_method: Some(ssl_support.into()),
155            min_protocol_version: self.min_protocol_version,
156            cloudfront_default_cert: self.cloudfront_default_cert,
157            acm_cert_arn: self.acm_cert_arn,
158            iam_cert_id: self.iam_cert_id,
159        }
160    }
161
162    #[must_use]
163    pub fn build(self) -> ViewerCertificate {
164        self.build_internal()
165    }
166}
167
168impl ViewerCertificateBuilder<ViewerCertificateStateEndState> {
169    #[must_use]
170    pub fn build(self) -> ViewerCertificate {
171        self.build_internal()
172    }
173}
174
175#[derive(Debug, Clone)]
176pub enum Cookies {
177    None,
178    Whitelist(Vec<String>),
179    AllExcept(Vec<String>),
180    All,
181}
182
183#[derive(Debug, Clone)]
184pub enum QueryString {
185    None,
186    Whitelist(Vec<String>),
187    AllExcept(Vec<String>),
188    All,
189}
190
191#[derive(Debug, Clone)]
192pub enum Headers {
193    None,
194    Whitelist(Vec<String>),
195}
196
197/// Builder for cache key and forwarding parameters.
198///
199/// Configures which request parameters (cookies, headers, query strings) are included in the cache key and forwarded to the origin.
200pub struct ParametersInCacheKeyAndForwardedToOriginBuilder {
201    cookies_config: CookiesConfig,
202    headers_config: HeadersConfig,
203    query_strings_config: QueryStringsConfig,
204    accept_encoding_gzip: bool,
205    accept_encoding_brotli: Option<bool>,
206}
207
208impl ParametersInCacheKeyAndForwardedToOriginBuilder {
209    pub fn new(accept_encoding_gzip: bool, cookies: Cookies, query_string: QueryString, headers: Headers) -> Self {
210        let cookies_config = match cookies {
211            Cookies::None => CookiesConfig {
212                cookie_behavior: "none".to_string(),
213                cookies: None,
214            },
215            Cookies::Whitelist(list) => CookiesConfig {
216                cookie_behavior: "whitelist".to_string(),
217                cookies: Some(list),
218            },
219            Cookies::AllExcept(list) => CookiesConfig {
220                cookie_behavior: "allExcept".to_string(),
221                cookies: Some(list),
222            },
223            Cookies::All => CookiesConfig {
224                cookie_behavior: "all".to_string(),
225                cookies: None,
226            },
227        };
228        let query_strings_config = match query_string {
229            QueryString::None => QueryStringsConfig {
230                query_strings_behavior: "none".to_string(),
231                query_strings: None,
232            },
233            QueryString::Whitelist(list) => QueryStringsConfig {
234                query_strings_behavior: "whitelist".to_string(),
235                query_strings: Some(list),
236            },
237            QueryString::AllExcept(list) => QueryStringsConfig {
238                query_strings_behavior: "allExcept".to_string(),
239                query_strings: Some(list),
240            },
241            QueryString::All => QueryStringsConfig {
242                query_strings_behavior: "all".to_string(),
243                query_strings: None,
244            },
245        };
246        let headers_config = match headers {
247            Headers::None => HeadersConfig {
248                headers_behavior: "none".to_string(),
249                headers: None,
250            },
251            Headers::Whitelist(list) => HeadersConfig {
252                headers_behavior: "whitelist".to_string(),
253                headers: Some(list),
254            },
255        };
256
257        Self {
258            cookies_config,
259            headers_config,
260            query_strings_config,
261            accept_encoding_gzip,
262            accept_encoding_brotli: None,
263        }
264    }
265
266    pub fn accept_encoding_brotli(self, accept: bool) -> Self {
267        Self {
268            accept_encoding_brotli: Some(accept),
269            ..self
270        }
271    }
272
273    #[must_use]
274    pub fn build(self) -> ParametersInCacheKeyAndForwardedToOrigin {
275        ParametersInCacheKeyAndForwardedToOrigin {
276            cookies_config: self.cookies_config,
277            accept_encoding_brotli: self.accept_encoding_brotli,
278            accept_encoding_gzip: self.accept_encoding_gzip,
279            headers_config: self.headers_config,
280            query_strings_config: self.query_strings_config,
281        }
282    }
283}
284
285/// Builder for CloudFront cache policies.
286pub struct CachePolicyBuilder {
287    id: Id,
288    name: String,
289    default_ttl: u32,
290    min_ttl: u32,
291    max_ttl: u32,
292    cache_params: ParametersInCacheKeyAndForwardedToOrigin,
293}
294
295impl CachePolicyBuilder {
296    /// Creates a new CloudFront cache policy builder.
297    ///
298    /// # Arguments
299    /// * `id` - Unique identifier for the cache policy
300    /// * `unique_name` - Name for the cache policy (must be unique)
301    /// * `default_ttl` - Default time to live in seconds
302    /// * `min_ttl` - Minimum time to live in seconds
303    /// * `max_ttl` - Maximum time to live in seconds
304    /// * `cache_params` - Parameters for cache key and origin forwarding
305    pub fn new(
306        id: &str,
307        unique_name: &str,
308        default_ttl: u32,
309        min_ttl: u32,
310        max_ttl: u32,
311        cache_params: ParametersInCacheKeyAndForwardedToOrigin,
312    ) -> Self {
313        Self {
314            id: Id(id.to_string()),
315            name: unique_name.to_string(),
316            default_ttl,
317            min_ttl,
318            max_ttl,
319            cache_params,
320        }
321    }
322
323    pub fn build(self, stack_builder: &mut StackBuilder) -> CachePolicyRef {
324        let resource_id = Resource::generate_id("CachePolicy");
325        stack_builder.add_resource(CachePolicy {
326            id: self.id,
327            resource_id: resource_id.clone(),
328            r#type: CachePolicyType::CachePolicyType,
329            properties: CachePolicyProperties {
330                config: CachePolicyConfig {
331                    default_ttl: self.default_ttl,
332                    min_ttl: self.min_ttl,
333                    max_ttl: self.max_ttl,
334                    name: self.name,
335                    params_in_cache_key_and_forwarded: self.cache_params,
336                },
337            },
338        });
339        CachePolicyRef::internal_new(resource_id)
340    }
341}
342
343#[derive(Debug, Clone)]
344pub enum HttpVersion {
345    Http1,
346    Http2,
347    Http3,
348    Http2And3,
349}
350
351impl From<HttpVersion> for String {
352    fn from(value: HttpVersion) -> Self {
353        match value {
354            HttpVersion::Http1 => "http1.1".to_string(),
355            HttpVersion::Http2 => "http2".to_string(),
356            HttpVersion::Http3 => "http3".to_string(),
357            HttpVersion::Http2And3 => "http2and3".to_string(),
358        }
359    }
360}
361
362#[derive(Debug, Clone)]
363pub enum PriceClass {
364    PriceClass100,
365    PriceClass200,
366    PriceClassAll,
367    None,
368}
369
370impl From<PriceClass> for String {
371    fn from(value: PriceClass) -> Self {
372        match value {
373            PriceClass::PriceClass100 => "PriceClass_100".to_string(),
374            PriceClass::PriceClass200 => "PriceClass_200".to_string(),
375            PriceClass::PriceClassAll => "PriceClass_All".to_string(),
376            PriceClass::None => "None".to_string(),
377        }
378    }
379}
380
381#[derive(Debug, Clone)]
382pub enum OriginProtocolPolicy {
383    HttpOnly,
384    MatchViewer,
385    HttpsOnly,
386}
387
388impl From<OriginProtocolPolicy> for String {
389    fn from(value: OriginProtocolPolicy) -> Self {
390        match value {
391            OriginProtocolPolicy::HttpOnly => "http-only".to_string(),
392            OriginProtocolPolicy::MatchViewer => "match-viewer".to_string(),
393            OriginProtocolPolicy::HttpsOnly => "https-only".to_string(),
394        }
395    }
396}
397
398#[derive(Debug, Clone)]
399pub enum IpAddressType {
400    IPv4,
401    IPv6,
402    Dualstack,
403}
404
405impl From<IpAddressType> for String {
406    fn from(value: IpAddressType) -> Self {
407        match value {
408            IpAddressType::IPv4 => "ipv4".to_string(),
409            IpAddressType::IPv6 => "ipv6".to_string(),
410            IpAddressType::Dualstack => "dualstack".to_string(),
411        }
412    }
413}
414
415type_state!(
416    OriginState,
417    OriginStartState,
418    OriginS3OriginState,
419    OriginCustomOriginState,
420);
421
422// TODO more origins
423
424/// Builder for CloudFront distribution origins.
425pub struct OriginBuilder<T: OriginState> {
426    phantom_data: PhantomData<T>,
427    id: String,
428    bucket_arn: Option<Value>,
429    bucket_ref: Option<Value>,
430    domain_name: Option<Value>,
431    connection_attempts: Option<u8>,
432    connection_timeout: Option<u16>,
433    response_completion_timeout: Option<u16>,
434    origin_access_control_id: Option<Value>,
435    origin_path: Option<String>,
436    s3origin_config: Option<S3OriginConfig>,
437    origin_custom_headers: Option<Vec<OriginCustomHeader>>,
438    vpc_origin_config: Option<VpcOriginConfig>,
439    custom_origin_config: Option<CustomOriginConfig>,
440}
441
442impl OriginBuilder<OriginStartState> {
443    pub fn new(origin_id: &str) -> Self {
444        Self {
445            phantom_data: Default::default(),
446            id: origin_id.to_string(),
447            bucket_arn: None,
448            bucket_ref: None,
449            domain_name: None,
450            connection_attempts: None,
451            connection_timeout: None,
452            origin_access_control_id: None,
453            origin_path: None,
454            response_completion_timeout: None,
455            s3origin_config: None,
456            origin_custom_headers: None,
457            vpc_origin_config: None,
458            custom_origin_config: None,
459        }
460    }
461
462    /// Configures an S3 bucket as the origin.
463    ///
464    /// Automatically creates a bucket policy allowing CloudFront access via Origin Access Control.
465    pub fn s3_origin(
466        self,
467        bucket: &BucketRef,
468        oac: &OriginAccessControlRef,
469        origin_read_timeout: Option<S3OriginReadTimeout>,
470    ) -> OriginBuilder<OriginS3OriginState> {
471        let s3origin_config = S3OriginConfig {
472            origin_read_timeout: origin_read_timeout.map(|v| v.0),
473        };
474
475        let domain = bucket.get_att("RegionalDomainName");
476
477        OriginBuilder {
478            phantom_data: Default::default(),
479            id: self.id.to_string(),
480            bucket_arn: Some(bucket.get_arn()),
481            bucket_ref: Some(bucket.get_ref()),
482            domain_name: Some(domain),
483            connection_attempts: self.connection_attempts,
484            connection_timeout: self.connection_timeout,
485            origin_access_control_id: Some(oac.get_att("Id")),
486            origin_path: self.origin_path,
487            response_completion_timeout: self.response_completion_timeout,
488            origin_custom_headers: self.origin_custom_headers,
489            s3origin_config: Some(s3origin_config),
490            vpc_origin_config: None,
491            custom_origin_config: None,
492        }
493    }
494    
495    // TODO add test
496    //  and could also add additional methods for ELB etc. that pass in the ELB, to have extra safety
497
498    /// Configures a custom origin.
499    pub fn custom_origin(self, domain: &str, policy: OriginProtocolPolicy) -> OriginBuilder<OriginCustomOriginState> {
500        let custom_origin_config = CustomOriginConfig {
501            origin_protocol_policy: policy.into(),
502            http_port: None,
503            https_port: None,
504            ip_address_type: None,
505            origin_keep_alive_timeout: None,
506            origin_read_timeout: None,
507            origin_ssl_protocols: None,
508        };
509
510        OriginBuilder {
511            phantom_data: Default::default(),
512            id: self.id.to_string(),
513            domain_name: Some(Value::String(domain.to_string())),
514            connection_attempts: self.connection_attempts,
515            connection_timeout: self.connection_timeout,
516            origin_path: self.origin_path,
517            response_completion_timeout: self.response_completion_timeout,
518            origin_custom_headers: self.origin_custom_headers,
519            custom_origin_config: Some(custom_origin_config),
520            vpc_origin_config: None,
521            origin_access_control_id: None,
522            s3origin_config: None,
523            bucket_arn: None,
524            bucket_ref: None,
525        }
526    }
527}
528
529impl<T: OriginState> OriginBuilder<T> {
530    pub fn connection_attempts(self, attempts: ConnectionAttempts) -> Self {
531        Self {
532            connection_attempts: Some(attempts.0),
533            ..self
534        }
535    }
536
537    pub fn timeouts(self, timeouts: CfConnectionTimeout) -> Self {
538        Self {
539            connection_timeout: timeouts.0,
540            response_completion_timeout: timeouts.1,
541            ..self
542        }
543    }
544
545    pub fn origin_path(self, path: OriginPath) -> Self {
546        Self {
547            origin_path: Some(path.0),
548            ..self
549        }
550    }
551
552    fn build_internal(self) -> Origin {
553        Origin {
554            id: self.id,
555            s3_bucket_policy: None,
556            domain_name: self.domain_name.expect("domain name should be present for cloudfront distribution"),
557            connection_attempts: self.connection_attempts,
558            connection_timeout: self.connection_timeout,
559            origin_access_control_id: self.origin_access_control_id,
560            origin_path: self.origin_path,
561            response_completion_timeout: self.response_completion_timeout,
562            s3origin_config: self.s3origin_config,
563            origin_custom_headers: self.origin_custom_headers,
564            vpc_origin_config: self.vpc_origin_config,
565            custom_origin_config: self.custom_origin_config,
566        }
567    }
568}
569
570impl OriginBuilder<OriginCustomOriginState> {
571    pub fn ip_address_type(self, address_type: IpAddressType) -> Self {
572        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
573        config.ip_address_type = Some(address_type.into());
574
575        OriginBuilder {
576            custom_origin_config: Some(config),
577            ..self
578        }
579    }
580    pub fn http_port(self, port: u16) -> Self {
581        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
582        config.http_port = Some(port);
583
584        OriginBuilder {
585            custom_origin_config: Some(config),
586            ..self
587        }
588    }
589
590    pub fn https_port(self, port: u16) -> Self {
591        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
592        config.https_port = Some(port);
593
594        OriginBuilder {
595            custom_origin_config: Some(config),
596            ..self
597        }
598    }
599
600    pub fn origin_keep_alive_timeout(self, timeout: u8) -> Self {
601        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
602        config.origin_keep_alive_timeout = Some(timeout);
603
604        OriginBuilder {
605            custom_origin_config: Some(config),
606            ..self
607        }
608    }
609
610    pub fn origin_read_timeout(self, timeout: u8) -> Self {
611        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
612        config.origin_read_timeout = Some(timeout);
613
614        OriginBuilder {
615            custom_origin_config: Some(config),
616            ..self
617        }
618    }
619
620    pub fn add_origin_ssl_protocol(self, protocol: String) -> Self {
621        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
622
623        let protocols = if let Some(mut protocols) = config.origin_ssl_protocols {
624            protocols.push(protocol);
625            protocols
626        } else {
627            vec![protocol]
628        };
629
630        config.origin_ssl_protocols = Some(protocols);
631
632        OriginBuilder {
633            custom_origin_config: Some(config),
634            ..self
635        }
636    }
637
638    pub fn build(self) -> Origin {
639        self.build_internal()
640    }
641}
642
643impl OriginBuilder<OriginS3OriginState> {
644    pub fn build(mut self) -> Origin {
645        let bucket_ref = self.bucket_ref.take().expect("bucket ref to be present in S3 origin state");
646        let bucket_arn = self.bucket_arn.take().expect("bucket arn to be present in S3 origin state");
647
648        let bucket_items = vec![join("", vec![bucket_arn, Value::String("/*".to_string())])];
649        let principal = PrincipalBuilder::new().service("cloudfront.amazonaws.com".to_string()).build();
650        let statement = StatementBuilder::new(vec![IamAction("s3:GetObject".to_string())], Effect::Allow)
651            .resources(bucket_items)
652            .principal(principal)
653            .build();
654        let doc = PolicyDocumentBuilder::new(vec![statement]).build();
655        let bucket_policy_id = format!("{}-website-s3-policy", self.id);
656        let (_, s3_policy) = BucketPolicyBuilder::internal_new(bucket_policy_id.as_str(), bucket_ref, doc).raw_build();
657
658        let mut origin = self.build_internal();
659        origin.s3_bucket_policy = Some(s3_policy);
660
661        origin
662    }
663}
664
665#[derive(Debug, Clone)]
666pub enum DefaultCacheAllowedMethods {
667    GetHead,
668    GetHeadOptions,
669    All,
670}
671
672impl From<DefaultCacheAllowedMethods> for Vec<String> {
673    fn from(value: DefaultCacheAllowedMethods) -> Self {
674        match value {
675            DefaultCacheAllowedMethods::GetHead => vec![Get.into(), Head.into()],
676            DefaultCacheAllowedMethods::GetHeadOptions => vec![Get.into(), Head.into(), Options.into()],
677            DefaultCacheAllowedMethods::All => vec![
678                Get.into(),
679                Head.into(),
680                Options.into(),
681                Put.into(),
682                Patch.into(),
683                Post.into(),
684                Delete.into(),
685            ],
686        }
687    }
688}
689
690#[derive(Debug, Clone)]
691pub enum DefaultCacheCachedMethods {
692    GetHead,
693    GetHeadOptions,
694}
695
696impl From<DefaultCacheCachedMethods> for Vec<String> {
697    fn from(value: DefaultCacheCachedMethods) -> Self {
698        match value {
699            DefaultCacheCachedMethods::GetHead => vec![Get.into(), Head.into()],
700            DefaultCacheCachedMethods::GetHeadOptions => vec![Get.into(), Head.into(), Options.into()],
701        }
702    }
703}
704
705#[derive(Debug, Clone)]
706pub enum ViewerProtocolPolicy {
707    AllowAll,
708    RedirectToHttps,
709    HttpsOnly,
710}
711
712impl From<ViewerProtocolPolicy> for String {
713    fn from(value: ViewerProtocolPolicy) -> Self {
714        match value {
715            ViewerProtocolPolicy::AllowAll => "allow-all".to_string(),
716            ViewerProtocolPolicy::RedirectToHttps => "redirect-to-https".to_string(),
717            ViewerProtocolPolicy::HttpsOnly => "https-only".to_string(),
718        }
719    }
720}
721
722/// Builder for CloudFront default cache behavior.
723pub struct DefaultCacheBehaviorBuilder {
724    target_origin_id: String,
725    cache_policy_id: Value,
726    viewer_protocol_policy: String,
727    allowed_methods: Option<Vec<String>>,
728    cached_methods: Option<Vec<String>>,
729    compress: Option<bool>,
730}
731
732impl DefaultCacheBehaviorBuilder {
733    pub fn new(origin: &Origin, policy: &CachePolicyRef, viewer_protocol_policy: ViewerProtocolPolicy) -> Self {
734        Self {
735            target_origin_id: origin.get_origin_id().to_string(),
736            cache_policy_id: policy.get_att("Id"),
737            viewer_protocol_policy: viewer_protocol_policy.into(),
738            allowed_methods: None,
739            cached_methods: None,
740            compress: None,
741        }
742    }
743
744    pub fn allowed_methods(self, methods: DefaultCacheAllowedMethods) -> Self {
745        Self {
746            allowed_methods: Some(methods.into()),
747            target_origin_id: self.target_origin_id,
748            cache_policy_id: self.cache_policy_id,
749            viewer_protocol_policy: self.viewer_protocol_policy,
750            cached_methods: self.cached_methods,
751            compress: self.compress,
752        }
753    }
754
755    pub fn cached_methods(self, methods: DefaultCacheCachedMethods) -> Self {
756        Self {
757            cached_methods: Some(methods.into()),
758            target_origin_id: self.target_origin_id,
759            cache_policy_id: self.cache_policy_id,
760            viewer_protocol_policy: self.viewer_protocol_policy,
761            allowed_methods: self.allowed_methods,
762            compress: self.compress,
763        }
764    }
765
766    pub fn compress(self, compress: bool) -> Self {
767        Self {
768            compress: Some(compress),
769            target_origin_id: self.target_origin_id,
770            cache_policy_id: self.cache_policy_id,
771            viewer_protocol_policy: self.viewer_protocol_policy,
772            allowed_methods: self.allowed_methods,
773            cached_methods: self.cached_methods,
774        }
775    }
776
777    pub fn build(self) -> DefaultCacheBehavior {
778        DefaultCacheBehavior {
779            target_origin_id: self.target_origin_id,
780            cache_policy_id: self.cache_policy_id,
781            viewer_protocol_policy: self.viewer_protocol_policy,
782            allowed_methods: self.allowed_methods,
783            cached_methods: self.cached_methods,
784            compress: self.compress,
785        }
786    }
787}
788
789type_state!(
790    DistributionState,
791    DistributionStartState,
792    DistributionOriginState,
793);
794
795#[derive(Debug, Clone)]
796pub enum SigningBehavior {
797    Never,
798    NoOverride,
799    Always,
800}
801
802impl From<SigningBehavior> for String {
803    fn from(value: SigningBehavior) -> Self {
804        match value {
805            SigningBehavior::Never => "never".to_string(),
806            SigningBehavior::NoOverride => "no-override".to_string(),
807            SigningBehavior::Always => "always".to_string(),
808        }
809    }
810}
811
812#[derive(Debug, Clone)]
813pub enum SigningProtocol {
814    SigV4,
815}
816
817impl From<SigningProtocol> for String {
818    fn from(value: SigningProtocol) -> Self {
819        match value {
820            SigningProtocol::SigV4 => "sigv4".to_string(),
821        }
822    }
823}
824
825#[derive(Debug, Clone)]
826pub enum OriginAccessControlType {
827    S3,
828    MediaStore,
829    Lambda,
830    MediaPackageV2,
831}
832
833impl From<OriginAccessControlType> for String {
834    fn from(value: OriginAccessControlType) -> Self {
835        match value {
836            OriginAccessControlType::S3 => "s3".to_string(),
837            OriginAccessControlType::MediaStore => "mediastore".to_string(),
838            OriginAccessControlType::Lambda => "lambda".to_string(),
839            OriginAccessControlType::MediaPackageV2 => "mediapackagev2".to_string(),
840        }
841    }
842}
843
844/// Builder for CloudFront Origin Access Control.
845///
846/// Controls access from CloudFront to origins like S3 buckets.
847pub struct OriginAccessControlBuilder {
848    id: Id,
849    name: String,
850    origin_access_control_type: OriginAccessControlType,
851    signing_behavior: SigningBehavior,
852    signing_protocol: SigningProtocol,
853}
854
855impl OriginAccessControlBuilder {
856    /// Creates a new CloudFront Origin Access Control builder.
857    ///
858    /// # Arguments
859    /// * `id` - Unique identifier for the origin access control
860    /// * `name` - Name of the origin access control
861    /// * `origin_access_control_type` - Type of origin (S3, MediaStore, Lambda, etc.)
862    /// * `signing_behavior` - When to sign requests (Never, NoOverride, Always)
863    /// * `signing_protocol` - Protocol for signing requests
864    pub fn new(
865        id: &str,
866        name: &str,
867        origin_access_control_type: OriginAccessControlType,
868        signing_behavior: SigningBehavior,
869        signing_protocol: SigningProtocol,
870    ) -> Self {
871        Self {
872            id: Id(id.to_string()),
873            name: name.to_string(),
874            origin_access_control_type,
875            signing_behavior,
876            signing_protocol,
877        }
878    }
879
880    pub fn build(self, stack_builder: &mut StackBuilder) -> OriginAccessControlRef {
881        let resource_id = Resource::generate_id("OAC");
882        stack_builder.add_resource(OriginAccessControl {
883            id: self.id,
884            resource_id: resource_id.clone(),
885            r#type: OriginAccessControlDtoType::OriginAccessControlType,
886            properties: OriginControlProperties {
887                config: OriginAccessControlConfig {
888                    name: self.name,
889                    origin_access_control_type: self.origin_access_control_type.into(),
890                    signing_behavior: self.signing_behavior.into(),
891                    signing_protocol: self.signing_protocol.into(),
892                },
893            },
894        });
895        OriginAccessControlRef::internal_new(resource_id)
896    }
897}
898
899/// Builder for CloudFront distributions.
900///
901/// Creates a CloudFront distribution with origins, cache behaviors, and other configuration.
902///
903/// # Example
904///
905/// ```rust,no_run
906/// use rusty_cdk_core::stack::StackBuilder;
907/// use rusty_cdk_core::cloudfront::{DistributionBuilder, OriginBuilder, DefaultCacheBehaviorBuilder};
908/// use rusty_cdk_core::s3::BucketBuilder;
909/// use rusty_cdk_core::wrappers::*;
910///
911/// let mut stack_builder = StackBuilder::new();
912///
913/// let bucket = unimplemented!("create a bucket");
914/// let oac = unimplemented!("create an origin access control");
915/// let policy = unimplemented!("create an origin");
916/// let viewer_protocol_policy = unimplemented!("create a viewer protocol");
917///
918/// let origin = OriginBuilder::new("my-origin").s3_origin(&bucket, &oac, None).build();
919/// let cache_behavior = DefaultCacheBehaviorBuilder::new(&origin, &policy, viewer_protocol_policy).build();
920///
921/// let distribution = DistributionBuilder::new("my-distribution", cache_behavior)
922///     .origins(vec![origin])
923///     .build(&mut stack_builder);
924/// ```
925pub struct DistributionBuilder<T: DistributionState> {
926    phantom_data: PhantomData<T>,
927    id: Id,
928    enabled: bool,
929    default_cache_behavior: DefaultCacheBehavior,
930    price_class: Option<String>,
931    http_version: Option<String>,
932    aliases: Option<Vec<String>>,
933    cnames: Option<Vec<String>>,
934    ipv6_enabled: Option<bool>,
935    viewer_certificate: Option<ViewerCertificate>,
936    cache_behaviors: Option<Vec<CacheBehavior>>,
937    default_root_object: Option<String>,
938    // TODO add. and either this or the next is required!
939    // origin_groups: Option<OriginGroups>,
940    origins: Option<Vec<Origin>>,
941}
942
943impl DistributionBuilder<DistributionStartState> {
944    /// Creates a new CloudFront distribution builder.
945    ///
946    /// # Arguments
947    /// * `id` - Unique identifier for the distribution
948    /// * `default_cache_behavior` - Default cache behavior for all requests
949    pub fn new(id: &str, default_cache_behavior: DefaultCacheBehavior) -> Self {
950        Self {
951            phantom_data: Default::default(),
952            id: Id(id.to_string()),
953            enabled: true,
954            default_cache_behavior,
955            aliases: None,
956            cache_behaviors: None,
957            cnames: None,
958            default_root_object: None,
959            http_version: None,
960            ipv6_enabled: None,
961            origins: None,
962            price_class: None,
963            viewer_certificate: None,
964        }
965    }
966
967    pub fn origins(self, origins: Vec<Origin>) -> DistributionBuilder<DistributionOriginState> {
968        DistributionBuilder {
969            phantom_data: Default::default(),
970            origins: Some(origins),
971            id: self.id,
972            enabled: self.enabled,
973            default_cache_behavior: self.default_cache_behavior,
974            price_class: self.price_class,
975            http_version: self.http_version,
976            aliases: self.aliases,
977            cnames: self.cnames,
978            ipv6_enabled: self.ipv6_enabled,
979            viewer_certificate: self.viewer_certificate,
980            cache_behaviors: self.cache_behaviors,
981            default_root_object: self.default_root_object,
982        }
983    }
984}
985
986impl DistributionBuilder<DistributionOriginState> {
987    pub fn build(mut self, stack_builder: &mut StackBuilder) -> DistributionRef {
988        let mut origins = self.origins.take().expect("origins to be present in distribution origin state");
989        let resource_id = Resource::generate_id("CloudFrontDistribution");
990
991        origins
992            .iter_mut()
993            .filter(|o| o.s3_bucket_policy.is_some())
994            .map(|s3| {
995                let mut policy = s3
996                    .s3_bucket_policy
997                    .take()
998                    .expect("just checked that this was present, only need to use it this one time");
999                let distro_id = get_att(&resource_id, "Id");
1000                let source_arn_value = join(
1001                    "",
1002                    vec![
1003                        Value::String("arn:aws:cloudfront::".to_string()),
1004                        get_ref(AWS_ACCOUNT_PSEUDO_PARAM),
1005                        Value::String(":distribution/".to_string()),
1006                        distro_id,
1007                    ],
1008                );
1009                let distro_condition = json!({
1010                    "StringEquals": {
1011                        "AWS:SourceArn": source_arn_value
1012                    }
1013                });
1014                policy
1015                    .properties
1016                    .policy_document
1017                    .statements
1018                    .iter_mut()
1019                    .for_each(|v| v.condition = Some(distro_condition.clone()));
1020                policy
1021            })
1022            .for_each(|p| {
1023                stack_builder.add_resource(p);
1024            });
1025
1026        self.origins = Some(origins);
1027
1028        self.build_internal(resource_id, stack_builder)
1029    }
1030}
1031
1032impl<T: DistributionState> DistributionBuilder<T> {
1033    pub fn add_cache_behavior(mut self, behavior: CacheBehavior) -> Self {
1034        if let Some(mut behaviors) = self.cache_behaviors {
1035            behaviors.push(behavior);
1036            self.cache_behaviors = Some(behaviors);
1037        } else {
1038            self.cache_behaviors = Some(vec![behavior])
1039        }
1040        self
1041    }
1042
1043    pub fn aliases(self, aliases: Vec<String>) -> Self {
1044        Self {
1045            aliases: Some(aliases),
1046            ..self
1047        }
1048    }
1049
1050    // could have a regex for this?
1051    pub fn cnames(self, cnames: Vec<String>) -> Self {
1052        Self {
1053            cnames: Some(cnames),
1054            ..self
1055        }
1056    }
1057
1058    pub fn price_class(self, price_class: PriceClass) -> Self {
1059        Self {
1060            price_class: Some(price_class.into()),
1061            ..self
1062        }
1063    }
1064
1065    pub fn http_version(self, http_version: HttpVersion) -> Self {
1066        Self {
1067            http_version: Some(http_version.into()),
1068            ..self
1069        }
1070    }
1071
1072    pub fn ipv6_enabled(self, enabled: bool) -> Self {
1073        Self {
1074            ipv6_enabled: Some(enabled),
1075            ..self
1076        }
1077    }
1078
1079    pub fn viewer_certificate(self, viewer_certificate: ViewerCertificate) -> Self {
1080        Self {
1081            viewer_certificate: Some(viewer_certificate),
1082            ..self
1083        }
1084    }
1085
1086    pub fn enabled(self, enabled: bool) -> Self {
1087        Self { enabled, ..self }
1088    }
1089
1090    pub fn default_root_object(self, default: DefaultRootObject) -> Self {
1091        Self {
1092            default_root_object: Some(default.0),
1093            ..self
1094        }
1095    }
1096
1097    fn build_internal(self, resource_id: String, stack_builder: &mut StackBuilder) -> DistributionRef {
1098        let config = DistributionConfig {
1099            enabled: self.enabled,
1100            default_cache_behavior: self.default_cache_behavior,
1101            aliases: self.aliases,
1102            cache_behaviors: self.cache_behaviors,
1103            cnames: self.cnames,
1104            default_root_object: self.default_root_object.unwrap_or_default(),
1105            http_version: self.http_version,
1106            ipv6_enabled: self.ipv6_enabled,
1107            price_class: self.price_class,
1108            viewer_certificate: self.viewer_certificate,
1109            origins: self.origins,
1110            origin_groups: None,
1111        };
1112        stack_builder.add_resource(Distribution {
1113            id: self.id,
1114            resource_id: resource_id.clone(),
1115            r#type: DistributionType::DistributionType,
1116            properties: DistributionProperties { config },
1117        });
1118
1119        DistributionRef::internal_new(resource_id)
1120    }
1121}