rusty_cdk_macros/
lib.rs

1#![allow(unused_comparisons)]
2//! This crate provides compile-time validation macros for AWS cloud infrastructure configuration.
3//! These macros ensure type safety and enforce AWS service limits at build time, preventing
4//! runtime errors from invalid configurations.
5//!
6//! ## Overview
7//!
8//! All macros perform validation at compile time and generate wrapper types that encapsulate
9//! validated values.
10//!
11//! The macros always return a newtype 'wrapper'.
12//! You should import those from the rusty_cdk::wrappers directory, as seen in the below example.
13//!
14//! ## Usage Example
15//!
16//! ```rust,compile_fail
17//! use rusty_cdk::wrappers::Memory; // import the wrapper
18//! use rusty_cdk::memory;
19//!
20//! // Lambda memory configuration with validated limit
21//! let mem = memory!(512);        // 512 MB (128-10240 range)
22//! ```
23
24mod bucket;
25mod bucket_name;
26mod file_util;
27mod iam_validation;
28mod location_uri;
29mod object_sizes;
30mod strings;
31mod timeouts;
32mod transition_in_days;
33mod bucket_tiering;
34
35use crate::file_util::get_absolute_file_path;
36use crate::iam_validation::{PermissionValidator, ValidationResponse};
37use crate::location_uri::LocationUri;
38use crate::object_sizes::ObjectSizes;
39use crate::strings::{check_string_requirements, StringRequirements};
40use crate::timeouts::Timeouts;
41use crate::transition_in_days::TransitionInfo;
42use proc_macro::TokenStream;
43use quote::__private::Span;
44use quote::quote;
45use std::env;
46use syn::spanned::Spanned;
47use syn::{parse_macro_input, Error, LitInt, LitStr};
48use crate::bucket_tiering::BucketTiering;
49
50/// Creates a validated `StringWithOnlyAlphaNumericsAndUnderscores` wrapper at compile time.
51///
52/// # Validation Rules
53///
54/// - String must not be empty
55/// - Only alphanumeric characters, and underscores are allowed
56#[proc_macro]
57pub fn string_with_only_alphanumerics_and_underscores(input: TokenStream) -> TokenStream {
58    let output: LitStr = syn::parse(input).unwrap();
59    let value = output.value();
60
61    let requirements = StringRequirements::not_empty_allowed_chars(vec!['_']);
62
63    match check_string_requirements(&value, output.span(), requirements) {
64        None => quote!(
65            StringWithOnlyAlphaNumericsAndUnderscores(#value.to_string())
66        )
67        .into(),
68        Some(e) => e.into_compile_error().into(),
69    }
70}
71
72/// Creates a validated `StringWithOnlyAlphaNumericsUnderscoresAndHyphens` wrapper at compile time.
73///
74/// # Validation Rules
75///
76/// - String must not be empty
77/// - Only alphanumeric characters, underscores, and hyphens are allowed
78#[proc_macro]
79pub fn string_with_only_alphanumerics_underscores_and_hyphens(input: TokenStream) -> TokenStream {
80    let output: LitStr = syn::parse(input).unwrap();
81    let value = output.value();
82
83    let requirements = StringRequirements::not_empty_allowed_chars(vec!['_', '-']);
84
85    match check_string_requirements(&value, output.span(), requirements) {
86        None => quote!(
87            StringWithOnlyAlphaNumericsUnderscoresAndHyphens(#value.to_string())
88        )
89        .into(),
90        Some(e) => e.into_compile_error().into(),
91    }
92}
93
94/// Creates a validated `StringWithOnlyAlphaNumericsUnderscoresAndHyphens` wrapper at compile time.
95///
96/// This macro ensures that the input string contains only alphanumeric characters (a-z, A-Z, 0-9),
97/// underscores (_), and hyphens (-). It's designed for creating safe identifiers for AWS resources
98/// that allow hyphens in their naming conventions.
99///
100/// # Validation Rules
101///
102/// - String must not be empty
103/// - Only alphanumeric characters, underscores, and hyphens are allowed
104#[proc_macro]
105pub fn string_with_only_alphanumerics_and_hyphens(input: TokenStream) -> TokenStream {
106    let output: LitStr = syn::parse(input).unwrap();
107    let value = output.value();
108
109    let requirements = StringRequirements::not_empty_allowed_chars(vec!['-']);
110
111    match check_string_requirements(&value, output.span(), requirements) {
112        None => quote!(
113            StringWithOnlyAlphaNumericsAndHyphens(#value.to_string())
114        )
115        .into(),
116        Some(e) => e.into_compile_error().into(),
117    }
118}
119
120/// Creates a validated `AppSyncApiName` wrapper for AppSync Api names at compile time.
121///
122/// This macro ensures that the input string is a valid name for AppSync Apis,
123/// following AWS naming conventions and character restrictions.
124///
125/// # Validation Rules
126///
127/// - String must not be empty
128/// - Only alphanumeric characters, and the following special characters are allowed: _, - and whitespace
129/// - Max length 50 characters
130#[proc_macro]
131pub fn app_sync_api_name(input: TokenStream) -> TokenStream {
132    let output: LitStr = syn::parse(input).unwrap();
133    let value = output.value();
134
135    if value.len() > 50 {
136        return Error::new(output.span(), "name cannot be longer than 50 characters".to_string())
137            .into_compile_error()
138            .into();
139    }
140
141    let requirements = StringRequirements::not_empty_allowed_chars(vec!['-', '_', ' ']);
142
143    match check_string_requirements(&value, output.span(), requirements) {
144        None => quote!(
145            AppSyncApiName(#value.to_string())
146        )
147            .into(),
148        Some(e) => e.into_compile_error().into(),
149    }
150}
151
152/// Creates a validated `ChannelNamespaceName` wrapper for AppSync Api at compile time.
153///
154/// This macro ensures that the input string is a valid name for a Channel Namespace,
155/// following AWS naming conventions and character restrictions.
156///
157/// # Validation Rules
158///
159/// - String must not be empty
160/// - Only alphanumeric characters, and the following special characters are allowed: -
161/// - Max length 50 characters
162#[proc_macro]
163pub fn channel_namespace_name(input: TokenStream) -> TokenStream {
164    let output: LitStr = syn::parse(input).unwrap();
165    let value = output.value();
166
167    if value.len() > 50 {
168        return Error::new(output.span(), "name cannot be longer than 50 characters".to_string())
169            .into_compile_error()
170            .into();
171    }
172
173    let requirements = StringRequirements::not_empty_allowed_chars(vec!['-']);
174
175    match check_string_requirements(&value, output.span(), requirements) {
176        None => quote!(
177            ChannelNamespaceName(#value.to_string())
178        )
179            .into(),
180        Some(e) => e.into_compile_error().into(),
181    }
182}
183
184/// Creates a validated `StringForSecret` wrapper for AWS Secrets Manager secret names at compile time.
185///
186/// This macro ensures that the input string is a valid name for AWS Secrets Manager secrets,
187/// following AWS naming conventions and character restrictions.
188///
189/// # Validation Rules
190///
191/// - String must not be empty
192/// - Only alphanumeric characters, and the following special characters are allowed: /, _, +, =, ., @, -
193#[proc_macro]
194pub fn string_for_secret(input: TokenStream) -> TokenStream {
195    let output: LitStr = syn::parse(input).unwrap();
196    let value = output.value();
197
198    let requirements = StringRequirements::not_empty_allowed_chars(vec!['/', '_', '+', '=', '.', '@', '-']);
199
200    match check_string_requirements(&value, output.span(), requirements) {
201        None => quote!(
202            StringForSecret(#value.to_string())
203        )
204        .into(),
205        Some(e) => e.into_compile_error().into(),
206    }
207}
208
209/// Creates a validated `EnvVarKey` wrapper for AWS Lambda environment variable keys at compile time.
210///
211/// # Validation Rules
212///
213/// - Key must be at least 2 characters long
214/// - Cannot start with an underscore (_)
215/// - Only alphanumeric characters and underscores are allowed
216#[proc_macro]
217pub fn env_var_key(input: TokenStream) -> TokenStream {
218    let output: LitStr = syn::parse(input).unwrap();
219    let value = output.value();
220
221    if value.len() < 2 {
222        return Error::new(output.span(), "env var key should be at least two characters long".to_string())
223            .into_compile_error()
224            .into();
225    }
226
227    if value.get(0..1).expect("just checked that length is at least 2") == "_" {
228        return Error::new(output.span(), "env var key should not start with an underscore".to_string())
229            .into_compile_error()
230            .into();
231    }
232
233    if value.chars().any(|c| !c.is_alphanumeric() && c != '_') {
234        return Error::new(
235            output.span(),
236            "env var key should only contain alphanumeric characters and underscores".to_string(),
237        )
238        .into_compile_error()
239        .into();
240    }
241
242    quote!(
243        EnvVarKey(#value.to_string())
244    )
245    .into()
246}
247
248/// Creates a validated `ZipFile` wrapper for AWS Lambda deployment packages at compile time.
249///
250/// This macro ensures that the input string refers to a valid ZIP file that exists on the filesystem at compile time.
251///
252/// See the `examples` dir of this library for some usage examples
253///
254/// # Validation Rules
255///
256/// - Path must end with `.zip` extension
257/// - File must exist at compile time
258/// - Path must be valid Unicode
259/// - Both relative and absolute paths are allowed
260#[proc_macro]
261pub fn zip_file(input: TokenStream) -> TokenStream {
262    let output: syn::Result<LitStr> = syn::parse(input);
263
264    let output = match output {
265        Ok(output) => output,
266        Err(_) => {
267            return Error::new(Span::call_site(), "zip_file macro should contain value".to_string())
268                .into_compile_error()
269                .into();
270        }
271    };
272
273    let value = output.value();
274
275    if !value.ends_with(".zip") {
276        return Error::new(output.span(), format!("zip should end with `.zip` (found `{value}`)"))
277            .into_compile_error()
278            .into();
279    }
280
281    let value = match get_absolute_file_path(&value) {
282        Ok(v) => v,
283        Err(e) => {
284            return Error::new(output.span(), e).into_compile_error().into();
285        }
286    };
287
288    quote!(
289        ZipFile(#value.to_string())
290    )
291    .into()
292}
293
294/// Creates a validated `TomlFile` wrapper.
295///
296/// See the `examples` dir of this library for some usage examples
297///
298/// # Validation Rules
299///
300/// - Path must end with `.toml` extension
301/// - File must exist at compile time
302/// - Path must be valid Unicode
303/// - Both relative and absolute paths are allowed
304#[proc_macro]
305pub fn toml_file(input: TokenStream) -> TokenStream {
306    let output: syn::Result<LitStr> = syn::parse(input);
307
308    let output = match output {
309        Ok(output) => output,
310        Err(_) => {
311            return Error::new(Span::call_site(), "toml_file macro should contain value".to_string())
312                .into_compile_error()
313                .into();
314        }
315    };
316
317    let value = output.value();
318
319    if !value.ends_with(".toml") {
320        return Error::new(output.span(), format!("toml file should end with `.toml` (found `{value}`)"))
321            .into_compile_error()
322            .into();
323    }
324
325    let value = match get_absolute_file_path(&value) {
326        Ok(v) => v,
327        Err(e) => {
328            return Error::new(output.span(), e).into_compile_error().into();
329        }
330    };
331
332    quote!(
333        TomlFile(#value.to_string())
334    )
335    .into()
336}
337
338/// Creates a validated `NonZeroNumber` wrapper for positive integers at compile time.
339#[proc_macro]
340pub fn non_zero_number(input: TokenStream) -> TokenStream {
341    let output = match syn::parse::<LitInt>(input) {
342        Ok(v) => v,
343        Err(_) => {
344            return Error::new(Span::call_site(), "value is not a valid number".to_string())
345                .into_compile_error()
346                .into();
347        }
348    };
349
350    let as_number: syn::Result<u32> = output.base10_parse();
351
352    let num = if let Ok(num) = as_number {
353        if num == 0 {
354            return Error::new(output.span(), "value should not be null".to_string())
355                .into_compile_error()
356                .into();
357        }
358        num
359    } else {
360        return Error::new(output.span(), "value is not a valid u32 number".to_string())
361            .into_compile_error()
362            .into();
363    };
364
365    quote!(
366        NonZeroNumber(#num)
367    )
368    .into()
369}
370
371macro_rules! number_check {
372    ($name:ident,$min:literal,$max:literal,$output:ident,$type:ty) => {
373        #[doc = "Checks whether the value that will be wrapped in the "]
374		#[doc = stringify!($output)]
375		#[doc = "struct is between "]
376		#[doc = stringify!($min)]
377		#[doc = "and "]
378        #[doc = stringify!($max)]
379        #[proc_macro]
380        pub fn $name(input: TokenStream) -> TokenStream {
381            let output: LitInt = syn::parse(input).unwrap();
382
383            let as_number: syn::Result<$type> = output.base10_parse();
384
385            if let Ok(num) = as_number {
386                if num < $min {
387                    Error::new(output.span(), format!("value should be at least {}", $min)).into_compile_error().into()
388                } else if num > $max {
389                    Error::new(output.span(), format!("value should be at most {}", $max)).into_compile_error().into()
390                } else {
391                    quote!(
392                        $output(#num)
393                    ).into()
394                }
395            } else {
396                Error::new(output.span(), "value is not a valid number".to_string()).into_compile_error().into()
397            }
398        }
399    }
400}
401
402number_check!(memory, 128, 10240, Memory, u16);
403number_check!(timeout, 1, 900, Timeout, u16);
404number_check!(delay_seconds, 0, 900, DelaySeconds, u16);
405number_check!(maximum_message_size, 1024, 1048576, MaximumMessageSize, u32);
406number_check!(message_retention_period, 60, 1209600, MessageRetentionPeriod, u32);
407number_check!(visibility_timeout, 0, 43200, VisibilityTimeout, u32);
408number_check!(receive_message_wait_time, 0, 20, ReceiveMessageWaitTime, u8);
409number_check!(sqs_event_source_max_concurrency, 2, 1000, SqsEventSourceMaxConcurrency, u16);
410number_check!(connection_attempts, 1, 3, ConnectionAttempts, u8);
411number_check!(s3_origin_read_timeout, 1, 120, S3OriginReadTimeout, u8);
412number_check!(deployment_duration_in_minutes, 0, 1440, DeploymentDurationInMinutes, u16);
413number_check!(growth_factor, 0, 100, GrowthFactor, u8);
414number_check!(record_expiration_days, 7, 2147483647, RecordExpirationDays, u32);
415
416const NO_REMOTE_OVERRIDE_ENV_VAR_NAME: &str = "RUSTY_CDK_NO_REMOTE";
417const RUSTY_CDK_RECHECK_ENV_VAR_NAME: &str = "RUSTY_CDK_RECHECK";
418
419/// Creates a validated `Bucket` wrapper for existing AWS S3 bucket references at compile time.
420///
421/// This macro ensures that the input string refers to an existing S3 bucket in your AWS account.
422/// It queries S3 to verify the bucket exists.
423///
424/// # Validation Rules
425///
426/// - Value must not be an ARN (cannot start with "arn:")
427/// - Value must not include the "s3:" prefix
428/// - Bucket must exist in your AWS account (verified at compile time)
429///
430/// # Environment Variables
431///
432/// - `rusty_cdk_NO_REMOTE`: Set to `true` to skip remote AWS checks (for offline development)
433/// - `rusty_cdk_RECHECK`: Set to `true` to force revalidation of cached bucket names
434///
435/// # Note
436///
437/// This macro caches validation results to improve compile times. The first compilation will
438/// query AWS to verify the bucket exists. Later compilations will use the cached result unless `rusty_cdk_RECHECK` is set to true.
439///
440/// # Override
441///
442/// You can avoid this verification by using the wrapper directly, but you lose all the above compile time guarantees by doing so.
443#[proc_macro]
444pub fn bucket(input: TokenStream) -> TokenStream {
445    let input: LitStr = syn::parse(input).unwrap();
446    let value = input.value();
447
448    if value.starts_with("arn:") {
449        return Error::new(input.span(), "value is an arn, not a bucket name".to_string())
450            .into_compile_error()
451            .into();
452    }
453
454    if value.starts_with("s3:") {
455        return Error::new(input.span(), "value has s3 prefix, should be plain bucket name".to_string())
456            .into_compile_error()
457            .into();
458    }
459
460    let no_remote_check_wanted = env::var(NO_REMOTE_OVERRIDE_ENV_VAR_NAME)
461        .ok()
462        .and_then(|v| v.parse().ok())
463        .unwrap_or(false);
464
465    if no_remote_check_wanted {
466        return bucket::bucket_output(value);
467    }
468
469    let rechecked_wanted = env::var(RUSTY_CDK_RECHECK_ENV_VAR_NAME)
470        .ok()
471        .and_then(|v| v.parse().ok())
472        .unwrap_or(false);
473
474    if !rechecked_wanted {
475        match bucket::valid_bucket_according_to_file_storage(&value) {
476            bucket::FileStorageOutput::Valid => {
477                return bucket::bucket_output(value)
478            }
479            bucket::FileStorageOutput::Invalid => {
480                return Error::new(input.span(), format!("(cached) did not find bucket with name `{value}` in your account. You can rerun this check by adding setting the `{RUSTY_CDK_RECHECK_ENV_VAR_NAME}` env var to true")).into_compile_error().into()
481            }
482            bucket::FileStorageOutput::Unknown => {}
483        }
484    }
485
486    let rt = tokio::runtime::Runtime::new().unwrap();
487
488    match rt.block_on(bucket::find_bucket(input.clone())) {
489        Ok(_) => {
490            bucket::update_file_storage(bucket::FileStorageInput::Valid(&value));
491            bucket::bucket_output(value)
492        }
493        Err(e) => {
494            bucket::update_file_storage(bucket::FileStorageInput::Invalid(&value));
495            e.into_compile_error().into()
496        }
497    }
498}
499
500const ADDITIONAL_ALLOWED_FOR_BUCKET_NAME: [char; 2] = ['.', '-'];
501
502/// Creates a validated `BucketName` wrapper for new AWS S3 bucket names at compile time.
503///
504/// This macro ensures that the input string is a valid S3 bucket name that follows AWS naming
505/// requirements and verifies the name is available at compile time.
506///
507/// # Validation Rules
508///
509/// - Must contain only lowercase letters, numbers, periods (.), and hyphens (-)
510/// - No uppercase letters are allowed
511/// - Bucket name must be globally unique and available (verified at compile time)
512///
513/// # Environment Variables
514///
515/// - `RUSTY_CDK_NO_REMOTE`: Set to `true` to skip remote AWS checks (for offline development)
516/// - `RUSTY_CDK_RECHECK`: Set to `true` to force revalidation of cached bucket name availability
517///
518/// # Note
519///
520/// This macro caches validation results to improve compile times. The first compilation will
521/// query AWS to verify the bucket name is available. Later compilations will use the cached
522/// result unless `RUSTY_CDK_RECHECK` is set to true.
523///
524/// # Override
525///
526/// You can avoid this verification by using the wrapper directly, but you lose all the above compile time guarantees by doing so.
527#[proc_macro]
528pub fn bucket_name(input: TokenStream) -> TokenStream {
529    let input: LitStr = syn::parse(input).unwrap();
530    let value = input.value();
531
532    if value.chars().any(|c| c.is_uppercase()) {
533        return Error::new(input.span(), "value contains uppercase letters".to_string())
534            .into_compile_error()
535            .into();
536    }
537
538    if value
539        .chars()
540        .any(|c| !c.is_alphanumeric() && !ADDITIONAL_ALLOWED_FOR_BUCKET_NAME.contains(&c))
541    {
542        return Error::new(
543            input.span(),
544            "value should contain only letters, numbers, periods and dashes".to_string(),
545        )
546        .into_compile_error()
547        .into();
548    }
549
550    let no_remote_check_wanted = env::var(NO_REMOTE_OVERRIDE_ENV_VAR_NAME)
551        .ok()
552        .and_then(|v| v.parse().ok())
553        .unwrap_or(false);
554
555    if no_remote_check_wanted {
556        return bucket_name::bucket_name_output(value);
557    }
558
559    let rechecked_wanted = env::var(RUSTY_CDK_RECHECK_ENV_VAR_NAME)
560        .ok()
561        .and_then(|v| v.parse().ok())
562        .unwrap_or(false);
563
564    if !rechecked_wanted {
565        match bucket_name::valid_bucket_name_according_to_file_storage(&value) {
566            bucket_name::FileStorageOutput::Valid => {
567                return bucket_name::bucket_name_output(value)
568            }
569            bucket_name::FileStorageOutput::Invalid => {
570                return Error::new(input.span(), format!("(cached) bucket name is already taken. You can rerun this check by adding setting the `{RUSTY_CDK_RECHECK_ENV_VAR_NAME}` env var to true")).into_compile_error().into()
571            }
572            bucket_name::FileStorageOutput::Unknown => {}
573        }
574    }
575
576    match bucket_name::check_bucket_name(input) {
577        Ok(_) => {
578            bucket_name::update_file_storage(bucket_name::FileStorageInput::Valid(&value));
579            bucket_name::bucket_name_output(value)
580        }
581        Err(e) => {
582            bucket_name::update_file_storage(bucket_name::FileStorageInput::Invalid(&value));
583            e.into_compile_error().into()
584        }
585    }
586}
587
588const POSSIBLE_LOG_RETENTION_VALUES: [u16; 22] = [
589    1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653,
590];
591
592/// Creates a validated `RetentionInDays` wrapper for AWS CloudWatch Logs retention periods at compile time.
593///
594/// # Validation Rules
595///
596/// - Value must be a number, and of the AWS-approved retention periods (in days)
597#[proc_macro]
598pub fn log_retention(input: TokenStream) -> TokenStream {
599    let output = match syn::parse::<LitInt>(input) {
600        Ok(v) => v,
601        Err(_) => {
602            return Error::new(Span::call_site(), "value is not a valid number".to_string())
603                .into_compile_error()
604                .into();
605        }
606    };
607
608    let as_number: syn::Result<u16> = output.base10_parse();
609
610    if let Ok(num) = as_number {
611        if POSSIBLE_LOG_RETENTION_VALUES.contains(&num) {
612            quote! {
613                RetentionInDays(#num)
614            }
615            .into()
616        } else {
617            Error::new(output.span(), format!("value should be one of {:?}", POSSIBLE_LOG_RETENTION_VALUES))
618                .into_compile_error()
619                .into()
620        }
621    } else {
622        Error::new(output.span(), "value is not a valid u16 number".to_string())
623            .into_compile_error()
624            .into()
625    }
626}
627
628const ADDITIONAL_ALLOWED_FOR_LOG_GROUP: [char; 6] = ['.', '-', '_', '#', '/', '\\'];
629
630/// Creates a validated `LogGroupName` wrapper for AWS CloudWatch Logs log group names at compile time.
631///
632/// # Validation Rules
633///
634/// - String must not be empty
635/// - The maximum length is 512 characters
636/// - Only alphanumeric characters, and the following special characters are allowed: . - _ # / \
637#[proc_macro]
638pub fn log_group_name(input: TokenStream) -> TokenStream {
639    let output: LitStr = syn::parse(input).unwrap();
640    let value = output.value();
641
642    if value.is_empty() {
643        return Error::new(output.span(), "value should not be blank".to_string())
644            .into_compile_error()
645            .into();
646    }
647
648    if value.len() > 512 {
649        return Error::new(output.span(), "value should not be longer than 512 chars".to_string())
650            .into_compile_error()
651            .into();
652    }
653
654    if value
655        .chars()
656        .any(|c| !c.is_alphanumeric() && !ADDITIONAL_ALLOWED_FOR_LOG_GROUP.contains(&c))
657    {
658        return Error::new(
659            output.span(),
660            format!(
661                "value should only contain alphanumeric characters and {:?}",
662                ADDITIONAL_ALLOWED_FOR_LOG_GROUP
663            ),
664        )
665        .into_compile_error()
666        .into();
667    }
668
669    quote!(
670        LogGroupName(#value.to_string())
671    )
672    .into()
673}
674
675/// Creates a validated `IamAction` wrapper for AWS IAM permissions at compile time.
676///
677/// This macro ensures that the input string represents a valid AWS IAM action permission.
678/// It validates the action against a comprehensive list of AWS service permissions to catch
679/// typos and invalid permissions at compile time.
680///
681/// # Validation Rules
682///
683/// - String must not be empty
684/// - Action must be a valid AWS IAM action (e.g., "s3:GetObject", "s3:Put*")
685/// - Action is validated against AWS's official permission list
686/// - Wildcards are supported
687///
688#[proc_macro]
689pub fn iam_action(input: TokenStream) -> TokenStream {
690    let output: LitStr = syn::parse(input).unwrap();
691    let value = output.value();
692
693    if value.is_empty() {
694        return Error::new(output.span(), "value should not be blank".to_string())
695            .into_compile_error()
696            .into();
697    }
698
699    let validator = PermissionValidator::new();
700    match validator.is_valid_action(&value) {
701        ValidationResponse::Valid => quote!(
702            IamAction(#value.to_string())
703        )
704        .into(),
705        ValidationResponse::Invalid(message) => Error::new(output.span(), message).into_compile_error().into(),
706    }
707}
708
709/// Creates a validated `S3LifecycleObjectSizes` wrapper for S3 lifecycle rule object size constraints at compile time.
710///
711/// This macro defines minimum and maximum object sizes for S3 lifecycle transitions, allowing
712/// lifecycle rules to apply only to objects within a specific size range.
713///
714/// # Validation Rules
715///
716/// - Both minimum and maximum sizes are optional
717/// - If both are provided, the minimum must be smaller than the maximum
718/// - Values are specified in bytes
719#[proc_macro]
720pub fn lifecycle_object_sizes(input: TokenStream) -> TokenStream {
721    let ObjectSizes { first, second } = parse_macro_input!(input);
722
723    // replace with if let Some
724    if first.is_some() && second.is_some() && first.unwrap() > second.unwrap() {
725        return Error::new(
726            Span::call_site(),
727            format!(
728                "first number ({}) in `lifecycle_object_sizes` should be smaller than second ({})",
729                first.unwrap(),
730                second.unwrap()
731            ),
732        )
733        .into_compile_error()
734        .into();
735    }
736
737    let first_output = if let Some(first) = first {
738        quote! {
739            Some(#first)
740        }
741    } else {
742        quote! { None }
743    };
744
745    let second_output = if let Some(second) = second {
746        quote! {
747            Some(#second)
748        }
749    } else {
750        quote! { None }
751    };
752
753    quote! {
754        S3LifecycleObjectSizes(#first_output, #second_output)
755    }
756    .into()
757}
758
759/// Creates a validated `OriginPath` wrapper for CloudFront origin path prefixes at compile time.
760///
761/// This macro ensures that the path string follows CloudFront's requirements for origin paths, which are appended to requests forwarded to the origin.
762///
763/// # Validation Rules
764///
765/// - Must start with a forward slash (/)
766/// - Must NOT end with a forward slash (/)
767/// - Example: "/production" is valid, but "/production/" and "production" are not
768#[proc_macro]
769pub fn origin_path(input: TokenStream) -> TokenStream {
770    let output: LitStr = syn::parse(input).unwrap();
771    let value = output.value();
772
773    if !value.starts_with("/") || value.ends_with("/") {
774        return Error::new(
775            value.span(),
776            format!("origin path should start with a / and should not end with / (but got {})", value),
777        )
778        .into_compile_error()
779        .into();
780    }
781
782    quote! {
783        OriginPath(#value)
784    }
785    .into()
786}
787
788/// Creates a validated `DefaultRootObject` wrapper for CloudFront default root objects at compile time.
789///
790/// This macro ensures that the object name follows CloudFront's requirements for default root objects, which are returned when viewers request the root URL of a distribution.
791///
792/// # Validation Rules
793///
794/// - Must NOT start with a forward slash (/)
795/// - Must NOT end with a forward slash (/)
796/// - Example: "index.html" is valid, but "/index.html" and "index.html/" are not
797#[proc_macro]
798pub fn default_root_object(input: TokenStream) -> TokenStream {
799    let output: LitStr = syn::parse(input).unwrap();
800    let value = output.value();
801
802    if value.starts_with("/") || value.ends_with("/") {
803        return Error::new(value.span(), "default root object should not start with /".to_string())
804            .into_compile_error()
805            .into();
806    }
807
808    quote! {
809        DefaultRootObject(#value)
810    }
811    .into()
812}
813
814/// Creates a validated `CfConnectionTimeout` wrapper for CloudFront origin connection timeouts at compile time.
815///
816/// # Validation Rules
817///
818/// - Connection timeout (first value) must be between 1 and 10 seconds (if provided)
819/// - Response completion timeout (second value) must be greater than or equal to connection timeout (if both provided)
820/// - Both values are optional
821#[proc_macro]
822pub fn cf_connection_timeout(input: TokenStream) -> TokenStream {
823    let Timeouts { first, second } = parse_macro_input!(input);
824
825    if let Some(first) = first {
826        if first > 10 {
827            return Error::new(
828                Span::call_site(),
829                format!("connection timeout was {} but should be between 1 and 10", first),
830            )
831            .into_compile_error()
832            .into();
833        } else if let Some(second) = second && second < first {
834            return Error::new(
835                Span::call_site(),
836                format!(
837                    "response completion timeout was {} but should be larger than connection timeout ({})",
838                    second, first
839                ),
840            )
841            .into_compile_error()
842            .into();
843        }
844    }
845
846    let first_output = if let Some(first) = first {
847        quote! {
848            Some(#first)
849        }
850    } else {
851        quote! { None }
852    };
853
854    let second_output = if let Some(second) = second {
855        quote! {
856            Some(#second)
857        }
858    } else {
859        quote! { None }
860    };
861
862    quote! {
863        CfConnectionTimeout(#first_output, #second_output)
864    }
865    .into()
866}
867
868/// Creates a validated `LambdaPermissionAction` wrapper for Lambda resource-based policy actions at compile time.
869///
870/// This macro ensures that the action string is properly formatted for Lambda resource-based
871/// policies, which control what AWS services and accounts can invoke Lambda functions.
872///
873/// # Validation Rules
874///
875/// - String must not be empty
876/// - Must start with "lambda:" prefix
877/// - Common values include "lambda:InvokeFunction" and "lambda:GetFunction"
878#[proc_macro]
879pub fn lambda_permission_action(input: TokenStream) -> TokenStream {
880    let output: LitStr = syn::parse(input).unwrap();
881    let value = output.value();
882
883    let requirements = StringRequirements::not_empty_prefix("lambda");
884
885    match check_string_requirements(&value, output.span(), requirements) {
886        None => quote!(
887            LambdaPermissionAction(#value.to_string())
888        )
889        .into(),
890        Some(e) => e.into_compile_error().into(),
891    }
892}
893
894/// Creates a validated `AppConfigName` wrapper for AWS AppConfig resource names at compile time.
895///
896/// This macro ensures that the name string follows AWS AppConfig naming conventions and
897/// length restrictions for applications, environments, and configuration profiles.
898///
899/// # Validation Rules
900///
901/// - String must not be empty
902/// - Maximum length of 64 characters
903/// - Used for AppConfig application names, environment names, and configuration profile names
904#[proc_macro]
905pub fn app_config_name(input: TokenStream) -> TokenStream {
906    let output: LitStr = syn::parse(input).unwrap();
907    let value = output.value();
908
909    if value.is_empty() || value.len() > 64 {
910        return Error::new(
911            Span::call_site(),
912            "app config name should be between 1 and 64 chars in length".to_string(),
913        )
914        .into_compile_error()
915        .into();
916    }
917
918    quote! {
919        AppConfigName(#value.to_string())
920    }
921    .into()
922}
923
924const LIFECYCLE_STORAGE_TYPES: [&str; 6] = [
925    "IntelligentTiering",
926    "OneZoneIA",
927    "StandardIA",
928    "GlacierDeepArchive",
929    "Glacier",
930    "GlacierInstantRetrieval",
931];
932const LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS: [&str; 2] = ["OneZoneIA", "StandardIA"];
933
934/// Creates a validated `LifecycleTransitionInDays` wrapper for S3 lifecycle transition rules at compile time.
935///
936/// # Validation Rules
937///
938/// - Days must be a positive number
939/// - Storage class must be one of: IntelligentTiering, OneZoneIA, StandardIA, GlacierDeepArchive, Glacier, GlacierInstantRetrieval
940/// - OneZoneIA and StandardIA storage classes require at least 30 days (not allowed to transition sooner)
941#[proc_macro]
942pub fn lifecycle_transition_in_days(input: TokenStream) -> TokenStream {
943    let TransitionInfo { days, service } = parse_macro_input!(input);
944    let service = service.trim();
945
946    if !LIFECYCLE_STORAGE_TYPES.contains(&service) {
947        return Error::new(
948            Span::call_site(),
949            format!("service should be one of {} (was {})", LIFECYCLE_STORAGE_TYPES.join(","), service),
950        )
951        .into_compile_error()
952        .into();
953    } else if LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS.contains(&service) && days <= 30 {
954        return Error::new(
955            Span::call_site(),
956            format!(
957                "service of type {} cannot have transition under 30 days",
958                LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS.join(" or ")
959            ),
960        )
961        .into_compile_error()
962        .into();
963    }
964
965    quote! {
966        LifecycleTransitionInDays(#days)
967    }
968    .into()
969}
970
971const ACCESS_TIERS: [&str; 2] = ["ARCHIVE_ACCESS", "DEEP_ARCHIVE_ACCESS"];
972
973#[proc_macro]
974pub fn bucket_tiering(input: TokenStream) -> TokenStream {
975    let BucketTiering { access_tier, days } = parse_macro_input!(input);
976    
977    if !ACCESS_TIERS.contains(&access_tier.as_str()) {
978        return Error::new(
979            Span::call_site(),
980            format!("access tier should be one of {} (was {})", ACCESS_TIERS.join(","), access_tier),
981        )
982            .into_compile_error()
983            .into();
984    }
985    
986    if &access_tier == "ARCHIVE_ACCESS" {
987        if days < 90 || days > 730 {
988            return Error::new(Span::call_site(), format!("days for access tier `ARCHIVE_ACCESS` should be between 90 and 730 (was {})", days))
989                .into_compile_error()
990                .into();
991        }
992    } else if &access_tier == "DEEP_ARCHIVE_ACCESS" {
993        if days < 180 || days > 730 {
994            return Error::new(Span::call_site(), format!("days for access tier `DEEP_ARCHIVE_ACCESS` should be between 180 and 730 (was {})", days))
995                .into_compile_error()
996                .into();
997        }
998    }
999    
1000    quote! {
1001        BucketTiering(#access_tier.to_string(), #days)
1002    }
1003    .into()
1004}
1005
1006const LOCATION_URI_TYPES: [&str; 4] = ["hosted", "codepipeline", "secretsmanager", "s3"];
1007const LOCATION_URI_CODEPIPELINE_START: &str = "codepipeline://";
1008const LOCATION_URI_SECRETS_MANAGER_START: &str = "secretsmanager://";
1009const LOCATION_URI_S3_START: &str = "s3://";
1010
1011/// Creates a validated `LocationUri` wrapper for AppConfig
1012///
1013/// # Validation Rules
1014///
1015/// - Must be one of "hosted", "codepipeline", "secretsmanager", "s3"
1016/// - Hosted does not need an additional argument
1017/// - The other values require a second value, separated from the first by a comma
1018#[proc_macro]
1019pub fn location_uri(input: TokenStream) -> TokenStream {
1020    let LocationUri {
1021        location_uri_type,
1022        content,
1023    } = parse_macro_input!(input);
1024    let location_uri_type = location_uri_type.trim();
1025
1026    #[allow(unused)] // bug? is used at the end for the error?
1027    let mut error = None;
1028
1029    if !LOCATION_URI_TYPES.contains(&location_uri_type) {
1030        error = Some(format!(
1031            "unrecognized location uri {}, should be one of {}",
1032            location_uri_type,
1033            LOCATION_URI_TYPES.join(",")
1034        ));
1035    } else {
1036        if location_uri_type == "hosted" {
1037            return quote! {
1038                LocationUri(#location_uri_type.to_string())
1039            }
1040            .into();
1041        } else if content.is_none() {
1042            error = Some(format!("location uri of type {}, should have content", location_uri_type));
1043        } else {
1044            let content = content.expect("just checked that this is present");
1045
1046            if location_uri_type == "codepipeline" && !content.starts_with(LOCATION_URI_CODEPIPELINE_START) {
1047                error = Some(format!(
1048                    "content of type codepipeline should start with {}",
1049                    LOCATION_URI_CODEPIPELINE_START
1050                ));
1051            } else if location_uri_type == "secretsmanager" && !content.starts_with(LOCATION_URI_SECRETS_MANAGER_START) {
1052                error = Some(format!(
1053                    "content of type secretsmanager should start with {}",
1054                    LOCATION_URI_SECRETS_MANAGER_START
1055                ));
1056            } else if location_uri_type == "s3" && !content.starts_with(LOCATION_URI_S3_START) {
1057                error = Some(format!("content of type s3 should start with {}", LOCATION_URI_S3_START));
1058            } else {
1059                return quote! {
1060                    LocationUri(#content.to_string())
1061                }
1062                .into();
1063            }
1064        }
1065    }
1066
1067    Error::new(
1068        Span::call_site(),
1069        error.unwrap_or_else(|| "unknown error".to_string()),
1070    )
1071    .into_compile_error()
1072    .into()
1073}