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]
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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[proc_macro]
739pub fn lifecycle_object_sizes(input: TokenStream) -> TokenStream {
740 let ObjectSizes { first, second } = parse_macro_input!(input);
741
742 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#[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! {
795 OriginPath(#value)
796 }
797 .into()
798}
799
800#[proc_macro]
810pub fn default_root_object(input: TokenStream) -> TokenStream {
811 let output: LitStr = syn::parse(input).unwrap();
812 let value = output.value();
813
814 if value.starts_with("/") || value.ends_with("/") {
815 return Error::new(value.span(), "default root object should not start with /".to_string())
816 .into_compile_error()
817 .into();
818 }
819
820 quote! {
821 DefaultRootObject(#value)
822 }
823 .into()
824}
825
826#[proc_macro]
834pub fn cf_connection_timeout(input: TokenStream) -> TokenStream {
835 let Timeouts { first, second } = parse_macro_input!(input);
836
837 if let Some(first) = first {
838 if first > 10 {
839 return Error::new(
840 Span::call_site(),
841 format!("connection timeout was {} but should be between 1 and 10", first),
842 )
843 .into_compile_error()
844 .into();
845 } else if let Some(second) = second && second < first {
846 return Error::new(
847 Span::call_site(),
848 format!(
849 "response completion timeout was {} but should be larger than connection timeout ({})",
850 second, first
851 ),
852 )
853 .into_compile_error()
854 .into();
855 }
856 }
857
858 let first_output = if let Some(first) = first {
859 quote!(Some(#first))
860 } else {
861 quote!(None)
862 };
863
864 let second_output = if let Some(second) = second {
865 quote!(Some(#second))
866 } else {
867 quote!(None)
868 };
869
870 quote!(CfConnectionTimeout(#first_output, #second_output)).into()
871}
872
873#[proc_macro]
884pub fn lambda_permission_action(input: TokenStream) -> TokenStream {
885 let output: LitStr = syn::parse(input).unwrap();
886 let value = output.value();
887 let requirements = StringRequirements::not_empty_prefix("lambda");
888
889 match validate_string(&value, requirements) {
890 Ok(()) => quote!(
891 LambdaPermissionAction(#value.to_string())
892 ),
893 Err(e) => Error::new(output.span(), e).into_compile_error(),
894 }.into()
895}
896
897#[proc_macro]
908pub fn app_config_name(input: TokenStream) -> TokenStream {
909 let output: LitStr = syn::parse(input).unwrap();
910 let value = output.value();
911
912 if value.is_empty() || value.len() > 64 {
913 return Error::new(
914 Span::call_site(),
915 "app config name should be between 1 and 64 chars in length".to_string(),
916 )
917 .into_compile_error()
918 .into();
919 }
920
921 quote!(AppConfigName(#value.to_string())).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#[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!(LifecycleTransitionInDays(#days)).into()
966}
967
968const ACCESS_TIERS: [&str; 2] = ["ARCHIVE_ACCESS", "DEEP_ARCHIVE_ACCESS"];
969
970#[proc_macro]
971pub fn bucket_tiering(input: TokenStream) -> TokenStream {
972 let BucketTiering { access_tier, days } = parse_macro_input!(input);
973
974 if !ACCESS_TIERS.contains(&access_tier.as_str()) {
975 return Error::new(
976 Span::call_site(),
977 format!("access tier should be one of {} (was {})", ACCESS_TIERS.join(","), access_tier),
978 )
979 .into_compile_error()
980 .into();
981 }
982
983 if &access_tier == "ARCHIVE_ACCESS" {
984 if days < 90 || days > 730 {
985 return Error::new(Span::call_site(), format!("days for access tier `ARCHIVE_ACCESS` should be between 90 and 730 (was {})", days))
986 .into_compile_error()
987 .into();
988 }
989 } else if &access_tier == "DEEP_ARCHIVE_ACCESS" {
990 if days < 180 || days > 730 {
991 return Error::new(Span::call_site(), format!("days for access tier `DEEP_ARCHIVE_ACCESS` should be between 180 and 730 (was {})", days))
992 .into_compile_error()
993 .into();
994 }
995 }
996
997 quote!(BucketTiering(#access_tier.to_string(), #days)).into()
998}
999
1000const LOCATION_URI_TYPES: [&str; 4] = ["hosted", "codepipeline", "secretsmanager", "s3"];
1001const LOCATION_URI_CODEPIPELINE_START: &str = "codepipeline://";
1002const LOCATION_URI_SECRETS_MANAGER_START: &str = "secretsmanager://";
1003const LOCATION_URI_S3_START: &str = "s3://";
1004
1005#[proc_macro]
1013pub fn location_uri(input: TokenStream) -> TokenStream {
1014 let LocationUri {
1015 location_uri_type,
1016 content,
1017 } = parse_macro_input!(input);
1018 let location_uri_type = location_uri_type.trim();
1019
1020 #[allow(unused)] let mut error = None;
1022
1023 if !LOCATION_URI_TYPES.contains(&location_uri_type) {
1024 error = Some(format!(
1025 "unrecognized location uri {}, should be one of {}",
1026 location_uri_type,
1027 LOCATION_URI_TYPES.join(",")
1028 ));
1029 } else {
1030 if location_uri_type == "hosted" {
1031 return quote! {
1032 LocationUri(#location_uri_type.to_string())
1033 }
1034 .into();
1035 } else if content.is_none() {
1036 error = Some(format!("location uri of type {}, should have content", location_uri_type));
1037 } else {
1038 let content = content.expect("just checked that this is present");
1039
1040 if location_uri_type == "codepipeline" && !content.starts_with(LOCATION_URI_CODEPIPELINE_START) {
1041 error = Some(format!(
1042 "content of type codepipeline should start with {}",
1043 LOCATION_URI_CODEPIPELINE_START
1044 ));
1045 } else if location_uri_type == "secretsmanager" && !content.starts_with(LOCATION_URI_SECRETS_MANAGER_START) {
1046 error = Some(format!(
1047 "content of type secretsmanager should start with {}",
1048 LOCATION_URI_SECRETS_MANAGER_START
1049 ));
1050 } else if location_uri_type == "s3" && !content.starts_with(LOCATION_URI_S3_START) {
1051 error = Some(format!("content of type s3 should start with {}", LOCATION_URI_S3_START));
1052 } else {
1053 return quote! {
1054 LocationUri(#content.to_string())
1055 }
1056 .into();
1057 }
1058 }
1059 }
1060
1061 Error::new(
1062 Span::call_site(),
1063 error.unwrap_or_else(|| "unknown error".to_string()),
1064 )
1065 .into_compile_error()
1066 .into()
1067}
1068
1069const RATE_UNITS: [&str; 3] = ["minutes", "hours", "days"];
1070
1071#[proc_macro]
1072pub fn schedule_rate_expression(input: TokenStream) -> TokenStream {
1073 let RateExpression {
1074 value, unit
1075 } = parse_macro_input!(input);
1076
1077 if !RATE_UNITS.contains(&unit.as_str()) {
1078 return Error::new(Span::call_site(), format!("unit of at expression should be one of {} (was {})", RATE_UNITS.join(","), unit))
1079 .into_compile_error()
1080 .into();
1081 }
1082
1083 if value == 0 {
1084 return Error::new(Span::call_site(), "rate value should be a positive number bigger than 0")
1085 .into_compile_error()
1086 .into();
1087 }
1088
1089 quote!(ScheduleRateExpression(#value, #unit.to_string())).into()
1090}
1091
1092#[proc_macro]
1093pub fn schedule_cron_expression(input: TokenStream) -> TokenStream {
1094 let output: LitStr = syn::parse(input).unwrap();
1095 let value = output.value();
1096
1097 match validate_cron(&value) {
1098 Ok(()) => quote!(
1099 ScheduleCronExpression(#value.to_string())
1100 ),
1101 Err(e) => Error::new(output.span(), e).into_compile_error(),
1102 }.into()
1103}
1104
1105#[proc_macro]
1106pub fn schedule_at_expression(input: TokenStream) -> TokenStream {
1107 let output: LitStr = syn::parse(input).unwrap();
1108 let value = output.value();
1109
1110 match validate_at(&value) {
1111 Ok(()) => quote!(
1112 ScheduleAtExpression(#value.to_string())
1113 ),
1114 Err(e) => Error::new(output.span(), e).into_compile_error(),
1115 }.into()
1116}
1117
1118#[proc_macro]
1119pub fn policy_name(input: TokenStream) -> TokenStream {
1120 let output: LitStr = syn::parse(input).unwrap();
1121 let value = output.value();
1122
1123 let requirements = StringRequirements::not_empty_with_allowed_chars(vec!['_', '+', '=', ',', '.', '@', '-', ';']).with_max_length(128);
1124
1125 match validate_string(&value, requirements) {
1126 Ok(()) => quote!(
1127 PolicyName(#value.to_string())
1128 ),
1129 Err(e) => Error::new(output.span(), e).into_compile_error()
1130 }.into()
1131}