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;
33
34use crate::file_util::get_absolute_file_path;
35use crate::iam_validation::{PermissionValidator, ValidationResponse};
36use crate::location_uri::LocationUri;
37use crate::object_sizes::ObjectSizes;
38use crate::strings::{check_string_requirements, StringRequirements};
39use crate::timeouts::Timeouts;
40use crate::transition_in_days::TransitionInfo;
41use proc_macro::TokenStream;
42use quote::__private::Span;
43use quote::quote;
44use std::env;
45use syn::spanned::Spanned;
46use syn::{parse_macro_input, Error, LitInt, LitStr};
47
48#[proc_macro]
55pub fn string_with_only_alphanumerics_and_underscores(input: TokenStream) -> TokenStream {
56 let output: LitStr = syn::parse(input).unwrap();
57 let value = output.value();
58
59 let requirements = StringRequirements::not_empty_allowed_chars(vec!['_']);
60
61 match check_string_requirements(&value, output.span(), requirements) {
62 None => quote!(
63 StringWithOnlyAlphaNumericsAndUnderscores(#value.to_string())
64 )
65 .into(),
66 Some(e) => e.into_compile_error().into(),
67 }
68}
69
70#[proc_macro]
77pub fn string_with_only_alphanumerics_underscores_and_hyphens(input: TokenStream) -> TokenStream {
78 let output: LitStr = syn::parse(input).unwrap();
79 let value = output.value();
80
81 let requirements = StringRequirements::not_empty_allowed_chars(vec!['_', '-']);
82
83 match check_string_requirements(&value, output.span(), requirements) {
84 None => quote!(
85 StringWithOnlyAlphaNumericsUnderscoresAndHyphens(#value.to_string())
86 )
87 .into(),
88 Some(e) => e.into_compile_error().into(),
89 }
90}
91
92#[proc_macro]
103pub fn string_with_only_alphanumerics_and_hyphens(input: TokenStream) -> TokenStream {
104 let output: LitStr = syn::parse(input).unwrap();
105 let value = output.value();
106
107 let requirements = StringRequirements::not_empty_allowed_chars(vec!['-']);
108
109 match check_string_requirements(&value, output.span(), requirements) {
110 None => quote!(
111 StringWithOnlyAlphaNumericsAndHyphens(#value.to_string())
112 )
113 .into(),
114 Some(e) => e.into_compile_error().into(),
115 }
116}
117
118#[proc_macro]
128pub fn string_for_secret(input: TokenStream) -> TokenStream {
129 let output: LitStr = syn::parse(input).unwrap();
130 let value = output.value();
131
132 let requirements = StringRequirements::not_empty_allowed_chars(vec!['/', '_', '+', '=', '.', '@', '-']);
133
134 match check_string_requirements(&value, output.span(), requirements) {
135 None => quote!(
136 StringForSecret(#value.to_string())
137 )
138 .into(),
139 Some(e) => e.into_compile_error().into(),
140 }
141}
142
143#[proc_macro]
151pub fn env_var_key(input: TokenStream) -> TokenStream {
152 let output: LitStr = syn::parse(input).unwrap();
153 let value = output.value();
154
155 if value.len() < 2 {
156 return Error::new(output.span(), "env var key should be at least two characters long".to_string())
157 .into_compile_error()
158 .into();
159 }
160
161 if value.get(0..1).expect("just checked that length is at least 2") == "_" {
162 return Error::new(output.span(), "env var key should not start with an underscore".to_string())
163 .into_compile_error()
164 .into();
165 }
166
167 if value.chars().any(|c| !c.is_alphanumeric() && c != '_') {
168 return Error::new(
169 output.span(),
170 "env var key should only contain alphanumeric characters and underscores".to_string(),
171 )
172 .into_compile_error()
173 .into();
174 }
175
176 quote!(
177 EnvVarKey(#value.to_string())
178 )
179 .into()
180}
181
182#[proc_macro]
195pub fn zip_file(input: TokenStream) -> TokenStream {
196 let output: syn::Result<LitStr> = syn::parse(input);
197
198 let output = match output {
199 Ok(output) => output,
200 Err(_) => {
201 return Error::new(Span::call_site(), "zip_file macro should contain value".to_string())
202 .into_compile_error()
203 .into();
204 }
205 };
206
207 let value = output.value();
208
209 if !value.ends_with(".zip") {
210 return Error::new(output.span(), format!("zip should end with `.zip` (found `{value}`)"))
211 .into_compile_error()
212 .into();
213 }
214
215 let value = match get_absolute_file_path(&value) {
216 Ok(v) => v,
217 Err(e) => {
218 return Error::new(output.span(), e).into_compile_error().into();
219 }
220 };
221
222 quote!(
223 ZipFile(#value.to_string())
224 )
225 .into()
226}
227
228#[proc_macro]
239pub fn toml_file(input: TokenStream) -> TokenStream {
240 let output: syn::Result<LitStr> = syn::parse(input);
241
242 let output = match output {
243 Ok(output) => output,
244 Err(_) => {
245 return Error::new(Span::call_site(), "toml_file macro should contain value".to_string())
246 .into_compile_error()
247 .into();
248 }
249 };
250
251 let value = output.value();
252
253 if !value.ends_with(".toml") {
254 return Error::new(output.span(), format!("toml file should end with `.toml` (found `{value}`)"))
255 .into_compile_error()
256 .into();
257 }
258
259 let value = match get_absolute_file_path(&value) {
260 Ok(v) => v,
261 Err(e) => {
262 return Error::new(output.span(), e).into_compile_error().into();
263 }
264 };
265
266 quote!(
267 TomlFile(#value.to_string())
268 )
269 .into()
270}
271
272#[proc_macro]
274pub fn non_zero_number(input: TokenStream) -> TokenStream {
275 let output = match syn::parse::<LitInt>(input) {
276 Ok(v) => v,
277 Err(_) => {
278 return Error::new(Span::call_site(), "value is not a valid number".to_string())
279 .into_compile_error()
280 .into();
281 }
282 };
283
284 let as_number: syn::Result<u32> = output.base10_parse();
285
286 let num = if let Ok(num) = as_number {
287 if num == 0 {
288 return Error::new(output.span(), "value should not be null".to_string())
289 .into_compile_error()
290 .into();
291 }
292 num
293 } else {
294 return Error::new(output.span(), "value is not a valid u32 number".to_string())
295 .into_compile_error()
296 .into();
297 };
298
299 quote!(
300 NonZeroNumber(#num)
301 )
302 .into()
303}
304
305macro_rules! number_check {
306 ($name:ident,$min:literal,$max:literal,$output:ident,$type:ty) => {
307 #[doc = "Checks whether the value that will be wrapped in the "]
308 #[doc = stringify!($output)]
309 #[doc = "struct is between "]
310 #[doc = stringify!($min)]
311 #[doc = "and "]
312 #[doc = stringify!($max)]
313 #[proc_macro]
314 pub fn $name(input: TokenStream) -> TokenStream {
315 let output: LitInt = syn::parse(input).unwrap();
316
317 let as_number: syn::Result<$type> = output.base10_parse();
318
319 if let Ok(num) = as_number {
320 if num < $min {
321 Error::new(output.span(), format!("value should be at least {}", $min)).into_compile_error().into()
322 } else if num > $max {
323 Error::new(output.span(), format!("value should be at most {}", $max)).into_compile_error().into()
324 } else {
325 quote!(
326 $output(#num)
327 ).into()
328 }
329 } else {
330 Error::new(output.span(), "value is not a valid number".to_string()).into_compile_error().into()
331 }
332 }
333 }
334}
335
336number_check!(memory, 128, 10240, Memory, u16);
337number_check!(timeout, 1, 900, Timeout, u16);
338number_check!(delay_seconds, 0, 900, DelaySeconds, u16);
339number_check!(maximum_message_size, 1024, 1048576, MaximumMessageSize, u32);
340number_check!(message_retention_period, 60, 1209600, MessageRetentionPeriod, u32);
341number_check!(visibility_timeout, 0, 43200, VisibilityTimeout, u32);
342number_check!(receive_message_wait_time, 0, 20, ReceiveMessageWaitTime, u8);
343number_check!(sqs_event_source_max_concurrency, 2, 1000, SqsEventSourceMaxConcurrency, u16);
344number_check!(connection_attempts, 1, 3, ConnectionAttempts, u8);
345number_check!(s3_origin_read_timeout, 1, 120, S3OriginReadTimeout, u8);
346number_check!(deployment_duration_in_minutes, 0, 1440, DeploymentDurationInMinutes, u16);
347number_check!(growth_factor, 0, 100, GrowthFactor, u8);
348
349const NO_REMOTE_OVERRIDE_ENV_VAR_NAME: &str = "RUSTY_CDK_NO_REMOTE";
350const RUSTY_CDK_RECHECK_ENV_VAR_NAME: &str = "RUSTY_CDK_RECHECK";
351
352#[proc_macro]
377pub fn bucket(input: TokenStream) -> TokenStream {
378 let input: LitStr = syn::parse(input).unwrap();
379 let value = input.value();
380
381 if value.starts_with("arn:") {
382 return Error::new(input.span(), "value is an arn, not a bucket name".to_string())
383 .into_compile_error()
384 .into();
385 }
386
387 if value.starts_with("s3:") {
388 return Error::new(input.span(), "value has s3 prefix, should be plain bucket name".to_string())
389 .into_compile_error()
390 .into();
391 }
392
393 let no_remote_check_wanted = env::var(NO_REMOTE_OVERRIDE_ENV_VAR_NAME)
394 .ok()
395 .and_then(|v| v.parse().ok())
396 .unwrap_or(false);
397
398 if no_remote_check_wanted {
399 return bucket::bucket_output(value);
400 }
401
402 let rechecked_wanted = env::var(RUSTY_CDK_RECHECK_ENV_VAR_NAME)
403 .ok()
404 .and_then(|v| v.parse().ok())
405 .unwrap_or(false);
406
407 if !rechecked_wanted {
408 match bucket::valid_bucket_according_to_file_storage(&value) {
409 bucket::FileStorageOutput::Valid => {
410 return bucket::bucket_output(value)
411 }
412 bucket::FileStorageOutput::Invalid => {
413 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()
414 }
415 bucket::FileStorageOutput::Unknown => {}
416 }
417 }
418
419 let rt = tokio::runtime::Runtime::new().unwrap();
420
421 match rt.block_on(bucket::find_bucket(input.clone())) {
422 Ok(_) => {
423 bucket::update_file_storage(bucket::FileStorageInput::Valid(&value));
424 bucket::bucket_output(value)
425 }
426 Err(e) => {
427 bucket::update_file_storage(bucket::FileStorageInput::Invalid(&value));
428 e.into_compile_error().into()
429 }
430 }
431}
432
433const ADDITIONAL_ALLOWED_FOR_BUCKET_NAME: [char; 2] = ['.', '-'];
434
435#[proc_macro]
461pub fn bucket_name(input: TokenStream) -> TokenStream {
462 let input: LitStr = syn::parse(input).unwrap();
463 let value = input.value();
464
465 if value.chars().any(|c| c.is_uppercase()) {
466 return Error::new(input.span(), "value contains uppercase letters".to_string())
467 .into_compile_error()
468 .into();
469 }
470
471 if value
472 .chars()
473 .any(|c| !c.is_alphanumeric() && !ADDITIONAL_ALLOWED_FOR_BUCKET_NAME.contains(&c))
474 {
475 return Error::new(
476 input.span(),
477 "value should contain only letters, numbers, periods and dashes".to_string(),
478 )
479 .into_compile_error()
480 .into();
481 }
482
483 let no_remote_check_wanted = env::var(NO_REMOTE_OVERRIDE_ENV_VAR_NAME)
484 .ok()
485 .and_then(|v| v.parse().ok())
486 .unwrap_or(false);
487
488 if no_remote_check_wanted {
489 return bucket_name::bucket_name_output(value);
490 }
491
492 let rechecked_wanted = env::var(RUSTY_CDK_RECHECK_ENV_VAR_NAME)
493 .ok()
494 .and_then(|v| v.parse().ok())
495 .unwrap_or(false);
496
497 if !rechecked_wanted {
498 match bucket_name::valid_bucket_name_according_to_file_storage(&value) {
499 bucket_name::FileStorageOutput::Valid => {
500 return bucket_name::bucket_name_output(value)
501 }
502 bucket_name::FileStorageOutput::Invalid => {
503 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()
504 }
505 bucket_name::FileStorageOutput::Unknown => {}
506 }
507 }
508
509 match bucket_name::check_bucket_name(input) {
510 Ok(_) => {
511 bucket_name::update_file_storage(bucket_name::FileStorageInput::Valid(&value));
512 bucket_name::bucket_name_output(value)
513 }
514 Err(e) => {
515 bucket_name::update_file_storage(bucket_name::FileStorageInput::Invalid(&value));
516 e.into_compile_error().into()
517 }
518 }
519}
520
521const POSSIBLE_LOG_RETENTION_VALUES: [u16; 22] = [
522 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653,
523];
524
525#[proc_macro]
531pub fn log_retention(input: TokenStream) -> TokenStream {
532 let output = match syn::parse::<LitInt>(input) {
533 Ok(v) => v,
534 Err(_) => {
535 return Error::new(Span::call_site(), "value is not a valid number".to_string())
536 .into_compile_error()
537 .into();
538 }
539 };
540
541 let as_number: syn::Result<u16> = output.base10_parse();
542
543 if let Ok(num) = as_number {
544 if POSSIBLE_LOG_RETENTION_VALUES.contains(&num) {
545 quote! {
546 RetentionInDays(#num)
547 }
548 .into()
549 } else {
550 Error::new(output.span(), format!("value should be one of {:?}", POSSIBLE_LOG_RETENTION_VALUES))
551 .into_compile_error()
552 .into()
553 }
554 } else {
555 Error::new(output.span(), "value is not a valid u16 number".to_string())
556 .into_compile_error()
557 .into()
558 }
559}
560
561const ADDITIONAL_ALLOWED_FOR_LOG_GROUP: [char; 6] = ['.', '-', '_', '#', '/', '\\'];
562
563#[proc_macro]
571pub fn log_group_name(input: TokenStream) -> TokenStream {
572 let output: LitStr = syn::parse(input).unwrap();
573 let value = output.value();
574
575 if value.is_empty() {
576 return Error::new(output.span(), "value should not be blank".to_string())
577 .into_compile_error()
578 .into();
579 }
580
581 if value.len() > 512 {
582 return Error::new(output.span(), "value should not be longer than 512 chars".to_string())
583 .into_compile_error()
584 .into();
585 }
586
587 if value
588 .chars()
589 .any(|c| !c.is_alphanumeric() && !ADDITIONAL_ALLOWED_FOR_LOG_GROUP.contains(&c))
590 {
591 return Error::new(
592 output.span(),
593 format!(
594 "value should only contain alphanumeric characters and {:?}",
595 ADDITIONAL_ALLOWED_FOR_LOG_GROUP
596 ),
597 )
598 .into_compile_error()
599 .into();
600 }
601
602 quote!(
603 LogGroupName(#value.to_string())
604 )
605 .into()
606}
607
608#[proc_macro]
622pub fn iam_action(input: TokenStream) -> TokenStream {
623 let output: LitStr = syn::parse(input).unwrap();
624 let value = output.value();
625
626 if value.is_empty() {
627 return Error::new(output.span(), "value should not be blank".to_string())
628 .into_compile_error()
629 .into();
630 }
631
632 let validator = PermissionValidator::new();
633 match validator.is_valid_action(&value) {
634 ValidationResponse::Valid => quote!(
635 IamAction(#value.to_string())
636 )
637 .into(),
638 ValidationResponse::Invalid(message) => Error::new(output.span(), message).into_compile_error().into(),
639 }
640}
641
642#[proc_macro]
653pub fn lifecycle_object_sizes(input: TokenStream) -> TokenStream {
654 let ObjectSizes { first, second } = parse_macro_input!(input);
655
656 if first.is_some() && second.is_some() && first.unwrap() > second.unwrap() {
658 return Error::new(
659 Span::call_site(),
660 format!(
661 "first number ({}) in `lifecycle_object_sizes` should be smaller than second ({})",
662 first.unwrap(),
663 second.unwrap()
664 ),
665 )
666 .into_compile_error()
667 .into();
668 }
669
670 let first_output = if let Some(first) = first {
671 quote! {
672 Some(#first)
673 }
674 } else {
675 quote! { None }
676 };
677
678 let second_output = if let Some(second) = second {
679 quote! {
680 Some(#second)
681 }
682 } else {
683 quote! { None }
684 };
685
686 quote! {
687 S3LifecycleObjectSizes(#first_output, #second_output)
688 }
689 .into()
690}
691
692#[proc_macro]
702pub fn origin_path(input: TokenStream) -> TokenStream {
703 let output: LitStr = syn::parse(input).unwrap();
704 let value = output.value();
705
706 if !value.starts_with("/") || value.ends_with("/") {
707 return Error::new(
708 value.span(),
709 format!("origin path should start with a / and should not end with / (but got {})", value),
710 )
711 .into_compile_error()
712 .into();
713 }
714
715 quote! {
716 OriginPath(#value)
717 }
718 .into()
719}
720
721#[proc_macro]
731pub fn default_root_object(input: TokenStream) -> TokenStream {
732 let output: LitStr = syn::parse(input).unwrap();
733 let value = output.value();
734
735 if value.starts_with("/") || value.ends_with("/") {
736 return Error::new(value.span(), "default root object should not start with /".to_string())
737 .into_compile_error()
738 .into();
739 }
740
741 quote! {
742 DefaultRootObject(#value)
743 }
744 .into()
745}
746
747#[proc_macro]
755pub fn cf_connection_timeout(input: TokenStream) -> TokenStream {
756 let Timeouts { first, second } = parse_macro_input!(input);
757
758 if let Some(first) = first {
759 if first > 10 {
760 return Error::new(
761 Span::call_site(),
762 format!("connection timeout was {} but should be between 1 and 10", first),
763 )
764 .into_compile_error()
765 .into();
766 } else if let Some(second) = second {
767 if second < first {
768 return Error::new(
769 Span::call_site(),
770 format!(
771 "response completion timeout was {} but should be larger than connection timeout ({})",
772 second, first
773 ),
774 )
775 .into_compile_error()
776 .into();
777 }
778 }
779 }
780
781 let first_output = if let Some(first) = first {
782 quote! {
783 Some(#first)
784 }
785 } else {
786 quote! { None }
787 };
788
789 let second_output = if let Some(second) = second {
790 quote! {
791 Some(#second)
792 }
793 } else {
794 quote! { None }
795 };
796
797 quote! {
798 CfConnectionTimeout(#first_output, #second_output)
799 }
800 .into()
801}
802
803#[proc_macro]
814pub fn lambda_permission_action(input: TokenStream) -> TokenStream {
815 let output: LitStr = syn::parse(input).unwrap();
816 let value = output.value();
817
818 let requirements = StringRequirements::not_empty_prefix("lambda");
819
820 match check_string_requirements(&value, output.span(), requirements) {
821 None => quote!(
822 LambdaPermissionAction(#value.to_string())
823 )
824 .into(),
825 Some(e) => e.into_compile_error().into(),
826 }
827}
828
829#[proc_macro]
840pub fn app_config_name(input: TokenStream) -> TokenStream {
841 let output: LitStr = syn::parse(input).unwrap();
842 let value = output.value();
843
844 if value.is_empty() || value.len() > 64 {
845 return Error::new(
846 Span::call_site(),
847 "app config name should be between 1 and 64 chars in length".to_string(),
848 )
849 .into_compile_error()
850 .into();
851 }
852
853 quote! {
854 AppConfigName(#value.to_string())
855 }
856 .into()
857}
858
859const LIFECYCLE_STORAGE_TYPES: [&str; 6] = [
860 "IntelligentTiering",
861 "OneZoneIA",
862 "StandardIA",
863 "GlacierDeepArchive",
864 "Glacier",
865 "GlacierInstantRetrieval",
866];
867const LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS: [&str; 2] = ["OneZoneIA", "StandardIA"];
868
869#[proc_macro]
877pub fn lifecycle_transition_in_days(input: TokenStream) -> TokenStream {
878 let TransitionInfo { days, service } = parse_macro_input!(input);
879 let service = service.trim();
880
881 if !LIFECYCLE_STORAGE_TYPES.contains(&service) {
882 return Error::new(
883 Span::call_site(),
884 format!("service should be one of {} (was {})", LIFECYCLE_STORAGE_TYPES.join(","), service),
885 )
886 .into_compile_error()
887 .into();
888 } else if LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS.contains(&service) && days <= 30 {
889 return Error::new(
890 Span::call_site(),
891 format!(
892 "service of type {} cannot have transition under 30 days",
893 LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS.join(" or ")
894 ),
895 )
896 .into_compile_error()
897 .into();
898 }
899
900 quote! {
901 LifecycleTransitionInDays(#days)
902 }
903 .into()
904}
905
906const LOCATION_URI_TYPES: [&str; 4] = ["hosted", "codepipeline", "secretsmanager", "s3"];
907const LOCATION_URI_CODEPIPELINE_START: &str = "codepipeline://";
908const LOCATION_URI_SECRETS_MANAGER_START: &str = "secretsmanager://";
909const LOCATION_URI_S3_START: &str = "s3://";
910
911
912#[proc_macro]
920pub fn location_uri(input: TokenStream) -> TokenStream {
921 let LocationUri {
922 location_uri_type,
923 content,
924 } = parse_macro_input!(input);
925 let location_uri_type = location_uri_type.trim();
926
927 #[allow(unused)] let mut error = None;
929
930 if !LOCATION_URI_TYPES.contains(&location_uri_type) {
931 error = Some(format!(
932 "unrecognized location uri {}, should be one of {}",
933 location_uri_type,
934 LOCATION_URI_TYPES.join(",")
935 ));
936 } else {
937 if location_uri_type == "hosted" {
938 return quote! {
939 LocationUri(#location_uri_type.to_string())
940 }
941 .into();
942 } else if content.is_none() {
943 error = Some(format!("location uri of type {}, should have content", location_uri_type));
944 } else {
945 let content = content.expect("just checked that this is present");
946
947 if location_uri_type == "codepipeline" && !content.starts_with(LOCATION_URI_CODEPIPELINE_START) {
948 error = Some(format!(
949 "content of type codepipeline should start with {}",
950 LOCATION_URI_CODEPIPELINE_START
951 ));
952 } else if location_uri_type == "secretsmanager" && !content.starts_with(LOCATION_URI_SECRETS_MANAGER_START) {
953 error = Some(format!(
954 "content of type secretsmanager should start with {}",
955 LOCATION_URI_SECRETS_MANAGER_START
956 ));
957 } else if location_uri_type == "s3" && !content.starts_with(LOCATION_URI_S3_START) {
958 error = Some(format!("content of type s3 should start with {}", LOCATION_URI_S3_START));
959 } else {
960 return quote! {
961 LocationUri(#content.to_string())
962 }
963 .into();
964 }
965 }
966 }
967
968 Error::new(
969 Span::call_site(),
970 error.unwrap_or_else(|| "unknown error".to_string()),
971 )
972 .into_compile_error()
973 .into()
974}