Skip to main content

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