1use std::collections::HashSet;
12
13use nasa_macro_support::runtime_root;
14use proc_macro::TokenStream;
15use proc_macro2::TokenStream as TokenStream2;
16use quote::{format_ident, quote};
17use syn::{
18 parse_macro_input, punctuated::Punctuated, Expr, FnArg, GenericArgument, ItemFn, ItemImpl, Lit,
19 LitStr, Meta, Path, PathArguments, ReturnType, Token, Type,
20};
21
22#[proc_macro_attribute]
175pub fn application(attr: TokenStream, item: TokenStream) -> TokenStream {
176 let components =
177 parse_macro_input!(attr with Punctuated::<LitStr, Token![,]>::parse_terminated);
178 let function = parse_macro_input!(item as ItemFn);
179 match expand_application(components.into_iter().collect(), function) {
180 Ok(expanded) => expanded.into(),
181 Err(error) => error.to_compile_error().into(),
182 }
183}
184
185#[proc_macro_attribute]
240pub fn initializer(attr: TokenStream, item: TokenStream) -> TokenStream {
241 let metas = parse_macro_input!(attr with Punctuated::<Meta, Token![,]>::parse_terminated);
242 let item_impl = parse_macro_input!(item as ItemImpl);
243 match expand_initializer(metas, item_impl) {
244 Ok(expanded) => expanded.into(),
245 Err(error) => error.to_compile_error().into(),
246 }
247}
248
249#[proc_macro_attribute]
255pub fn redis_job(attr: TokenStream, item: TokenStream) -> TokenStream {
256 let metas = parse_macro_input!(attr with Punctuated::<Meta, Token![,]>::parse_terminated);
257 let function = parse_macro_input!(item as ItemFn);
258 match expand_redis_job(metas, function) {
259 Ok(expanded) => expanded.into(),
260 Err(error) => error.to_compile_error().into(),
261 }
262}
263
264#[derive(Default)]
266struct RedisJobArgs {
267 name: Option<LitStr>,
268 qualifier: Option<LitStr>,
269 worker: Option<LitStr>,
270 trigger: Option<LitStr>,
271 cron: Option<LitStr>,
272 zone: Option<LitStr>,
273 fixed_rate_ms: Option<syn::LitInt>,
274 fixed_delay_ms: Option<syn::LitInt>,
275 concurrency: Option<LitStr>,
276 misfire: Option<LitStr>,
277 timeout_ms: Option<syn::LitInt>,
278 max_attempts: Option<syn::LitInt>,
279 retry_delay_ms: Option<syn::LitInt>,
280 contract_revision: Option<syn::LitInt>,
281 schema: Option<LitStr>,
282 codecs: Option<Vec<LitStr>>,
283 fanout_receipt_timeout_ms: Option<syn::LitInt>,
284 fanout_receipt_max_retries: Option<syn::LitInt>,
285 fanout_failure_policy: Option<LitStr>,
286 definition_revision: Option<syn::LitInt>,
287}
288
289enum RedisJobParameter {
291 Application,
292 Context,
293 Payload(Box<Type>),
294}
295
296fn expand_redis_job(
302 metas: Punctuated<Meta, Token![,]>,
303 function: ItemFn,
304) -> syn::Result<TokenStream2> {
305 if function.sig.asyncness.is_none() {
306 return Err(syn::Error::new_spanned(
307 function.sig.fn_token,
308 "redis_job function must be async",
309 ));
310 }
311 if function.sig.receiver().is_some() || !function.sig.generics.params.is_empty() {
312 return Err(syn::Error::new_spanned(
313 &function.sig,
314 "redis_job function cannot have a receiver or generic parameters",
315 ));
316 }
317 if function.sig.inputs.len() > 3 {
318 return Err(syn::Error::new_spanned(
319 &function.sig.inputs,
320 "redis_job function accepts at most Application, JobContext, and one payload",
321 ));
322 }
323 let args = parse_redis_job_args(&metas)?;
324 validate_redis_job_args(&args)?;
325 let runtime = runtime_root("application", "napp")
326 .map_err(|message| syn::Error::new_spanned(&function.sig.ident, message))?;
327 let mut parameters = Vec::new();
328 let mut has_application = false;
329 let mut has_context = false;
330 let mut has_payload = false;
331 for input in &function.sig.inputs {
332 let FnArg::Typed(argument) = input else {
333 return Err(syn::Error::new_spanned(
334 input,
335 "redis_job does not accept self",
336 ));
337 };
338 if matches!(argument.ty.as_ref(), Type::Reference(_)) {
339 return Err(syn::Error::new_spanned(
340 &argument.ty,
341 "redis_job parameters must be owned values",
342 ));
343 }
344 let kind = classify_redis_job_parameter(&argument.ty);
345 match &kind {
346 RedisJobParameter::Application if has_application => {
347 return Err(syn::Error::new_spanned(
348 &argument.ty,
349 "Application can only be injected once",
350 ));
351 }
352 RedisJobParameter::Context if has_context => {
353 return Err(syn::Error::new_spanned(
354 &argument.ty,
355 "JobContext can only be injected once",
356 ));
357 }
358 RedisJobParameter::Payload(_) if has_payload => {
359 return Err(syn::Error::new_spanned(
360 &argument.ty,
361 "redis_job accepts only one payload",
362 ));
363 }
364 RedisJobParameter::Application => has_application = true,
365 RedisJobParameter::Context => has_context = true,
366 RedisJobParameter::Payload(_) => has_payload = true,
367 }
368 parameters.push(kind);
369 }
370 let function_name = &function.sig.ident;
371 let derived_name = function_name.to_string();
372 let name = args.name.clone().unwrap_or_else(|| {
373 LitStr::new(
374 derived_name.strip_prefix("r#").unwrap_or(&derived_name),
375 function_name.span(),
376 )
377 });
378 let builder_steps = redis_job_builder_steps(&args, &runtime)?;
379 let handler_field = has_application.then(|| quote!(application: #runtime::WeakApplication,));
380 let handler_value = if has_application {
381 quote!(__NasaRedisJobHandler {
382 application: application.downgrade()
383 })
384 } else {
385 quote!({
386 let _ = application;
387 __NasaRedisJobHandler {}
388 })
389 };
390 let application_clone = has_application.then(|| {
391 quote! {
392 let application = match self.application.upgrade() {
393 ::std::option::Option::Some(application) => application,
394 ::std::option::Option::None => {
395 return ::std::boxed::Box::pin(async {
396 #runtime::__private::nadis::job::JobOutcome::retry(
397 "Application lifecycle ended before RedisJob invocation"
398 )
399 });
400 }
401 };
402 }
403 });
404 let declared_codecs = args
405 .codecs
406 .clone()
407 .unwrap_or_else(|| vec![LitStr::new("json", proc_macro2::Span::call_site())]);
408 let codec_wires = declared_codecs
409 .iter()
410 .map(|codec| match codec.value().to_ascii_lowercase().as_str() {
411 "json" => Ok(LitStr::new("JSON", codec.span())),
412 "protobuf" => Ok(LitStr::new("PROTOBUF", codec.span())),
413 "raw" => Ok(LitStr::new("RAW", codec.span())),
414 _ => Err(syn::Error::new_spanned(codec, "redis_job codec is unknown")),
415 })
416 .collect::<syn::Result<Vec<_>>>()?;
417 let codec_guard = quote! {
418 if !matches!(context.wire_codec.as_str(), #(#codec_wires)|*) {
419 return #runtime::__private::nadis::job::JobOutcome::fail_permanent(
420 "payload codec is outside the static Worker contract"
421 );
422 }
423 };
424 let mut call_arguments = Vec::new();
425 let mut payload_decode = TokenStream2::new();
426 for parameter in parameters {
427 match parameter {
428 RedisJobParameter::Application => call_arguments.push(quote!(application)),
429 RedisJobParameter::Context => call_arguments.push(quote!(context.clone())),
430 RedisJobParameter::Payload(payload_type) => {
431 payload_decode = if declared_codecs.len() == 1 {
432 match declared_codecs[0].value().to_ascii_lowercase().as_str() {
433 "json" => quote! {
434 let payload: #payload_type = match #runtime::__private::nadis::job::decode_json_payload(context.payload()) {
435 ::std::result::Result::Ok(value) => value,
436 ::std::result::Result::Err(error) => {
437 return #runtime::__private::nadis::job::JobOutcome::fail_permanent(
438 ::std::format!("payload JSON decode rejected: {error}")
439 );
440 }
441 };
442 },
443 "protobuf" => quote! {
444 let payload: #payload_type = match
445 <#payload_type as #runtime::__private::prost::Message>::decode(context.payload())
446 {
447 ::std::result::Result::Ok(value) => value,
448 ::std::result::Result::Err(error) => {
449 return #runtime::__private::nadis::job::JobOutcome::fail_permanent(
450 ::std::format!("payload Protobuf decode rejected: {error}")
451 );
452 }
453 };
454 },
455 "raw" => quote! {
456 let payload: #payload_type = match
457 <#payload_type as ::std::convert::TryFrom<::std::vec::Vec<u8>>>::try_from(
458 context.payload().to_vec()
459 )
460 {
461 ::std::result::Result::Ok(value) => value,
462 ::std::result::Result::Err(error) => {
463 return #runtime::__private::nadis::job::JobOutcome::fail_permanent(
464 ::std::format!("payload RAW decode rejected: {error}")
465 );
466 }
467 };
468 },
469 _ => unreachable!("codec 已在宏期校验"),
470 }
471 } else {
472 quote! {
473 let codec = match #runtime::__private::nadis::job::JobWireCodec::parse(
474 context.wire_codec.as_str()
475 ) {
476 ::std::option::Option::Some(codec) => codec,
477 ::std::option::Option::None => {
478 return #runtime::__private::nadis::job::JobOutcome::fail_permanent(
479 "payload codec is not a known wire value"
480 );
481 }
482 };
483 if codec == #runtime::__private::nadis::job::JobWireCodec::Json {
484 if let ::std::result::Result::Err(error) =
485 #runtime::__private::nadis::job::validate_json_payload(context.payload())
486 {
487 return #runtime::__private::nadis::job::JobOutcome::fail_permanent(
488 ::std::format!("payload JSON validation rejected: {error}")
489 );
490 }
491 }
492 let payload: #payload_type = match
493 <#payload_type as #runtime::__private::nadis::job::JobParameter>::decode(
494 codec,
495 context.payload(),
496 )
497 {
498 ::std::result::Result::Ok(value) => value,
499 ::std::result::Result::Err(error) => {
500 return #runtime::__private::nadis::job::JobOutcome::fail_permanent(
501 ::std::format!("payload decode rejected: {error}")
502 );
503 }
504 };
505 }
506 };
507 call_arguments.push(quote!(payload));
508 }
509 }
510 }
511
512 Ok(quote! {
513 #function
514
515 const _: () = {
516 struct __NasaRedisJobHandler { #handler_field }
518
519 impl #runtime::__private::nadis::job::JobHandler for __NasaRedisJobHandler {
520 fn handle<'a>(
526 &'a self,
527 execution: &'a #runtime::__private::nadis::job::JobExecution,
528 ) -> #runtime::__private::nadis::job::JobHandlerFuture<'a> {
529 use #runtime::__private::nadis::job::IntoJobHandlerResult as _;
530 let context = execution.clone();
531 #application_clone
532 ::std::boxed::Box::pin(async move {
533 #codec_guard
534 #payload_decode
535 #function_name(#(#call_arguments),*)
536 .await
537 .into_job_handler_result()
538 })
539 }
540 }
541
542 fn __nasa_redis_job_factory(
548 application: #runtime::Application,
549 ) -> #runtime::ApplicationResult<(
550 #runtime::__private::nadis::job::JobDefinition,
551 ::std::sync::Arc<dyn #runtime::__private::nadis::job::JobHandler>,
552 )> {
553 let mut builder = #runtime::__private::nadis::job::JobDefinition::builder(#name);
554 #(#builder_steps)*
555 let definition = builder.build().map_err(|error| {
556 #runtime::ApplicationError::with_source(
557 #runtime::ComponentId::RedisJob,
558 #runtime::ApplicationPhase::Prepare,
559 "invalid #[redis_job] definition",
560 error,
561 )
562 })?;
563 let handler: ::std::sync::Arc<dyn #runtime::__private::nadis::job::JobHandler> =
564 ::std::sync::Arc::new(#handler_value);
565 ::std::result::Result::Ok((definition, handler))
566 }
567
568 #[#runtime::__private::linkme::distributed_slice(#runtime::COLLECTED_REDIS_JOBS)]
569 #[linkme(crate = #runtime::__private::linkme)]
570 static __NASA_REDIS_JOB_DESCRIPTOR: #runtime::RedisJobDescriptor =
571 #runtime::RedisJobDescriptor::__new(
572 __nasa_redis_job_factory,
573 concat!(module_path!(), ":", file!(), ":", line!()),
574 );
575 };
576 })
577}
578
579fn classify_redis_job_parameter(ty: &Type) -> RedisJobParameter {
585 if let Type::Path(path) = ty {
586 let segments: Vec<String> = path
587 .path
588 .segments
589 .iter()
590 .map(|segment| segment.ident.to_string())
591 .collect();
592 let unqualified = segments.len() == 1;
593 if let Some(name) = segments.last() {
594 if name == "Application"
595 && (unqualified
596 || matches!(segments.as_slice(), [root, value] if matches!(root.as_str(), "nasa" | "napp") && value == "Application"))
597 {
598 return RedisJobParameter::Application;
599 }
600 let framework_job_path = segments.len() >= 2
601 && segments
602 .get(segments.len() - 2)
603 .is_some_and(|segment| segment == "job");
604 if matches!(name.as_str(), "JobContext" | "JobExecution")
605 && (unqualified || framework_job_path)
606 {
607 return RedisJobParameter::Context;
608 }
609 }
610 }
611 RedisJobParameter::Payload(Box::new(ty.clone()))
612}
613
614fn parse_redis_job_args(metas: &Punctuated<Meta, Token![,]>) -> syn::Result<RedisJobArgs> {
620 let mut result = RedisJobArgs::default();
621 let mut seen = HashSet::new();
622 for meta in metas {
623 let Meta::NameValue(value) = meta else {
624 return Err(syn::Error::new_spanned(
625 meta,
626 "redis_job attributes must use `key = value`",
627 ));
628 };
629 let key = value
630 .path
631 .get_ident()
632 .map(ToString::to_string)
633 .ok_or_else(|| {
634 syn::Error::new_spanned(
635 &value.path,
636 "redis_job attribute key must be an identifier",
637 )
638 })?;
639 if !seen.insert(key.clone()) {
640 return Err(syn::Error::new_spanned(
641 meta,
642 "redis_job attribute key is repeated",
643 ));
644 }
645 macro_rules! string_field {
646 ($field:ident) => {{
647 result.$field = Some(parse_job_string(&value.value, &key)?);
648 }};
649 }
650 macro_rules! integer_field {
651 ($field:ident) => {{
652 result.$field = Some(parse_job_integer(&value.value, &key)?);
653 }};
654 }
655 match key.as_str() {
656 "name" => string_field!(name),
657 "qualifier" => string_field!(qualifier),
658 "worker" => string_field!(worker),
659 "trigger" => string_field!(trigger),
660 "cron" => string_field!(cron),
661 "zone" => string_field!(zone),
662 "fixed_rate_ms" => integer_field!(fixed_rate_ms),
663 "fixed_delay_ms" => integer_field!(fixed_delay_ms),
664 "concurrency" => string_field!(concurrency),
665 "misfire" => string_field!(misfire),
666 "timeout_ms" => integer_field!(timeout_ms),
667 "max_attempts" => integer_field!(max_attempts),
668 "retry_delay_ms" => integer_field!(retry_delay_ms),
669 "contract_revision" => integer_field!(contract_revision),
670 "schema" => string_field!(schema),
671 "fanout_receipt_timeout_ms" => integer_field!(fanout_receipt_timeout_ms),
672 "fanout_receipt_max_retries" => integer_field!(fanout_receipt_max_retries),
673 "fanout_failure_policy" => string_field!(fanout_failure_policy),
674 "definition_revision" => integer_field!(definition_revision),
675 "codecs" => {
676 let Expr::Array(array) = &value.value else {
677 return Err(syn::Error::new_spanned(
678 &value.value,
679 "redis_job codecs must be an array of strings",
680 ));
681 };
682 result.codecs = Some(
683 array
684 .elems
685 .iter()
686 .map(|item| parse_job_string(item, "codecs"))
687 .collect::<syn::Result<Vec<_>>>()?,
688 );
689 }
690 _ => {
691 return Err(syn::Error::new_spanned(
692 &value.path,
693 format!("unknown redis_job attribute `{key}`"),
694 ))
695 }
696 }
697 }
698 Ok(result)
699}
700
701fn validate_redis_job_args(args: &RedisJobArgs) -> syn::Result<()> {
707 if args.codecs.as_ref().is_some_and(Vec::is_empty) {
708 return Err(syn::Error::new(
709 proc_macro2::Span::call_site(),
710 "redis_job codecs must not be empty",
711 ));
712 }
713 let schedule_count = usize::from(args.cron.is_some())
714 + usize::from(args.fixed_rate_ms.is_some())
715 + usize::from(args.fixed_delay_ms.is_some());
716 if schedule_count > 1 {
717 return Err(syn::Error::new(
718 proc_macro2::Span::call_site(),
719 "redis_job cron, fixed_rate_ms, and fixed_delay_ms are mutually exclusive",
720 ));
721 }
722 if args
723 .trigger
724 .as_ref()
725 .is_some_and(|trigger| trigger.value().eq_ignore_ascii_case("fanout_only"))
726 && schedule_count != 0
727 {
728 return Err(syn::Error::new_spanned(
729 args.trigger.as_ref().expect("trigger 已确认存在"),
730 "redis_job fanout_only must not declare a schedule",
731 ));
732 }
733 for (field, value) in [
734 ("fixed_rate_ms", args.fixed_rate_ms.as_ref()),
735 ("fixed_delay_ms", args.fixed_delay_ms.as_ref()),
736 ("timeout_ms", args.timeout_ms.as_ref()),
737 ("max_attempts", args.max_attempts.as_ref()),
738 ("retry_delay_ms", args.retry_delay_ms.as_ref()),
739 ("contract_revision", args.contract_revision.as_ref()),
740 (
741 "fanout_receipt_timeout_ms",
742 args.fanout_receipt_timeout_ms.as_ref(),
743 ),
744 ("definition_revision", args.definition_revision.as_ref()),
745 ] {
746 if value.is_some_and(|literal| matches!(literal.base10_parse::<u64>(), Ok(0))) {
747 return Err(syn::Error::new_spanned(
748 value.expect("value 已确认存在"),
749 format!("redis_job {field} must be greater than zero"),
750 ));
751 }
752 }
753 Ok(())
754}
755
756fn parse_job_string(expression: &Expr, field: &str) -> syn::Result<LitStr> {
762 match expression {
763 Expr::Lit(value) => match &value.lit {
764 Lit::Str(literal) => Ok(literal.clone()),
765 _ => Err(syn::Error::new_spanned(
766 expression,
767 format!("redis_job {field} must be a string literal"),
768 )),
769 },
770 _ => Err(syn::Error::new_spanned(
771 expression,
772 format!("redis_job {field} must be a string literal"),
773 )),
774 }
775}
776
777fn parse_job_integer(expression: &Expr, field: &str) -> syn::Result<syn::LitInt> {
783 match expression {
784 Expr::Lit(value) => match &value.lit {
785 Lit::Int(literal) => Ok(literal.clone()),
786 _ => Err(syn::Error::new_spanned(
787 expression,
788 format!("redis_job {field} must be a non-negative integer literal"),
789 )),
790 },
791 _ => Err(syn::Error::new_spanned(
792 expression,
793 format!("redis_job {field} must be a non-negative integer literal"),
794 )),
795 }
796}
797
798fn redis_job_builder_steps(
804 args: &RedisJobArgs,
805 runtime: &TokenStream2,
806) -> syn::Result<Vec<TokenStream2>> {
807 let mut steps = Vec::new();
808 macro_rules! scalar_step {
809 ($field:ident, $method:ident) => { if let Some(value) = &args.$field { steps.push(quote!(builder = builder.$method(#value);)); } };
810 }
811 scalar_step!(qualifier, qualifier);
812 scalar_step!(worker, worker_name);
813 if let Some(trigger) = &args.trigger {
814 match trigger.value().to_ascii_lowercase().as_str() {
815 "scheduled" => {}
816 "fanout_only" => steps.push(quote!(builder = builder.fanout_only();)),
817 _ => {
818 return Err(syn::Error::new_spanned(
819 trigger,
820 "redis_job trigger must be `scheduled` or `fanout_only`",
821 ))
822 }
823 }
824 }
825 if let Some(cron) = &args.cron {
826 let zone = args
827 .zone
828 .clone()
829 .unwrap_or_else(|| LitStr::new("UTC", cron.span()));
830 steps.push(quote! {
831 builder = builder.cron(#cron, #zone).map_err(|error| {
832 #runtime::ApplicationError::with_source(
833 #runtime::ComponentId::RedisJob,
834 #runtime::ApplicationPhase::Prepare,
835 "invalid #[redis_job] cron declaration",
836 error,
837 )
838 })?;
839 });
840 } else if let Some(zone) = &args.zone {
841 return Err(syn::Error::new_spanned(
842 zone,
843 "redis_job zone requires cron",
844 ));
845 }
846 scalar_step!(fixed_rate_ms, fixed_rate_ms);
847 scalar_step!(fixed_delay_ms, fixed_delay_ms);
848 if let Some(value) = &args.concurrency {
849 let variant = match value.value().to_ascii_lowercase().as_str() {
850 "serial_queue" => quote!(#runtime::__private::nadis::job::JobConcurrency::SerialQueue),
851 "discard_if_running" => {
852 quote!(#runtime::__private::nadis::job::JobConcurrency::DiscardIfRunning)
853 }
854 "parallel" => quote!(#runtime::__private::nadis::job::JobConcurrency::Parallel),
855 _ => {
856 return Err(syn::Error::new_spanned(
857 value,
858 "redis_job concurrency is unknown",
859 ))
860 }
861 };
862 steps.push(quote!(builder = builder.concurrency(#variant);));
863 }
864 if let Some(value) = &args.misfire {
865 let variant = match value.value().to_ascii_lowercase().as_str() {
866 "do_nothing" => quote!(#runtime::__private::nadis::job::JobMisfire::DoNothing),
867 "fire_once_now" => quote!(#runtime::__private::nadis::job::JobMisfire::FireOnceNow),
868 "catch_up" => quote!(#runtime::__private::nadis::job::JobMisfire::CatchUp),
869 _ => {
870 return Err(syn::Error::new_spanned(
871 value,
872 "redis_job misfire is unknown",
873 ))
874 }
875 };
876 steps.push(quote!(builder = builder.misfire(#variant);));
877 }
878 scalar_step!(timeout_ms, timeout_ms);
879 scalar_step!(max_attempts, max_attempts);
880 scalar_step!(retry_delay_ms, retry_delay_ms);
881 scalar_step!(contract_revision, contract_revision);
882 scalar_step!(schema, schema_id);
883 if let Some(codecs) = &args.codecs {
884 let mut variants = Vec::new();
885 for codec in codecs {
886 variants.push(match codec.value().to_ascii_lowercase().as_str() {
887 "json" => quote!(#runtime::__private::nadis::job::JobWireCodec::Json),
888 "protobuf" => quote!(#runtime::__private::nadis::job::JobWireCodec::Protobuf),
889 "raw" => quote!(#runtime::__private::nadis::job::JobWireCodec::Raw),
890 _ => return Err(syn::Error::new_spanned(codec, "redis_job codec is unknown")),
891 });
892 }
893 steps.push(quote!(builder = builder.codecs([#(#variants),*]);));
894 }
895 if args.fanout_receipt_timeout_ms.is_some() || args.fanout_receipt_max_retries.is_some() {
896 let timeout = args
897 .fanout_receipt_timeout_ms
898 .clone()
899 .unwrap_or_else(|| syn::LitInt::new("2000", proc_macro2::Span::call_site()));
900 let retries = args
901 .fanout_receipt_max_retries
902 .clone()
903 .unwrap_or_else(|| syn::LitInt::new("3", proc_macro2::Span::call_site()));
904 steps.push(quote!(builder = builder.fanout_receipt(#timeout, #retries);));
905 }
906 if let Some(value) = &args.fanout_failure_policy {
907 let variant = match value.value().to_ascii_lowercase().as_str() {
908 "reassign_on_failure" => {
909 quote!(#runtime::__private::nadis::job::JobFanoutFailurePolicy::ReassignOnFailure)
910 }
911 "strict_snapshot" => {
912 quote!(#runtime::__private::nadis::job::JobFanoutFailurePolicy::StrictSnapshot)
913 }
914 "best_effort" => {
915 quote!(#runtime::__private::nadis::job::JobFanoutFailurePolicy::BestEffort)
916 }
917 _ => {
918 return Err(syn::Error::new_spanned(
919 value,
920 "redis_job fanout_failure_policy is unknown",
921 ))
922 }
923 };
924 steps.push(quote!(builder = builder.fanout_failure_policy(#variant);));
925 }
926 scalar_step!(definition_revision, definition_revision);
927 Ok(steps)
928}
929
930struct InitializerArgs {
932 name: LitStr,
933 order: Option<i32>,
934 requires: Vec<LitStr>,
935 kind: InitializerKindArg,
936 factory: Option<Path>,
937}
938
939enum InitializerKindArg {
941 OneShot,
942 Hosted,
943}
944
945fn expand_initializer(
953 metas: Punctuated<Meta, Token![,]>,
954 item_impl: ItemImpl,
955) -> syn::Result<TokenStream2> {
956 verify_initializer_impl(&item_impl)?;
957 let args = parse_initializer_args(&metas, &item_impl.self_ty)?;
958 let runtime = runtime_root("application", "napp")
959 .map_err(|message| syn::Error::new_spanned(&item_impl.self_ty, message))?;
960 let initializer_type = (*item_impl.self_ty).clone();
961 let name = args.name;
962 let order = args
963 .order
964 .map(|value| quote!(#value))
965 .unwrap_or_else(|| quote!(#runtime::DEFAULT_INITIALIZER_ORDER));
966 let requires = args.requires;
967 let kind = match args.kind {
968 InitializerKindArg::OneShot => quote!(#runtime::InitializerKind::OneShot),
969 InitializerKindArg::Hosted => quote!(#runtime::InitializerKind::Hosted),
970 };
971 let construct = match args.factory {
972 Some(factory) => quote! {
973 let result: #runtime::ApplicationResult<::std::option::Option<#initializer_type>> =
974 #factory(application).await;
975 let initializer = result?;
976 ::std::result::Result::Ok(initializer.map(|value| {
977 ::std::boxed::Box::new(value)
978 as ::std::boxed::Box<dyn #runtime::Initialization>
979 }))
980 },
981 None => quote! {
982 let _ = application;
983 let value: #initializer_type =
984 <#initializer_type as ::std::default::Default>::default();
985 ::std::result::Result::Ok(::std::option::Option::Some(
986 ::std::boxed::Box::new(value)
987 as ::std::boxed::Box<dyn #runtime::Initialization>
988 ))
989 },
990 };
991
992 Ok(quote! {
993 #item_impl
994
995 const _: () = {
996 fn __nasa_initializer_factory(
1003 application: #runtime::Application,
1004 ) -> #runtime::ApplicationFuture<
1005 'static,
1006 ::std::option::Option<
1007 ::std::boxed::Box<dyn #runtime::Initialization>
1008 >,
1009 > {
1010 ::std::boxed::Box::pin(async move { #construct })
1011 }
1012
1013 #[#runtime::__private::linkme::distributed_slice(#runtime::COLLECTED_INITIALIZERS)]
1014 #[linkme(crate = #runtime::__private::linkme)]
1015 static __NASA_INITIALIZER_DESCRIPTOR: #runtime::InitializerDescriptor =
1016 #runtime::InitializerDescriptor::__new(
1017 #name,
1018 #order,
1019 &[#(#requires),*],
1020 #kind,
1021 __nasa_initializer_factory,
1022 concat!(module_path!(), ":", file!(), ":", line!()),
1023 );
1024 };
1025 })
1026}
1027
1028fn parse_initializer_args(
1036 metas: &Punctuated<Meta, Token![,]>,
1037 self_type: &Type,
1038) -> syn::Result<InitializerArgs> {
1039 let mut name = None;
1040 let mut order = None;
1041 let mut requires = None;
1042 let mut kind = None;
1043 let mut factory = None;
1044 for meta in metas {
1045 let Meta::NameValue(value) = meta else {
1046 return Err(syn::Error::new_spanned(
1047 meta,
1048 "initializer attributes must use `key = value` syntax",
1049 ));
1050 };
1051 let key = value
1052 .path
1053 .get_ident()
1054 .map(ToString::to_string)
1055 .ok_or_else(|| {
1056 syn::Error::new_spanned(
1057 &value.path,
1058 "initializer attribute key must be an identifier",
1059 )
1060 })?;
1061 match key.as_str() {
1062 "name" => set_once(&mut name, parse_string_expr(&value.value, "name")?, meta)?,
1063 "order" => {
1064 let parsed = parse_initializer_order(&value.value)?;
1065 set_once(&mut order, parsed, meta)?;
1066 }
1067 "requires" => {
1068 let Expr::Array(array) = &value.value else {
1069 return Err(syn::Error::new_spanned(
1070 &value.value,
1071 "initializer requires must be an array of string literals",
1072 ));
1073 };
1074 let mut parsed = Vec::with_capacity(array.elems.len());
1075 for element in &array.elems {
1076 parsed.push(parse_string_expr(element, "requires entry")?);
1077 }
1078 set_once(&mut requires, parsed, meta)?;
1079 }
1080 "kind" => {
1081 let literal = parse_string_expr(&value.value, "kind")?;
1082 let parsed = match literal.value().as_str() {
1083 "one-shot" => InitializerKindArg::OneShot,
1084 "hosted" => InitializerKindArg::Hosted,
1085 _ => {
1086 return Err(syn::Error::new_spanned(
1087 literal,
1088 "initializer kind must be `one-shot` or `hosted`",
1089 ));
1090 }
1091 };
1092 set_once(&mut kind, parsed, meta)?;
1093 }
1094 "factory" => {
1095 let Expr::Path(path) = &value.value else {
1096 return Err(syn::Error::new_spanned(
1097 &value.value,
1098 "initializer factory must be a function path",
1099 ));
1100 };
1101 set_once(&mut factory, path.path.clone(), meta)?;
1102 }
1103 _ => {
1104 return Err(syn::Error::new_spanned(
1105 &value.path,
1106 "unknown initializer attribute key",
1107 ));
1108 }
1109 }
1110 }
1111
1112 let name = match name {
1113 Some(name) => name,
1114 None => default_initializer_name(self_type)?,
1115 };
1116 validate_initializer_name(&name, "initializer name")?;
1117 let requires = requires.unwrap_or_default();
1118 if requires.len() > 32 {
1119 return Err(syn::Error::new_spanned(
1120 &name,
1121 "initializer requires cannot contain more than 32 entries",
1122 ));
1123 }
1124 let mut seen = HashSet::new();
1125 for required in &requires {
1126 validate_initializer_name(required, "initializer dependency")?;
1127 if required.value() == name.value() {
1128 return Err(syn::Error::new_spanned(
1129 required,
1130 "initializer cannot require itself",
1131 ));
1132 }
1133 if !seen.insert(required.value()) {
1134 return Err(syn::Error::new_spanned(
1135 required,
1136 "initializer dependency is repeated",
1137 ));
1138 }
1139 }
1140 Ok(InitializerArgs {
1141 name,
1142 order,
1143 requires,
1144 kind: kind.unwrap_or(InitializerKindArg::OneShot),
1145 factory,
1146 })
1147}
1148
1149fn default_initializer_name(self_type: &Type) -> syn::Result<LitStr> {
1156 let Type::Path(path) = self_type else {
1157 return Err(syn::Error::new_spanned(
1158 self_type,
1159 "initializer name cannot be derived from this type; declare `name` explicitly",
1160 ));
1161 };
1162 if path.qself.is_some() {
1163 return Err(syn::Error::new_spanned(
1164 self_type,
1165 "initializer name cannot be derived from a qualified self type; declare `name` explicitly",
1166 ));
1167 }
1168 let segment = path.path.segments.last().ok_or_else(|| {
1169 syn::Error::new_spanned(
1170 self_type,
1171 "initializer name cannot be derived from this type; declare `name` explicitly",
1172 )
1173 })?;
1174 let identifier = segment.ident.to_string();
1175 let name = canonicalize_type_name(&identifier).ok_or_else(|| {
1176 syn::Error::new_spanned(
1177 &segment.ident,
1178 "initializer type name cannot form a canonical name; declare `name` explicitly",
1179 )
1180 })?;
1181 let name = LitStr::new(&name, segment.ident.span());
1182 validate_initializer_name(&name, "derived initializer name")?;
1183 Ok(name)
1184}
1185
1186fn canonicalize_type_name(identifier: &str) -> Option<String> {
1193 let identifier = identifier.strip_prefix("r#").unwrap_or(identifier);
1194 let bytes = identifier.as_bytes();
1195 let mut output = String::with_capacity(bytes.len());
1196 let mut pending_separator = false;
1197 for (index, byte) in bytes.iter().copied().enumerate() {
1198 if byte == b'_' {
1199 pending_separator = !output.is_empty();
1200 continue;
1201 }
1202 if !byte.is_ascii_alphanumeric() {
1203 return None;
1204 }
1205 let previous = index
1206 .checked_sub(1)
1207 .and_then(|value| bytes.get(value))
1208 .copied();
1209 let next = bytes.get(index + 1).copied();
1210 let word_boundary = byte.is_ascii_uppercase()
1211 && (previous.is_some_and(|value| value.is_ascii_lowercase() || value.is_ascii_digit())
1212 || (previous.is_some_and(|value| value.is_ascii_uppercase())
1213 && next.is_some_and(|value| value.is_ascii_lowercase())));
1214 if (pending_separator || word_boundary) && !output.is_empty() && !output.ends_with('-') {
1215 output.push('-');
1216 }
1217 output.push(byte.to_ascii_lowercase() as char);
1218 pending_separator = false;
1219 }
1220 while output.ends_with('-') {
1221 output.pop();
1222 }
1223 (!output.is_empty()).then_some(output)
1224}
1225
1226fn parse_initializer_order(expression: &Expr) -> syn::Result<i32> {
1233 let invalid = || {
1234 syn::Error::new_spanned(
1235 expression,
1236 "initializer order must be an i32 integer literal",
1237 )
1238 };
1239 let signed = match expression {
1240 Expr::Lit(expr) => match &expr.lit {
1241 Lit::Int(value) => value.base10_parse::<i64>().map_err(|_| invalid())?,
1242 _ => return Err(invalid()),
1243 },
1244 Expr::Unary(expr) if matches!(expr.op, syn::UnOp::Neg(_)) => match expr.expr.as_ref() {
1245 Expr::Lit(expr) => match &expr.lit {
1246 Lit::Int(value) => value
1247 .base10_parse::<i64>()
1248 .ok()
1249 .and_then(i64::checked_neg)
1250 .ok_or_else(invalid)?,
1251 _ => return Err(invalid()),
1252 },
1253 _ => return Err(invalid()),
1254 },
1255 _ => return Err(invalid()),
1256 };
1257 i32::try_from(signed).map_err(|_| invalid())
1258}
1259
1260fn verify_initializer_impl(item_impl: &ItemImpl) -> syn::Result<()> {
1267 if item_impl.unsafety.is_some() {
1268 return Err(syn::Error::new_spanned(
1269 item_impl.unsafety,
1270 "initializer cannot annotate an unsafe impl",
1271 ));
1272 }
1273 if !item_impl.generics.params.is_empty() {
1274 return Err(syn::Error::new_spanned(
1275 &item_impl.generics,
1276 "initializer impl cannot declare generic parameters",
1277 ));
1278 }
1279 let Some((polarity, trait_path, _)) = &item_impl.trait_ else {
1280 return Err(syn::Error::new_spanned(
1281 &item_impl.self_ty,
1282 "initializer must annotate an Initialization trait impl",
1283 ));
1284 };
1285 if polarity.is_some() {
1286 return Err(syn::Error::new_spanned(
1287 polarity,
1288 "initializer cannot annotate a negative impl",
1289 ));
1290 }
1291 if trait_path
1292 .segments
1293 .last()
1294 .is_none_or(|segment| segment.ident != "Initialization")
1295 {
1296 return Err(syn::Error::new_spanned(
1297 trait_path,
1298 "initializer trait path must end with Initialization",
1299 ));
1300 }
1301 Ok(())
1302}
1303
1304fn parse_string_expr(expression: &Expr, field: &str) -> syn::Result<LitStr> {
1312 match expression {
1313 Expr::Lit(expr) => match &expr.lit {
1314 Lit::Str(value) => Ok(value.clone()),
1315 _ => Err(syn::Error::new_spanned(
1316 expression,
1317 format!("initializer {field} must be a string literal"),
1318 )),
1319 },
1320 _ => Err(syn::Error::new_spanned(
1321 expression,
1322 format!("initializer {field} must be a string literal"),
1323 )),
1324 }
1325}
1326
1327fn set_once<T>(slot: &mut Option<T>, value: T, meta: &Meta) -> syn::Result<()> {
1336 if slot.is_some() {
1337 return Err(syn::Error::new_spanned(
1338 meta,
1339 "initializer attribute key is repeated",
1340 ));
1341 }
1342 *slot = Some(value);
1343 Ok(())
1344}
1345
1346fn validate_initializer_name(name: &LitStr, field: &str) -> syn::Result<()> {
1354 let value = name.value();
1355 if value.is_empty() || value.len() > 128 {
1356 return Err(syn::Error::new_spanned(
1357 name,
1358 format!("{field} must contain between 1 and 128 bytes"),
1359 ));
1360 }
1361 if !value.bytes().all(|byte| {
1362 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-' | b'.')
1363 }) {
1364 return Err(syn::Error::new_spanned(
1365 name,
1366 format!("{field} must contain only lowercase ASCII letters, digits, `_`, `-`, or `.`"),
1367 ));
1368 }
1369 Ok(())
1370}
1371
1372fn expand_application(
1380 components: Vec<LitStr>,
1381 mut function: ItemFn,
1382) -> syn::Result<proc_macro2::TokenStream> {
1383 validate_function(&function)?;
1384 let component_names = validate_components(&components)?;
1385 let runtime = runtime_root("application", "napp")
1386 .map_err(|message| syn::Error::new_spanned(&function.sig.ident, message))?;
1387
1388 let has_web = component_names.iter().any(|name| name == "web");
1389 let component_variants = component_names
1390 .iter()
1391 .map(|name| component_variant(name))
1392 .collect::<syn::Result<Vec<_>>>()?;
1393 let feature_modules = component_names
1394 .iter()
1395 .map(|name| component_feature_module(name))
1396 .collect::<syn::Result<Vec<_>>>()?;
1397 let accepts_application = function.sig.inputs.len() == 1;
1398 function.sig.ident = format_ident!("__nasa_user_main");
1399
1400 let hook = if accepts_application {
1401 quote!(|application| __nasa_user_main(application))
1402 } else {
1403 quote!(|_application| __nasa_user_main())
1404 };
1405 let web_items = if has_web {
1406 quote! {
1407 #runtime::__private::naweb::mvc_router!(#runtime::Application);
1408
1409 fn __nasa_route_meta() -> ::std::vec::Vec<#runtime::RouteMeta> {
1415 crate::__mvc::ROUTES
1416 .iter()
1417 .map(|entry| #runtime::RouteMeta {
1418 method: entry.method,
1419 path: entry.path,
1420 handler: entry.handler,
1421 produces: entry.produces,
1422 consumes: entry.consumes,
1423 request_schema: entry.request_schema,
1424 response_schema: entry.response_schema,
1425 query_parameters: entry.query_parameters,
1426 header_parameters: entry.header_parameters,
1427 success_status: entry.success_status,
1428 additional_responses: entry.additional_responses,
1429 streaming: entry.streaming,
1430 auth_required: ::core::matches!(
1431 entry.policy.auth,
1432 #runtime::__private::naweb::AuthRequirement::Required
1433 ),
1434 })
1435 .collect()
1436 }
1437
1438 fn __nasa_build_router(
1448 context: #runtime::WebBuildContext,
1449 ) -> #runtime::ApplicationResult<
1450 #runtime::__private::axum::Router<#runtime::Application>,
1451 > {
1452 context.build(|router, mapping_runtime, mapping_plan, application| {
1453 crate::__mvc::try_register_all(
1454 router,
1455 mapping_runtime,
1456 mapping_plan,
1457 application,
1458 )
1459 })
1460 }
1461 }
1462 } else {
1463 quote! {}
1464 };
1465 let spec_web = if has_web {
1466 quote!(
1467 .with_web_route_meta(__nasa_route_meta)
1468 .with_web_factory(__nasa_build_router)
1469 )
1470 } else {
1471 quote! {}
1472 };
1473
1474 Ok(quote! {
1475 #[doc(hidden)]
1476 pub mod __nasa_application_must_be_at_crate_root {}
1477 use crate::__nasa_application_must_be_at_crate_root as _;
1478 #(
1479 const _: () = #runtime::components::#feature_modules::FEATURE_CHECK;
1480 )*
1481
1482 #web_items
1483 #function
1484
1485 fn __nasa_require_user_hook<F, Fut, E>(hook: F) -> F
1494 where
1495 F: ::std::ops::FnOnce(#runtime::Application) -> Fut + ::std::marker::Send + 'static,
1496 Fut: ::std::future::Future<Output = ::std::result::Result<(), E>>
1497 + ::std::marker::Send
1498 + 'static,
1499 E: ::std::convert::Into<#runtime::__private::anyhow::Error> + 'static,
1500 {
1501 hook
1502 }
1503
1504 fn main() -> ::std::process::ExitCode {
1510 #runtime::run(
1511 #runtime::ApplicationSpec::new(&[
1512 #(#runtime::ComponentId::#component_variants),*
1513 ])
1514 .with_default_name(env!("CARGO_PKG_NAME"))
1515 #spec_web,
1516 __nasa_require_user_hook(#hook),
1517 )
1518 }
1519 })
1520}
1521
1522fn validate_function(function: &ItemFn) -> syn::Result<()> {
1529 if function.sig.ident != "main" {
1530 return Err(syn::Error::new_spanned(
1531 &function.sig.ident,
1532 "application attribute must be attached to the crate main function",
1533 ));
1534 }
1535 if function.sig.asyncness.is_none() {
1536 return Err(syn::Error::new_spanned(
1537 function.sig.fn_token,
1538 "application main must be async and must not use another runtime entry attribute",
1539 ));
1540 }
1541 if !function.sig.generics.params.is_empty() {
1542 return Err(syn::Error::new_spanned(
1543 &function.sig.generics,
1544 "application main cannot declare generics",
1545 ));
1546 }
1547 if function.sig.inputs.len() > 1 {
1548 return Err(syn::Error::new_spanned(
1549 &function.sig.inputs,
1550 "application main accepts at most one Application parameter",
1551 ));
1552 }
1553 if let Some(argument) = function.sig.inputs.first() {
1554 validate_application_parameter(argument)?;
1555 }
1556 validate_return_type(&function.sig.output)?;
1557 for attribute in &function.attrs {
1558 let segments = attribute
1559 .path()
1560 .segments
1561 .iter()
1562 .map(|segment| segment.ident.to_string())
1563 .collect::<Vec<_>>();
1564 if segments.first().is_some_and(|name| name == "tokio")
1565 && segments.last().is_some_and(|name| name == "main")
1566 {
1567 return Err(syn::Error::new_spanned(
1568 attribute,
1569 "remove the other runtime entry attribute because application owns the runtime",
1570 ));
1571 }
1572 if segments
1573 .last()
1574 .is_some_and(|name| matches!(name.as_str(), "EnableScheduling" | "EnableAsync"))
1575 {
1576 return Err(syn::Error::new_spanned(
1577 attribute,
1578 "declare the scheduling component in application instead of using an entry attribute",
1579 ));
1580 }
1581 }
1582 Ok(())
1583}
1584
1585fn validate_application_parameter(argument: &FnArg) -> syn::Result<()> {
1592 let FnArg::Typed(argument) = argument else {
1593 return Err(syn::Error::new_spanned(
1594 argument,
1595 "application main cannot use a receiver parameter",
1596 ));
1597 };
1598 let Type::Path(path) = argument.ty.as_ref() else {
1599 return Err(syn::Error::new_spanned(
1600 &argument.ty,
1601 "application main parameter must be Application",
1602 ));
1603 };
1604 if path
1605 .path
1606 .segments
1607 .last()
1608 .is_none_or(|segment| segment.ident != "Application")
1609 {
1610 return Err(syn::Error::new_spanned(
1611 &argument.ty,
1612 "application main parameter must be Application",
1613 ));
1614 }
1615 Ok(())
1616}
1617
1618fn validate_return_type(output: &ReturnType) -> syn::Result<()> {
1625 let ReturnType::Type(_, output_type) = output else {
1626 return Err(syn::Error::new_spanned(
1627 output,
1628 "application main must return anyhow::Result<()>",
1629 ));
1630 };
1631 let Type::Path(path) = output_type.as_ref() else {
1632 return Err(syn::Error::new_spanned(
1633 output_type,
1634 "application main must return anyhow::Result<()>",
1635 ));
1636 };
1637 let Some(result) = path.path.segments.last() else {
1638 return Err(syn::Error::new_spanned(
1639 output_type,
1640 "application main must return anyhow::Result<()>",
1641 ));
1642 };
1643 let PathArguments::AngleBracketed(arguments) = &result.arguments else {
1644 return Err(syn::Error::new_spanned(
1645 output_type,
1646 "application main must return anyhow::Result<()>",
1647 ));
1648 };
1649 let unit_success = matches!(
1650 arguments.args.first(),
1651 Some(GenericArgument::Type(Type::Tuple(tuple))) if tuple.elems.is_empty()
1652 );
1653 if result.ident != "Result" || arguments.args.len() != 1 || !unit_success {
1654 return Err(syn::Error::new_spanned(
1655 output_type,
1656 "application main must return anyhow::Result<()>",
1657 ));
1658 }
1659 Ok(())
1660}
1661
1662const CANONICAL_COMPONENT_ORDER: [&str; 17] = [
1667 "log",
1668 "nacos-config",
1669 "telemetry",
1670 "db",
1671 "redis",
1672 "cache",
1673 "partition",
1674 "saga",
1675 "kafka",
1676 "outbox",
1677 "redis-job",
1678 "grpc",
1679 "auth",
1680 "web",
1681 "ws",
1682 "nacos-discovery",
1683 "scheduling",
1684];
1685
1686fn validate_components(components: &[LitStr]) -> syn::Result<Vec<String>> {
1696 let mut seen = HashSet::new();
1697 let mut names = Vec::with_capacity(components.len());
1698 for component in components.iter() {
1699 let name = component.value();
1700 if !CANONICAL_COMPONENT_ORDER.contains(&name.as_str()) {
1701 return Err(syn::Error::new_spanned(
1702 component,
1703 format!("unknown application component `{name}`"),
1704 ));
1705 }
1706 if !seen.insert(name.clone()) {
1707 return Err(syn::Error::new_spanned(
1708 component,
1709 format!("application component `{name}` is declared more than once"),
1710 ));
1711 }
1712 names.push(name);
1713 }
1714 if seen.contains("saga") && seen.insert("outbox".to_string()) {
1718 names.push("outbox".to_string());
1719 }
1720 if seen.contains("outbox") && seen.insert("db".to_string()) {
1721 names.push("db".to_string());
1722 }
1723 if seen.contains("redis-job") && seen.insert("redis".to_string()) {
1725 names.push("redis".to_string());
1726 }
1727 names.sort_by_key(|name| {
1729 CANONICAL_COMPONENT_ORDER
1730 .iter()
1731 .position(|canonical| canonical == name)
1732 .expect("name validated against CANONICAL_COMPONENT_ORDER above")
1733 });
1734 Ok(names)
1735}
1736
1737fn component_variant(name: &str) -> syn::Result<syn::Ident> {
1744 let variant = match name {
1745 "log" => "Log",
1746 "nacos-config" => "NacosConfig",
1747 "db" => "Db",
1748 "redis" => "Redis",
1749 "redis-job" => "RedisJob",
1750 "telemetry" => "Telemetry",
1751 "cache" => "Cache",
1752 "partition" => "Partition",
1753 "grpc" => "Grpc",
1754 "saga" => "Saga",
1755 "kafka" => "Kafka",
1756 "outbox" => "Outbox",
1757 "auth" => "Auth",
1758 "web" => "Web",
1759 "ws" => "Ws",
1760 "nacos-discovery" => "NacosDiscovery",
1761 "scheduling" => "Scheduling",
1762 _ => {
1763 return Err(syn::Error::new(
1764 proc_macro2::Span::call_site(),
1765 "component name was not validated",
1766 ));
1767 }
1768 };
1769 Ok(format_ident!("{variant}"))
1770}
1771
1772fn component_feature_module(name: &str) -> syn::Result<syn::Ident> {
1779 match name {
1780 "log" | "db" | "redis" | "telemetry" | "cache" | "partition" | "grpc" | "saga"
1781 | "kafka" | "outbox" | "auth" | "web" | "ws" | "scheduling" => Ok(format_ident!("{name}")),
1782 "redis-job" => Ok(format_ident!("redis_job")),
1783 "nacos-config" => Ok(format_ident!("nacos_config")),
1784 "nacos-discovery" => Ok(format_ident!("nacos_discovery")),
1785 _ => Err(syn::Error::new(
1786 proc_macro2::Span::call_site(),
1787 "component name was not validated",
1788 )),
1789 }
1790}