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;
34
35use crate::file_util::get_absolute_file_path;
36use crate::iam_validation::{PermissionValidator, ValidationResponse};
37use crate::location_uri::LocationUri;
38use crate::object_sizes::ObjectSizes;
39use crate::strings::{check_string_requirements, StringRequirements};
40use crate::timeouts::Timeouts;
41use crate::transition_in_days::TransitionInfo;
42use proc_macro::TokenStream;
43use quote::__private::Span;
44use quote::quote;
45use std::env;
46use syn::spanned::Spanned;
47use syn::{parse_macro_input, Error, LitInt, LitStr};
48use crate::bucket_tiering::BucketTiering;
49
50#[proc_macro]
57pub fn string_with_only_alphanumerics_and_underscores(input: TokenStream) -> TokenStream {
58 let output: LitStr = syn::parse(input).unwrap();
59 let value = output.value();
60
61 let requirements = StringRequirements::not_empty_allowed_chars(vec!['_']);
62
63 match check_string_requirements(&value, output.span(), requirements) {
64 None => quote!(
65 StringWithOnlyAlphaNumericsAndUnderscores(#value.to_string())
66 )
67 .into(),
68 Some(e) => e.into_compile_error().into(),
69 }
70}
71
72#[proc_macro]
79pub fn string_with_only_alphanumerics_underscores_and_hyphens(input: TokenStream) -> TokenStream {
80 let output: LitStr = syn::parse(input).unwrap();
81 let value = output.value();
82
83 let requirements = StringRequirements::not_empty_allowed_chars(vec!['_', '-']);
84
85 match check_string_requirements(&value, output.span(), requirements) {
86 None => quote!(
87 StringWithOnlyAlphaNumericsUnderscoresAndHyphens(#value.to_string())
88 )
89 .into(),
90 Some(e) => e.into_compile_error().into(),
91 }
92}
93
94#[proc_macro]
105pub fn string_with_only_alphanumerics_and_hyphens(input: TokenStream) -> TokenStream {
106 let output: LitStr = syn::parse(input).unwrap();
107 let value = output.value();
108
109 let requirements = StringRequirements::not_empty_allowed_chars(vec!['-']);
110
111 match check_string_requirements(&value, output.span(), requirements) {
112 None => quote!(
113 StringWithOnlyAlphaNumericsAndHyphens(#value.to_string())
114 )
115 .into(),
116 Some(e) => e.into_compile_error().into(),
117 }
118}
119
120#[proc_macro]
131pub fn app_sync_api_name(input: TokenStream) -> TokenStream {
132 let output: LitStr = syn::parse(input).unwrap();
133 let value = output.value();
134
135 if value.len() > 50 {
136 return Error::new(output.span(), "name cannot be longer than 50 characters".to_string())
137 .into_compile_error()
138 .into();
139 }
140
141 let requirements = StringRequirements::not_empty_allowed_chars(vec!['-', '_', ' ']);
142
143 match check_string_requirements(&value, output.span(), requirements) {
144 None => quote!(
145 AppSyncApiName(#value.to_string())
146 )
147 .into(),
148 Some(e) => e.into_compile_error().into(),
149 }
150}
151
152#[proc_macro]
163pub fn channel_namespace_name(input: TokenStream) -> TokenStream {
164 let output: LitStr = syn::parse(input).unwrap();
165 let value = output.value();
166
167 if value.len() > 50 {
168 return Error::new(output.span(), "name cannot be longer than 50 characters".to_string())
169 .into_compile_error()
170 .into();
171 }
172
173 let requirements = StringRequirements::not_empty_allowed_chars(vec!['-']);
174
175 match check_string_requirements(&value, output.span(), requirements) {
176 None => quote!(
177 ChannelNamespaceName(#value.to_string())
178 )
179 .into(),
180 Some(e) => e.into_compile_error().into(),
181 }
182}
183
184#[proc_macro]
194pub fn string_for_secret(input: TokenStream) -> TokenStream {
195 let output: LitStr = syn::parse(input).unwrap();
196 let value = output.value();
197
198 let requirements = StringRequirements::not_empty_allowed_chars(vec!['/', '_', '+', '=', '.', '@', '-']);
199
200 match check_string_requirements(&value, output.span(), requirements) {
201 None => quote!(
202 StringForSecret(#value.to_string())
203 )
204 .into(),
205 Some(e) => e.into_compile_error().into(),
206 }
207}
208
209#[proc_macro]
217pub fn env_var_key(input: TokenStream) -> TokenStream {
218 let output: LitStr = syn::parse(input).unwrap();
219 let value = output.value();
220
221 if value.len() < 2 {
222 return Error::new(output.span(), "env var key should be at least two characters long".to_string())
223 .into_compile_error()
224 .into();
225 }
226
227 if value.get(0..1).expect("just checked that length is at least 2") == "_" {
228 return Error::new(output.span(), "env var key should not start with an underscore".to_string())
229 .into_compile_error()
230 .into();
231 }
232
233 if value.chars().any(|c| !c.is_alphanumeric() && c != '_') {
234 return Error::new(
235 output.span(),
236 "env var key should only contain alphanumeric characters and underscores".to_string(),
237 )
238 .into_compile_error()
239 .into();
240 }
241
242 quote!(
243 EnvVarKey(#value.to_string())
244 )
245 .into()
246}
247
248#[proc_macro]
261pub fn zip_file(input: TokenStream) -> TokenStream {
262 let output: syn::Result<LitStr> = syn::parse(input);
263
264 let output = match output {
265 Ok(output) => output,
266 Err(_) => {
267 return Error::new(Span::call_site(), "zip_file macro should contain value".to_string())
268 .into_compile_error()
269 .into();
270 }
271 };
272
273 let value = output.value();
274
275 if !value.ends_with(".zip") {
276 return Error::new(output.span(), format!("zip should end with `.zip` (found `{value}`)"))
277 .into_compile_error()
278 .into();
279 }
280
281 let value = match get_absolute_file_path(&value) {
282 Ok(v) => v,
283 Err(e) => {
284 return Error::new(output.span(), e).into_compile_error().into();
285 }
286 };
287
288 quote!(
289 ZipFile(#value.to_string())
290 )
291 .into()
292}
293
294#[proc_macro]
305pub fn toml_file(input: TokenStream) -> TokenStream {
306 let output: syn::Result<LitStr> = syn::parse(input);
307
308 let output = match output {
309 Ok(output) => output,
310 Err(_) => {
311 return Error::new(Span::call_site(), "toml_file macro should contain value".to_string())
312 .into_compile_error()
313 .into();
314 }
315 };
316
317 let value = output.value();
318
319 if !value.ends_with(".toml") {
320 return Error::new(output.span(), format!("toml file should end with `.toml` (found `{value}`)"))
321 .into_compile_error()
322 .into();
323 }
324
325 let value = match get_absolute_file_path(&value) {
326 Ok(v) => v,
327 Err(e) => {
328 return Error::new(output.span(), e).into_compile_error().into();
329 }
330 };
331
332 quote!(
333 TomlFile(#value.to_string())
334 )
335 .into()
336}
337
338#[proc_macro]
340pub fn non_zero_number(input: TokenStream) -> TokenStream {
341 let output = match syn::parse::<LitInt>(input) {
342 Ok(v) => v,
343 Err(_) => {
344 return Error::new(Span::call_site(), "value is not a valid number".to_string())
345 .into_compile_error()
346 .into();
347 }
348 };
349
350 let as_number: syn::Result<u32> = output.base10_parse();
351
352 let num = if let Ok(num) = as_number {
353 if num == 0 {
354 return Error::new(output.span(), "value should not be null".to_string())
355 .into_compile_error()
356 .into();
357 }
358 num
359 } else {
360 return Error::new(output.span(), "value is not a valid u32 number".to_string())
361 .into_compile_error()
362 .into();
363 };
364
365 quote!(
366 NonZeroNumber(#num)
367 )
368 .into()
369}
370
371macro_rules! number_check {
372 ($name:ident,$min:literal,$max:literal,$output:ident,$type:ty) => {
373 #[doc = "Checks whether the value that will be wrapped in the "]
374 #[doc = stringify!($output)]
375 #[doc = "struct is between "]
376 #[doc = stringify!($min)]
377 #[doc = "and "]
378 #[doc = stringify!($max)]
379 #[proc_macro]
380 pub fn $name(input: TokenStream) -> TokenStream {
381 let output: LitInt = syn::parse(input).unwrap();
382
383 let as_number: syn::Result<$type> = output.base10_parse();
384
385 if let Ok(num) = as_number {
386 if num < $min {
387 Error::new(output.span(), format!("value should be at least {}", $min)).into_compile_error().into()
388 } else if num > $max {
389 Error::new(output.span(), format!("value should be at most {}", $max)).into_compile_error().into()
390 } else {
391 quote!(
392 $output(#num)
393 ).into()
394 }
395 } else {
396 Error::new(output.span(), "value is not a valid number".to_string()).into_compile_error().into()
397 }
398 }
399 }
400}
401
402number_check!(memory, 128, 10240, Memory, u16);
403number_check!(timeout, 1, 900, Timeout, u16);
404number_check!(delay_seconds, 0, 900, DelaySeconds, u16);
405number_check!(maximum_message_size, 1024, 1048576, MaximumMessageSize, u32);
406number_check!(message_retention_period, 60, 1209600, MessageRetentionPeriod, u32);
407number_check!(visibility_timeout, 0, 43200, VisibilityTimeout, u32);
408number_check!(receive_message_wait_time, 0, 20, ReceiveMessageWaitTime, u8);
409number_check!(sqs_event_source_max_concurrency, 2, 1000, SqsEventSourceMaxConcurrency, u16);
410number_check!(connection_attempts, 1, 3, ConnectionAttempts, u8);
411number_check!(s3_origin_read_timeout, 1, 120, S3OriginReadTimeout, u8);
412number_check!(deployment_duration_in_minutes, 0, 1440, DeploymentDurationInMinutes, u16);
413number_check!(growth_factor, 0, 100, GrowthFactor, u8);
414number_check!(record_expiration_days, 7, 2147483647, RecordExpirationDays, u32);
415
416const NO_REMOTE_OVERRIDE_ENV_VAR_NAME: &str = "RUSTY_CDK_NO_REMOTE";
417const RUSTY_CDK_RECHECK_ENV_VAR_NAME: &str = "RUSTY_CDK_RECHECK";
418
419#[proc_macro]
444pub fn bucket(input: TokenStream) -> TokenStream {
445 let input: LitStr = syn::parse(input).unwrap();
446 let value = input.value();
447
448 if value.starts_with("arn:") {
449 return Error::new(input.span(), "value is an arn, not a bucket name".to_string())
450 .into_compile_error()
451 .into();
452 }
453
454 if value.starts_with("s3:") {
455 return Error::new(input.span(), "value has s3 prefix, should be plain bucket name".to_string())
456 .into_compile_error()
457 .into();
458 }
459
460 let no_remote_check_wanted = env::var(NO_REMOTE_OVERRIDE_ENV_VAR_NAME)
461 .ok()
462 .and_then(|v| v.parse().ok())
463 .unwrap_or(false);
464
465 if no_remote_check_wanted {
466 return bucket::bucket_output(value);
467 }
468
469 let rechecked_wanted = env::var(RUSTY_CDK_RECHECK_ENV_VAR_NAME)
470 .ok()
471 .and_then(|v| v.parse().ok())
472 .unwrap_or(false);
473
474 if !rechecked_wanted {
475 match bucket::valid_bucket_according_to_file_storage(&value) {
476 bucket::FileStorageOutput::Valid => {
477 return bucket::bucket_output(value)
478 }
479 bucket::FileStorageOutput::Invalid => {
480 return Error::new(input.span(), format!("(cached) did not find bucket with name `{value}` in your account. You can rerun this check by adding setting the `{RUSTY_CDK_RECHECK_ENV_VAR_NAME}` env var to true")).into_compile_error().into()
481 }
482 bucket::FileStorageOutput::Unknown => {}
483 }
484 }
485
486 let rt = tokio::runtime::Runtime::new().unwrap();
487
488 match rt.block_on(bucket::find_bucket(input.clone())) {
489 Ok(_) => {
490 bucket::update_file_storage(bucket::FileStorageInput::Valid(&value));
491 bucket::bucket_output(value)
492 }
493 Err(e) => {
494 bucket::update_file_storage(bucket::FileStorageInput::Invalid(&value));
495 e.into_compile_error().into()
496 }
497 }
498}
499
500const ADDITIONAL_ALLOWED_FOR_BUCKET_NAME: [char; 2] = ['.', '-'];
501
502#[proc_macro]
528pub fn bucket_name(input: TokenStream) -> TokenStream {
529 let input: LitStr = syn::parse(input).unwrap();
530 let value = input.value();
531
532 if value.chars().any(|c| c.is_uppercase()) {
533 return Error::new(input.span(), "value contains uppercase letters".to_string())
534 .into_compile_error()
535 .into();
536 }
537
538 if value
539 .chars()
540 .any(|c| !c.is_alphanumeric() && !ADDITIONAL_ALLOWED_FOR_BUCKET_NAME.contains(&c))
541 {
542 return Error::new(
543 input.span(),
544 "value should contain only letters, numbers, periods and dashes".to_string(),
545 )
546 .into_compile_error()
547 .into();
548 }
549
550 let no_remote_check_wanted = env::var(NO_REMOTE_OVERRIDE_ENV_VAR_NAME)
551 .ok()
552 .and_then(|v| v.parse().ok())
553 .unwrap_or(false);
554
555 if no_remote_check_wanted {
556 return bucket_name::bucket_name_output(value);
557 }
558
559 let rechecked_wanted = env::var(RUSTY_CDK_RECHECK_ENV_VAR_NAME)
560 .ok()
561 .and_then(|v| v.parse().ok())
562 .unwrap_or(false);
563
564 if !rechecked_wanted {
565 match bucket_name::valid_bucket_name_according_to_file_storage(&value) {
566 bucket_name::FileStorageOutput::Valid => {
567 return bucket_name::bucket_name_output(value)
568 }
569 bucket_name::FileStorageOutput::Invalid => {
570 return Error::new(input.span(), format!("(cached) bucket name is already taken. You can rerun this check by adding setting the `{RUSTY_CDK_RECHECK_ENV_VAR_NAME}` env var to true")).into_compile_error().into()
571 }
572 bucket_name::FileStorageOutput::Unknown => {}
573 }
574 }
575
576 match bucket_name::check_bucket_name(input) {
577 Ok(_) => {
578 bucket_name::update_file_storage(bucket_name::FileStorageInput::Valid(&value));
579 bucket_name::bucket_name_output(value)
580 }
581 Err(e) => {
582 bucket_name::update_file_storage(bucket_name::FileStorageInput::Invalid(&value));
583 e.into_compile_error().into()
584 }
585 }
586}
587
588const POSSIBLE_LOG_RETENTION_VALUES: [u16; 22] = [
589 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653,
590];
591
592#[proc_macro]
598pub fn log_retention(input: TokenStream) -> TokenStream {
599 let output = match syn::parse::<LitInt>(input) {
600 Ok(v) => v,
601 Err(_) => {
602 return Error::new(Span::call_site(), "value is not a valid number".to_string())
603 .into_compile_error()
604 .into();
605 }
606 };
607
608 let as_number: syn::Result<u16> = output.base10_parse();
609
610 if let Ok(num) = as_number {
611 if POSSIBLE_LOG_RETENTION_VALUES.contains(&num) {
612 quote! {
613 RetentionInDays(#num)
614 }
615 .into()
616 } else {
617 Error::new(output.span(), format!("value should be one of {:?}", POSSIBLE_LOG_RETENTION_VALUES))
618 .into_compile_error()
619 .into()
620 }
621 } else {
622 Error::new(output.span(), "value is not a valid u16 number".to_string())
623 .into_compile_error()
624 .into()
625 }
626}
627
628const ADDITIONAL_ALLOWED_FOR_LOG_GROUP: [char; 6] = ['.', '-', '_', '#', '/', '\\'];
629
630#[proc_macro]
638pub fn log_group_name(input: TokenStream) -> TokenStream {
639 let output: LitStr = syn::parse(input).unwrap();
640 let value = output.value();
641
642 if value.is_empty() {
643 return Error::new(output.span(), "value should not be blank".to_string())
644 .into_compile_error()
645 .into();
646 }
647
648 if value.len() > 512 {
649 return Error::new(output.span(), "value should not be longer than 512 chars".to_string())
650 .into_compile_error()
651 .into();
652 }
653
654 if value
655 .chars()
656 .any(|c| !c.is_alphanumeric() && !ADDITIONAL_ALLOWED_FOR_LOG_GROUP.contains(&c))
657 {
658 return Error::new(
659 output.span(),
660 format!(
661 "value should only contain alphanumeric characters and {:?}",
662 ADDITIONAL_ALLOWED_FOR_LOG_GROUP
663 ),
664 )
665 .into_compile_error()
666 .into();
667 }
668
669 quote!(
670 LogGroupName(#value.to_string())
671 )
672 .into()
673}
674
675#[proc_macro]
689pub fn iam_action(input: TokenStream) -> TokenStream {
690 let output: LitStr = syn::parse(input).unwrap();
691 let value = output.value();
692
693 if value.is_empty() {
694 return Error::new(output.span(), "value should not be blank".to_string())
695 .into_compile_error()
696 .into();
697 }
698
699 let validator = PermissionValidator::new();
700 match validator.is_valid_action(&value) {
701 ValidationResponse::Valid => quote!(
702 IamAction(#value.to_string())
703 )
704 .into(),
705 ValidationResponse::Invalid(message) => Error::new(output.span(), message).into_compile_error().into(),
706 }
707}
708
709#[proc_macro]
720pub fn lifecycle_object_sizes(input: TokenStream) -> TokenStream {
721 let ObjectSizes { first, second } = parse_macro_input!(input);
722
723 if first.is_some() && second.is_some() && first.unwrap() > second.unwrap() {
725 return Error::new(
726 Span::call_site(),
727 format!(
728 "first number ({}) in `lifecycle_object_sizes` should be smaller than second ({})",
729 first.unwrap(),
730 second.unwrap()
731 ),
732 )
733 .into_compile_error()
734 .into();
735 }
736
737 let first_output = if let Some(first) = first {
738 quote! {
739 Some(#first)
740 }
741 } else {
742 quote! { None }
743 };
744
745 let second_output = if let Some(second) = second {
746 quote! {
747 Some(#second)
748 }
749 } else {
750 quote! { None }
751 };
752
753 quote! {
754 S3LifecycleObjectSizes(#first_output, #second_output)
755 }
756 .into()
757}
758
759#[proc_macro]
769pub fn origin_path(input: TokenStream) -> TokenStream {
770 let output: LitStr = syn::parse(input).unwrap();
771 let value = output.value();
772
773 if !value.starts_with("/") || value.ends_with("/") {
774 return Error::new(
775 value.span(),
776 format!("origin path should start with a / and should not end with / (but got {})", value),
777 )
778 .into_compile_error()
779 .into();
780 }
781
782 quote! {
783 OriginPath(#value)
784 }
785 .into()
786}
787
788#[proc_macro]
798pub fn default_root_object(input: TokenStream) -> TokenStream {
799 let output: LitStr = syn::parse(input).unwrap();
800 let value = output.value();
801
802 if value.starts_with("/") || value.ends_with("/") {
803 return Error::new(value.span(), "default root object should not start with /".to_string())
804 .into_compile_error()
805 .into();
806 }
807
808 quote! {
809 DefaultRootObject(#value)
810 }
811 .into()
812}
813
814#[proc_macro]
822pub fn cf_connection_timeout(input: TokenStream) -> TokenStream {
823 let Timeouts { first, second } = parse_macro_input!(input);
824
825 if let Some(first) = first {
826 if first > 10 {
827 return Error::new(
828 Span::call_site(),
829 format!("connection timeout was {} but should be between 1 and 10", first),
830 )
831 .into_compile_error()
832 .into();
833 } else if let Some(second) = second && second < first {
834 return Error::new(
835 Span::call_site(),
836 format!(
837 "response completion timeout was {} but should be larger than connection timeout ({})",
838 second, first
839 ),
840 )
841 .into_compile_error()
842 .into();
843 }
844 }
845
846 let first_output = if let Some(first) = first {
847 quote! {
848 Some(#first)
849 }
850 } else {
851 quote! { None }
852 };
853
854 let second_output = if let Some(second) = second {
855 quote! {
856 Some(#second)
857 }
858 } else {
859 quote! { None }
860 };
861
862 quote! {
863 CfConnectionTimeout(#first_output, #second_output)
864 }
865 .into()
866}
867
868#[proc_macro]
879pub fn lambda_permission_action(input: TokenStream) -> TokenStream {
880 let output: LitStr = syn::parse(input).unwrap();
881 let value = output.value();
882
883 let requirements = StringRequirements::not_empty_prefix("lambda");
884
885 match check_string_requirements(&value, output.span(), requirements) {
886 None => quote!(
887 LambdaPermissionAction(#value.to_string())
888 )
889 .into(),
890 Some(e) => e.into_compile_error().into(),
891 }
892}
893
894#[proc_macro]
905pub fn app_config_name(input: TokenStream) -> TokenStream {
906 let output: LitStr = syn::parse(input).unwrap();
907 let value = output.value();
908
909 if value.is_empty() || value.len() > 64 {
910 return Error::new(
911 Span::call_site(),
912 "app config name should be between 1 and 64 chars in length".to_string(),
913 )
914 .into_compile_error()
915 .into();
916 }
917
918 quote! {
919 AppConfigName(#value.to_string())
920 }
921 .into()
922}
923
924const LIFECYCLE_STORAGE_TYPES: [&str; 6] = [
925 "IntelligentTiering",
926 "OneZoneIA",
927 "StandardIA",
928 "GlacierDeepArchive",
929 "Glacier",
930 "GlacierInstantRetrieval",
931];
932const LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS: [&str; 2] = ["OneZoneIA", "StandardIA"];
933
934#[proc_macro]
942pub fn lifecycle_transition_in_days(input: TokenStream) -> TokenStream {
943 let TransitionInfo { days, service } = parse_macro_input!(input);
944 let service = service.trim();
945
946 if !LIFECYCLE_STORAGE_TYPES.contains(&service) {
947 return Error::new(
948 Span::call_site(),
949 format!("service should be one of {} (was {})", LIFECYCLE_STORAGE_TYPES.join(","), service),
950 )
951 .into_compile_error()
952 .into();
953 } else if LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS.contains(&service) && days <= 30 {
954 return Error::new(
955 Span::call_site(),
956 format!(
957 "service of type {} cannot have transition under 30 days",
958 LIFECYCLE_STORAGE_TYPES_MORE_THAN_THIRTY_DAYS.join(" or ")
959 ),
960 )
961 .into_compile_error()
962 .into();
963 }
964
965 quote! {
966 LifecycleTransitionInDays(#days)
967 }
968 .into()
969}
970
971const ACCESS_TIERS: [&str; 2] = ["ARCHIVE_ACCESS", "DEEP_ARCHIVE_ACCESS"];
972
973#[proc_macro]
974pub fn bucket_tiering(input: TokenStream) -> TokenStream {
975 let BucketTiering { access_tier, days } = parse_macro_input!(input);
976
977 if !ACCESS_TIERS.contains(&access_tier.as_str()) {
978 return Error::new(
979 Span::call_site(),
980 format!("access tier should be one of {} (was {})", ACCESS_TIERS.join(","), access_tier),
981 )
982 .into_compile_error()
983 .into();
984 }
985
986 if &access_tier == "ARCHIVE_ACCESS" {
987 if days < 90 || days > 730 {
988 return Error::new(Span::call_site(), format!("days for access tier `ARCHIVE_ACCESS` should be between 90 and 730 (was {})", days))
989 .into_compile_error()
990 .into();
991 }
992 } else if &access_tier == "DEEP_ARCHIVE_ACCESS" {
993 if days < 180 || days > 730 {
994 return Error::new(Span::call_site(), format!("days for access tier `DEEP_ARCHIVE_ACCESS` should be between 180 and 730 (was {})", days))
995 .into_compile_error()
996 .into();
997 }
998 }
999
1000 quote! {
1001 BucketTiering(#access_tier.to_string(), #days)
1002 }
1003 .into()
1004}
1005
1006const LOCATION_URI_TYPES: [&str; 4] = ["hosted", "codepipeline", "secretsmanager", "s3"];
1007const LOCATION_URI_CODEPIPELINE_START: &str = "codepipeline://";
1008const LOCATION_URI_SECRETS_MANAGER_START: &str = "secretsmanager://";
1009const LOCATION_URI_S3_START: &str = "s3://";
1010
1011#[proc_macro]
1019pub fn location_uri(input: TokenStream) -> TokenStream {
1020 let LocationUri {
1021 location_uri_type,
1022 content,
1023 } = parse_macro_input!(input);
1024 let location_uri_type = location_uri_type.trim();
1025
1026 #[allow(unused)] let mut error = None;
1028
1029 if !LOCATION_URI_TYPES.contains(&location_uri_type) {
1030 error = Some(format!(
1031 "unrecognized location uri {}, should be one of {}",
1032 location_uri_type,
1033 LOCATION_URI_TYPES.join(",")
1034 ));
1035 } else {
1036 if location_uri_type == "hosted" {
1037 return quote! {
1038 LocationUri(#location_uri_type.to_string())
1039 }
1040 .into();
1041 } else if content.is_none() {
1042 error = Some(format!("location uri of type {}, should have content", location_uri_type));
1043 } else {
1044 let content = content.expect("just checked that this is present");
1045
1046 if location_uri_type == "codepipeline" && !content.starts_with(LOCATION_URI_CODEPIPELINE_START) {
1047 error = Some(format!(
1048 "content of type codepipeline should start with {}",
1049 LOCATION_URI_CODEPIPELINE_START
1050 ));
1051 } else if location_uri_type == "secretsmanager" && !content.starts_with(LOCATION_URI_SECRETS_MANAGER_START) {
1052 error = Some(format!(
1053 "content of type secretsmanager should start with {}",
1054 LOCATION_URI_SECRETS_MANAGER_START
1055 ));
1056 } else if location_uri_type == "s3" && !content.starts_with(LOCATION_URI_S3_START) {
1057 error = Some(format!("content of type s3 should start with {}", LOCATION_URI_S3_START));
1058 } else {
1059 return quote! {
1060 LocationUri(#content.to_string())
1061 }
1062 .into();
1063 }
1064 }
1065 }
1066
1067 Error::new(
1068 Span::call_site(),
1069 error.unwrap_or_else(|| "unknown error".to_string()),
1070 )
1071 .into_compile_error()
1072 .into()
1073}