1#![allow(unused_comparisons)]
2mod 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#[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#[proc_macro]
82pub fn string_with_only_alphanumerics_underscores_and_hyphens(input: TokenStream) -> TokenStream {
83 let output: LitStr = syn::parse(input).unwrap();
84 let value = output.value();
85
86 let requirements = StringRequirements::not_empty_with_allowed_chars(vec!['_', '-']);
87
88 match validate_string(&value, requirements) {
89 Ok(()) => quote!(
90 StringWithOnlyAlphaNumericsUnderscoresAndHyphens(#value.to_string())
91 ),
92 Err(e) => Error::new(output.span(), e).into_compile_error(),
93 }.into()
94}
95
96#[proc_macro]
107pub fn string_with_only_alphanumerics_and_hyphens(input: TokenStream) -> TokenStream {
108 let output: LitStr = syn::parse(input).unwrap();
109 let value = output.value();
110
111 let requirements = StringRequirements::not_empty_with_allowed_chars(vec!['-']);
112
113 match validate_string(&value, requirements) {
114 Ok(()) => quote!(
115 StringWithOnlyAlphaNumericsAndHyphens(#value.to_string())
116 ),
117 Err(e) => Error::new(output.span(), e).into_compile_error(),
118 }.into()
119}
120
121#[proc_macro]
132pub fn app_sync_api_name(input: TokenStream) -> TokenStream {
133 let output: LitStr = syn::parse(input).unwrap();
134 let value = output.value();
135 let requirements = StringRequirements::not_empty_with_allowed_chars(vec!['-', '_', ' ']).with_max_length(50);
136
137 match validate_string(&value, requirements) {
138 Ok(()) => quote!(
139 AppSyncApiName(#value.to_string())
140 ),
141 Err(e) => Error::new(output.span(), e).into_compile_error(),
142 }.into()
143}
144
145#[proc_macro]
146pub fn schedule_name(input: TokenStream) -> TokenStream {
147 let output: LitStr = syn::parse(input).unwrap();
148 let value = output.value();
149 let requirements = StringRequirements::not_empty_with_allowed_chars(vec!['-', '_', '.']).with_max_length(64);
150
151 match validate_string(&value, requirements) {
152 Ok(()) => quote!(
153 ScheduleName(#value.to_string())
154 ),
155 Err(e) => Error::new(output.span(), e).into_compile_error(),
156 }.into()
157}
158
159#[proc_macro]
170pub fn channel_namespace_name(input: TokenStream) -> TokenStream {
171 let output: LitStr = syn::parse(input).unwrap();
172 let value = output.value();
173 let requirements = StringRequirements::not_empty_with_allowed_chars(vec!['-']).with_max_length(50);
174
175 match validate_string(&value, requirements) {
176 Ok(()) => quote!(
177 ChannelNamespaceName(#value.to_string())
178 ),
179 Err(e) => Error::new(output.span(), e).into_compile_error(),
180 }.into()
181}
182
183#[proc_macro]
193pub fn string_for_secret(input: TokenStream) -> TokenStream {
194 let output: LitStr = syn::parse(input).unwrap();
195 let value = output.value();
196
197 let requirements = StringRequirements::not_empty_with_allowed_chars(vec!['/', '_', '+', '=', '.', '@', '-']);
198
199 match validate_string(&value, requirements) {
200 Ok(()) => quote!(
201 StringForSecret(#value.to_string())
202 ),
203 Err(e) => Error::new(output.span(), e).into_compile_error(),
204 }.into()
205}
206
207#[proc_macro]
215pub fn env_var_key(input: TokenStream) -> TokenStream {
216 let output: LitStr = syn::parse(input).unwrap();
217 let value = output.value();
218
219 if value.len() < 2 {
220 return Error::new(output.span(), "env var key should be at least two characters long".to_string())
221 .into_compile_error()
222 .into();
223 }
224
225 if value.get(0..1).expect("just checked that length is at least 2") == "_" {
226 return Error::new(output.span(), "env var key should not start with an underscore".to_string())
227 .into_compile_error()
228 .into();
229 }
230
231 if value.chars().any(|c| !c.is_alphanumeric() && c != '_') {
232 return Error::new(
233 output.span(),
234 "env var key should only contain alphanumeric characters and underscores".to_string(),
235 )
236 .into_compile_error()
237 .into();
238 }
239
240 quote!(
241 EnvVarKey(#value.to_string())
242 )
243 .into()
244}
245
246#[proc_macro]
259pub fn zip_file(input: TokenStream) -> TokenStream {
260 let output: syn::Result<LitStr> = syn::parse(input);
261
262 let output = match output {
263 Ok(output) => output,
264 Err(_) => {
265 return Error::new(Span::call_site(), "zip_file macro should contain value".to_string())
266 .into_compile_error()
267 .into();
268 }
269 };
270
271 let value = output.value();
272
273 if !value.ends_with(".zip") {
274 return Error::new(output.span(), format!("zip should end with `.zip` (found `{value}`)"))
275 .into_compile_error()
276 .into();
277 }
278
279 let value = match get_absolute_file_path(&value) {
280 Ok(v) => v,
281 Err(e) => {
282 return Error::new(output.span(), e).into_compile_error().into();
283 }
284 };
285
286 quote!(
287 ZipFile(#value.to_string())
288 )
289 .into()
290}
291
292#[proc_macro]
303pub fn toml_file(input: TokenStream) -> TokenStream {
304 let output: syn::Result<LitStr> = syn::parse(input);
305
306 let output = match output {
307 Ok(output) => output,
308 Err(_) => {
309 return Error::new(Span::call_site(), "toml_file macro should contain value".to_string())
310 .into_compile_error()
311 .into();
312 }
313 };
314
315 let value = output.value();
316
317 if !value.ends_with(".toml") {
318 return Error::new(output.span(), format!("toml file should end with `.toml` (found `{value}`)"))
319 .into_compile_error()
320 .into();
321 }
322
323 let value = match get_absolute_file_path(&value) {
324 Ok(v) => v,
325 Err(e) => {
326 return Error::new(output.span(), e).into_compile_error().into();
327 }
328 };
329
330 quote!(
331 TomlFile(#value.to_string())
332 )
333 .into()
334}
335
336#[proc_macro]
338pub fn non_zero_number(input: TokenStream) -> TokenStream {
339 let output = match syn::parse::<LitInt>(input) {
340 Ok(v) => v,
341 Err(_) => {
342 return Error::new(Span::call_site(), "value is not a valid number".to_string())
343 .into_compile_error()
344 .into();
345 }
346 };
347
348 let as_number: syn::Result<u32> = output.base10_parse();
349
350 let num = if let Ok(num) = as_number {
351 if num == 0 {
352 return Error::new(output.span(), "value should not be null".to_string())
353 .into_compile_error()
354 .into();
355 }
356 num
357 } else {
358 return Error::new(output.span(), "value is not a valid u32 number".to_string())
359 .into_compile_error()
360 .into();
361 };
362
363 quote!(
364 NonZeroNumber(#num)
365 )
366 .into()
367}
368
369macro_rules! number_check {
370 ($name:ident,$min:literal,$max:literal,$output:ident,$type:ty) => {
371 #[doc = "Checks whether the value that will be wrapped in the "]
372 #[doc = stringify!($output)]
373 #[doc = "struct is between "]
374 #[doc = stringify!($min)]
375 #[doc = "and "]
376 #[doc = stringify!($max)]
377 #[proc_macro]
378 pub fn $name(input: TokenStream) -> TokenStream {
379 let output: LitInt = syn::parse(input).unwrap();
380
381 let as_number: syn::Result<$type> = output.base10_parse();
382
383 if let Ok(num) = as_number {
384 if num < $min {
385 Error::new(output.span(), format!("value should be at least {}", $min)).into_compile_error().into()
386 } else if num > $max {
387 Error::new(output.span(), format!("value should be at most {}", $max)).into_compile_error().into()
388 } else {
389 quote!(
390 $output(#num)
391 ).into()
392 }
393 } else {
394 Error::new(output.span(), "value is not a valid number".to_string()).into_compile_error().into()
395 }
396 }
397 }
398}
399
400number_check!(memory, 128, 10240, Memory, u16);
401number_check!(timeout, 1, 900, Timeout, u16);
402number_check!(delay_seconds, 0, 900, DelaySeconds, u16);
403number_check!(maximum_message_size, 1024, 1048576, MaximumMessageSize, u32);
404number_check!(message_retention_period, 60, 1209600, MessageRetentionPeriod, u32);
405number_check!(visibility_timeout, 0, 43200, VisibilityTimeout, u32);
406number_check!(receive_message_wait_time, 0, 20, ReceiveMessageWaitTime, u8);
407number_check!(sqs_event_source_max_concurrency, 2, 1000, SqsEventSourceMaxConcurrency, u16);
408number_check!(connection_attempts, 1, 3, ConnectionAttempts, u8);
409number_check!(s3_origin_read_timeout, 1, 120, S3OriginReadTimeout, u8);
410number_check!(deployment_duration_in_minutes, 0, 1440, DeploymentDurationInMinutes, u16);
411number_check!(growth_factor, 0, 100, GrowthFactor, u8);
412number_check!(record_expiration_days, 7, 2147483647, RecordExpirationDays, u32);
413number_check!(retry_policy_event_age, 60, 86400, RetryPolicyEventAge, u32);
414number_check!(retry_policy_retries, 0, 185, RetryPolicyRetries, u8);
415number_check!(max_flexible_time_window, 1, 1440, MaxFlexibleTimeWindow, u16);
416
417const NO_REMOTE_OVERRIDE_ENV_VAR_NAME: &str = "RUSTY_CDK_NO_REMOTE";
418const RUSTY_CDK_RECHECK_ENV_VAR_NAME: &str = "RUSTY_CDK_RECHECK";
419
420#[proc_macro]
445pub fn bucket(input: TokenStream) -> TokenStream {
446 let input: LitStr = syn::parse(input).unwrap();
447 let value = input.value();
448
449 if value.starts_with("arn:") {
450 return Error::new(input.span(), "value is an arn, not a bucket name".to_string())
451 .into_compile_error()
452 .into();
453 }
454
455 if value.starts_with("s3:") {
456 return Error::new(input.span(), "value has s3 prefix, should be plain bucket name".to_string())
457 .into_compile_error()
458 .into();
459 }
460
461 let no_remote_check_wanted = env::var(NO_REMOTE_OVERRIDE_ENV_VAR_NAME)
462 .ok()
463 .and_then(|v| v.parse().ok())
464 .unwrap_or(false);
465
466 if no_remote_check_wanted {
467 return bucket::bucket_output(value);
468 }
469
470 let rechecked_wanted = env::var(RUSTY_CDK_RECHECK_ENV_VAR_NAME)
471 .ok()
472 .and_then(|v| v.parse().ok())
473 .unwrap_or(false);
474
475 if !rechecked_wanted {
476 match bucket::valid_bucket_according_to_file_storage(&value) {
477 bucket::FileStorageOutput::Valid => {
478 return bucket::bucket_output(value)
479 }
480 bucket::FileStorageOutput::Invalid => {
481 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()
482 }
483 bucket::FileStorageOutput::Unknown => {}
484 }
485 }
486
487 let rt = tokio::runtime::Runtime::new().unwrap();
488
489 match rt.block_on(bucket::find_bucket(input.clone())) {
490 Ok(_) => {
491 bucket::update_file_storage(bucket::FileStorageInput::Valid(&value));
492 bucket::bucket_output(value)
493 }
494 Err(e) => {
495 bucket::update_file_storage(bucket::FileStorageInput::Invalid(&value));
496 e.into_compile_error().into()
497 }
498 }
499}
500
501const ADDITIONAL_ALLOWED_FOR_BUCKET_NAME: [char; 2] = ['.', '-'];
502
503#[proc_macro]
529pub fn bucket_name(input: TokenStream) -> TokenStream {
530 let input: LitStr = syn::parse(input).unwrap();
531 let value = input.value();
532
533 if value.chars().any(|c| c.is_uppercase()) {
534 return Error::new(input.span(), "value contains uppercase letters".to_string())
535 .into_compile_error()
536 .into();
537 }
538
539 if value
540 .chars()
541 .any(|c| !c.is_alphanumeric() && !ADDITIONAL_ALLOWED_FOR_BUCKET_NAME.contains(&c))
542 {
543 return Error::new(
544 input.span(),
545 "value should contain only letters, numbers, periods and dashes".to_string(),
546 )
547 .into_compile_error()
548 .into();
549 }
550
551 let no_remote_check_wanted = env::var(NO_REMOTE_OVERRIDE_ENV_VAR_NAME)
552 .ok()
553 .and_then(|v| v.parse().ok())
554 .unwrap_or(false);
555
556 if no_remote_check_wanted {
557 return bucket_name::bucket_name_output(value);
558 }
559
560 let rechecked_wanted = env::var(RUSTY_CDK_RECHECK_ENV_VAR_NAME)
561 .ok()
562 .and_then(|v| v.parse().ok())
563 .unwrap_or(false);
564
565 if !rechecked_wanted {
566 match bucket_name::valid_bucket_name_according_to_file_storage(&value) {
567 bucket_name::FileStorageOutput::Valid => {
568 return bucket_name::bucket_name_output(value)
569 }
570 bucket_name::FileStorageOutput::Invalid => {
571 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()
572 }
573 bucket_name::FileStorageOutput::Unknown => {}
574 }
575 }
576
577 match bucket_name::check_bucket_name(input) {
578 Ok(_) => {
579 bucket_name::update_file_storage(bucket_name::FileStorageInput::Valid(&value));
580 bucket_name::bucket_name_output(value)
581 }
582 Err(e) => {
583 bucket_name::update_file_storage(bucket_name::FileStorageInput::Invalid(&value));
584 e.into_compile_error().into()
585 }
586 }
587}
588
589const POSSIBLE_LOG_RETENTION_VALUES: [u16; 22] = [
590 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653,
591];
592
593#[proc_macro]
599pub fn log_retention(input: TokenStream) -> TokenStream {
600 let output = match syn::parse::<LitInt>(input) {
601 Ok(v) => v,
602 Err(_) => {
603 return Error::new(Span::call_site(), "value is not a valid number".to_string())
604 .into_compile_error()
605 .into();
606 }
607 };
608
609 let as_number: syn::Result<u16> = output.base10_parse();
610
611 if let Ok(num) = as_number {
612 if POSSIBLE_LOG_RETENTION_VALUES.contains(&num) {
613 quote! {
614 RetentionInDays(#num)
615 }
616 .into()
617 } else {
618 Error::new(output.span(), format!("value should be one of {:?}", POSSIBLE_LOG_RETENTION_VALUES))
619 .into_compile_error()
620 .into()
621 }
622 } else {
623 Error::new(output.span(), "value is not a valid u16 number".to_string())
624 .into_compile_error()
625 .into()
626 }
627}
628
629const ADDITIONAL_ALLOWED_FOR_LOG_GROUP: [char; 6] = ['.', '-', '_', '#', '/', '\\'];
630
631#[proc_macro]
639pub fn log_group_name(input: TokenStream) -> TokenStream {
640 let output: LitStr = syn::parse(input).unwrap();
641 let value = output.value();
642
643 if value.is_empty() {
644 return Error::new(output.span(), "value should not be blank".to_string())
645 .into_compile_error()
646 .into();
647 }
648
649 if value.len() > 512 {
650 return Error::new(output.span(), "value should not be longer than 512 chars".to_string())
651 .into_compile_error()
652 .into();
653 }
654
655 if value
656 .chars()
657 .any(|c| !c.is_alphanumeric() && !ADDITIONAL_ALLOWED_FOR_LOG_GROUP.contains(&c))
658 {
659 return Error::new(
660 output.span(),
661 format!(
662 "value should only contain alphanumeric characters and {:?}",
663 ADDITIONAL_ALLOWED_FOR_LOG_GROUP
664 ),
665 )
666 .into_compile_error()
667 .into();
668 }
669
670 quote!(
671 LogGroupName(#value.to_string())
672 )
673 .into()
674}
675
676#[proc_macro]
690pub fn iam_action(input: TokenStream) -> TokenStream {
691 let output: LitStr = syn::parse(input).unwrap();
692 let value = output.value();
693 let validator = PermissionValidator::new();
694
695 match validator.is_valid_action(&value) {
696 ValidationResponse::Valid => quote!(
697 IamAction(#value.to_string())
698 ),
699 ValidationResponse::Invalid(message) => Error::new(output.span(), message).into_compile_error()
700 }.into()
701}
702
703#[proc_macro]
714pub fn lifecycle_object_sizes(input: TokenStream) -> TokenStream {
715 let ObjectSizes { first, second } = parse_macro_input!(input);
716
717 if first.is_some() && second.is_some() && first.unwrap() > second.unwrap() {
719 return Error::new(
720 Span::call_site(),
721 format!(
722 "first number ({}) in `lifecycle_object_sizes` should be smaller than second ({})",
723 first.unwrap(),
724 second.unwrap()
725 ),
726 )
727 .into_compile_error()
728 .into();
729 }
730
731 let first_output = if let Some(first) = first {
732 quote!(Some(#first))
733 } else {
734 quote!(None)
735 };
736
737 let second_output = if let Some(second) = second {
738 quote!(Some(#second))
739 } else {
740 quote!(None)
741 };
742
743 quote!(S3LifecycleObjectSizes(#first_output, #second_output)).into()
744}
745
746#[proc_macro]
756pub fn origin_path(input: TokenStream) -> TokenStream {
757 let output: LitStr = syn::parse(input).unwrap();
758 let value = output.value();
759
760 if !value.starts_with("/") || value.ends_with("/") {
761 return Error::new(
762 value.span(),
763 format!("origin path should start with a / and should not end with / (but got {})", value),
764 )
765 .into_compile_error()
766 .into();
767 }
768
769 quote! {
770 OriginPath(#value)
771 }
772 .into()
773}
774
775#[proc_macro]
785pub fn default_root_object(input: TokenStream) -> TokenStream {
786 let output: LitStr = syn::parse(input).unwrap();
787 let value = output.value();
788
789 if value.starts_with("/") || value.ends_with("/") {
790 return Error::new(value.span(), "default root object should not start with /".to_string())
791 .into_compile_error()
792 .into();
793 }
794
795 quote! {
796 DefaultRootObject(#value)
797 }
798 .into()
799}
800
801#[proc_macro]
809pub fn cf_connection_timeout(input: TokenStream) -> TokenStream {
810 let Timeouts { first, second } = parse_macro_input!(input);
811
812 if let Some(first) = first {
813 if first > 10 {
814 return Error::new(
815 Span::call_site(),
816 format!("connection timeout was {} but should be between 1 and 10", first),
817 )
818 .into_compile_error()
819 .into();
820 } else if let Some(second) = second && second < first {
821 return Error::new(
822 Span::call_site(),
823 format!(
824 "response completion timeout was {} but should be larger than connection timeout ({})",
825 second, first
826 ),
827 )
828 .into_compile_error()
829 .into();
830 }
831 }
832
833 let first_output = if let Some(first) = first {
834 quote!(Some(#first))
835 } else {
836 quote!(None)
837 };
838
839 let second_output = if let Some(second) = second {
840 quote!(Some(#second))
841 } else {
842 quote!(None)
843 };
844
845 quote!(CfConnectionTimeout(#first_output, #second_output)).into()
846}
847
848#[proc_macro]
859pub fn lambda_permission_action(input: TokenStream) -> TokenStream {
860 let output: LitStr = syn::parse(input).unwrap();
861 let value = output.value();
862 let requirements = StringRequirements::not_empty_prefix("lambda");
863
864 match validate_string(&value, requirements) {
865 Ok(()) => quote!(
866 LambdaPermissionAction(#value.to_string())
867 ),
868 Err(e) => Error::new(output.span(), e).into_compile_error(),
869 }.into()
870}
871
872#[proc_macro]
883pub fn app_config_name(input: TokenStream) -> TokenStream {
884 let output: LitStr = syn::parse(input).unwrap();
885 let value = output.value();
886
887 if value.is_empty() || value.len() > 64 {
888 return Error::new(
889 Span::call_site(),
890 "app config name should be between 1 and 64 chars in length".to_string(),
891 )
892 .into_compile_error()
893 .into();
894 }
895
896 quote!(AppConfigName(#value.to_string())).into()
897}
898
899const LIFECYCLE_STORAGE_TYPES: [&str; 6] = [
900 "IntelligentTiering",
901 "OneZoneIA",
902 "StandardIA",
903 "GlacierDeepArchive",
904 "Glacier",
905 "GlacierInstantRetrieval",
906];
907const LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS: [&str; 2] = ["OneZoneIA", "StandardIA"];
908
909#[proc_macro]
917pub fn lifecycle_transition_in_days(input: TokenStream) -> TokenStream {
918 let TransitionInfo { days, service } = parse_macro_input!(input);
919 let service = service.trim();
920
921 if !LIFECYCLE_STORAGE_TYPES.contains(&service) {
922 return Error::new(
923 Span::call_site(),
924 format!("service should be one of {} (was {})", LIFECYCLE_STORAGE_TYPES.join(","), service),
925 )
926 .into_compile_error()
927 .into();
928 } else if LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS.contains(&service) && days <= 30 {
929 return Error::new(
930 Span::call_site(),
931 format!(
932 "service of type {} cannot have transition under 30 days",
933 LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS.join(" or ")
934 ),
935 )
936 .into_compile_error()
937 .into();
938 }
939
940 quote!(LifecycleTransitionInDays(#days)).into()
941}
942
943const ACCESS_TIERS: [&str; 2] = ["ARCHIVE_ACCESS", "DEEP_ARCHIVE_ACCESS"];
944
945#[proc_macro]
946pub fn bucket_tiering(input: TokenStream) -> TokenStream {
947 let BucketTiering { access_tier, days } = parse_macro_input!(input);
948
949 if !ACCESS_TIERS.contains(&access_tier.as_str()) {
950 return Error::new(
951 Span::call_site(),
952 format!("access tier should be one of {} (was {})", ACCESS_TIERS.join(","), access_tier),
953 )
954 .into_compile_error()
955 .into();
956 }
957
958 if &access_tier == "ARCHIVE_ACCESS" {
959 if days < 90 || days > 730 {
960 return Error::new(Span::call_site(), format!("days for access tier `ARCHIVE_ACCESS` should be between 90 and 730 (was {})", days))
961 .into_compile_error()
962 .into();
963 }
964 } else if &access_tier == "DEEP_ARCHIVE_ACCESS" {
965 if days < 180 || days > 730 {
966 return Error::new(Span::call_site(), format!("days for access tier `DEEP_ARCHIVE_ACCESS` should be between 180 and 730 (was {})", days))
967 .into_compile_error()
968 .into();
969 }
970 }
971
972 quote!(BucketTiering(#access_tier.to_string(), #days)).into()
973}
974
975const LOCATION_URI_TYPES: [&str; 4] = ["hosted", "codepipeline", "secretsmanager", "s3"];
976const LOCATION_URI_CODEPIPELINE_START: &str = "codepipeline://";
977const LOCATION_URI_SECRETS_MANAGER_START: &str = "secretsmanager://";
978const LOCATION_URI_S3_START: &str = "s3://";
979
980#[proc_macro]
988pub fn location_uri(input: TokenStream) -> TokenStream {
989 let LocationUri {
990 location_uri_type,
991 content,
992 } = parse_macro_input!(input);
993 let location_uri_type = location_uri_type.trim();
994
995 #[allow(unused)] let mut error = None;
997
998 if !LOCATION_URI_TYPES.contains(&location_uri_type) {
999 error = Some(format!(
1000 "unrecognized location uri {}, should be one of {}",
1001 location_uri_type,
1002 LOCATION_URI_TYPES.join(",")
1003 ));
1004 } else {
1005 if location_uri_type == "hosted" {
1006 return quote! {
1007 LocationUri(#location_uri_type.to_string())
1008 }
1009 .into();
1010 } else if content.is_none() {
1011 error = Some(format!("location uri of type {}, should have content", location_uri_type));
1012 } else {
1013 let content = content.expect("just checked that this is present");
1014
1015 if location_uri_type == "codepipeline" && !content.starts_with(LOCATION_URI_CODEPIPELINE_START) {
1016 error = Some(format!(
1017 "content of type codepipeline should start with {}",
1018 LOCATION_URI_CODEPIPELINE_START
1019 ));
1020 } else if location_uri_type == "secretsmanager" && !content.starts_with(LOCATION_URI_SECRETS_MANAGER_START) {
1021 error = Some(format!(
1022 "content of type secretsmanager should start with {}",
1023 LOCATION_URI_SECRETS_MANAGER_START
1024 ));
1025 } else if location_uri_type == "s3" && !content.starts_with(LOCATION_URI_S3_START) {
1026 error = Some(format!("content of type s3 should start with {}", LOCATION_URI_S3_START));
1027 } else {
1028 return quote! {
1029 LocationUri(#content.to_string())
1030 }
1031 .into();
1032 }
1033 }
1034 }
1035
1036 Error::new(
1037 Span::call_site(),
1038 error.unwrap_or_else(|| "unknown error".to_string()),
1039 )
1040 .into_compile_error()
1041 .into()
1042}
1043
1044const RATE_UNITS: [&str; 3] = ["minutes", "hours", "days"];
1045
1046#[proc_macro]
1047pub fn schedule_rate_expression(input: TokenStream) -> TokenStream {
1048 let RateExpression {
1049 value, unit
1050 } = parse_macro_input!(input);
1051
1052 if !RATE_UNITS.contains(&unit.as_str()) {
1053 return Error::new(Span::call_site(), format!("unit of at expression should be one of {} (was {})", RATE_UNITS.join(","), unit))
1054 .into_compile_error()
1055 .into();
1056 }
1057
1058 if value == 0 {
1059 return Error::new(Span::call_site(), "rate value should be a positive number bigger than 0")
1060 .into_compile_error()
1061 .into();
1062 }
1063
1064 quote!(ScheduleRateExpression(#value, #unit.to_string())).into()
1065}
1066
1067#[proc_macro]
1068pub fn schedule_cron_expression(input: TokenStream) -> TokenStream {
1069 let output: LitStr = syn::parse(input).unwrap();
1070 let value = output.value();
1071
1072 match validate_cron(&value) {
1073 Ok(()) => quote!(
1074 ScheduleCronExpression(#value.to_string())
1075 ),
1076 Err(e) => Error::new(output.span(), e).into_compile_error(),
1077 }.into()
1078}
1079
1080#[proc_macro]
1081pub fn schedule_at_expression(input: TokenStream) -> TokenStream {
1082 let output: LitStr = syn::parse(input).unwrap();
1083 let value = output.value();
1084
1085 match validate_at(&value) {
1086 Ok(()) => quote!(
1087 ScheduleAtExpression(#value.to_string())
1088 ),
1089 Err(e) => Error::new(output.span(), e).into_compile_error(),
1090 }.into()
1091}
1092
1093#[proc_macro]
1094pub fn policy_name(input: TokenStream) -> TokenStream {
1095 let output: LitStr = syn::parse(input).unwrap();
1096 let value = output.value();
1097
1098 let requirements = StringRequirements::not_empty_with_allowed_chars(vec!['_', '+', '=', ',', '.', '@', '-', ';']).with_max_length(128);
1099
1100 match validate_string(&value, requirements) {
1101 Ok(()) => quote!(
1102 PolicyName(#value.to_string())
1103 ),
1104 Err(e) => Error::new(output.span(), e).into_compile_error()
1105 }.into()
1106}