1mod cmd_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6 Expr, ExprLit, FnArg, GenericArgument, ItemFn, Lit, Meta, MetaNameValue, Pat, PathArguments,
7 ReturnType, Type, TypePath, parse_macro_input,
8};
9
10const KNOWN_PRIMITIVES: &[&str] = &[
14 "String", "bool", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128", "f32",
15 "f64", "usize", "isize",
16];
17
18enum ArgForm {
20 ZeroArgs,
22 SimpleArgs(Vec<SimpleParam>),
24 ParserStruct {
26 #[allow(dead_code)]
27 param_name: syn::Ident,
28 param_type: Box<syn::Type>,
29 },
30}
31
32struct SimpleParam {
34 name: syn::Ident,
35 ty: syn::Type,
36 kind: SimpleParamKind,
37}
38
39enum SimpleParamKind {
41 Bool,
43 Required,
45 Optional(syn::Type),
47 Repeatable(syn::Type),
49}
50
51fn type_ident_is(ty: &Type, name: &str) -> bool {
53 if let Type::Path(TypePath { path, .. }) = ty
54 && let Some(seg) = path.segments.last()
55 {
56 return seg.ident == name;
57 }
58 false
59}
60
61fn extract_generic_inner(ty: &Type) -> Option<syn::Type> {
63 if let Type::Path(TypePath { path, .. }) = ty
64 && let Some(seg) = path.segments.last()
65 && let PathArguments::AngleBracketed(ref args) = seg.arguments
66 && let Some(GenericArgument::Type(inner)) = args.args.first()
67 {
68 return Some(inner.clone());
69 }
70 None
71}
72
73fn is_known_primitive(ty: &Type) -> bool {
75 if let Type::Path(TypePath { path, .. }) = ty
76 && let Some(seg) = path.segments.last()
77 {
78 let name = seg.ident.to_string();
79 if KNOWN_PRIMITIVES.contains(&name.as_str()) {
80 return true;
81 }
82 if (name == "Option" || name == "Vec") && extract_generic_inner(ty).is_some() {
84 return true;
85 }
86 }
87 false
88}
89
90fn classify_param(name: syn::Ident, ty: syn::Type) -> SimpleParam {
92 let kind = if type_ident_is(&ty, "bool") {
93 SimpleParamKind::Bool
94 } else if type_ident_is(&ty, "Option") {
95 let inner = extract_generic_inner(&ty).unwrap();
96 SimpleParamKind::Optional(inner)
97 } else if type_ident_is(&ty, "Vec") {
98 let inner = extract_generic_inner(&ty).unwrap();
99 SimpleParamKind::Repeatable(inner)
100 } else {
101 SimpleParamKind::Required
102 };
103 SimpleParam { name, ty, kind }
104}
105
106fn detect_arg_form(input_fn: &ItemFn) -> Result<ArgForm, syn::Error> {
108 let params: Vec<_> = input_fn
110 .sig
111 .inputs
112 .iter()
113 .skip(1) .collect();
115
116 if params.is_empty() {
117 return Ok(ArgForm::ZeroArgs);
118 }
119
120 if params.len() > 1 {
121 let mut simple_params = Vec::new();
123 for param in params {
124 let (name, ty) = extract_typed_param(param)?;
125 simple_params.push(classify_param(name, ty));
126 }
127 return Ok(ArgForm::SimpleArgs(simple_params));
128 }
129
130 let (name, ty) = extract_typed_param(params[0])?;
132 if is_known_primitive(&ty) {
133 let simple = classify_param(name, ty);
134 return Ok(ArgForm::SimpleArgs(vec![simple]));
135 }
136
137 Ok(ArgForm::ParserStruct {
139 param_name: name,
140 param_type: Box::new(ty),
141 })
142}
143
144fn extract_typed_param(arg: &FnArg) -> Result<(syn::Ident, syn::Type), syn::Error> {
146 match arg {
147 FnArg::Typed(pat_type) => {
148 let name = match pat_type.pat.as_ref() {
149 Pat::Ident(pat_ident) => pat_ident.ident.clone(),
150 other => {
151 return Err(syn::Error::new_spanned(
152 other,
153 "expected a simple identifier pattern for task parameter",
154 ));
155 }
156 };
157 Ok((name, (*pat_type.ty).clone()))
158 }
159 FnArg::Receiver(r) => Err(syn::Error::new_spanned(
160 r,
161 "task functions cannot have a `self` parameter",
162 )),
163 }
164}
165
166#[proc_macro_attribute]
223pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
224 let mut input_fn = parse_macro_input!(item as ItemFn);
225 let fn_name = input_fn.sig.ident.clone();
226 let fn_name_str = fn_name.to_string();
227 let is_async = input_fn.sig.asyncness.is_some();
228
229 let body_name = syn::Ident::new(&format!("__rnme_body_{}", fn_name), fn_name.span());
235
236 let TaskFnMeta {
237 desc_tokens,
238 ui_hint_tokens,
239 arg_form,
240 } = match parse_task_attrs_and_meta(attr, &input_fn) {
241 Ok(m) => m,
242 Err(e) => return e.to_compile_error().into(),
243 };
244
245 {
247 let task_name_str = &fn_name_str;
248 let ctx_ident = match input_fn.sig.inputs.first() {
250 Some(FnArg::Typed(pat_type)) => match pat_type.pat.as_ref() {
251 Pat::Ident(pat_ident) => pat_ident.ident.clone(),
252 _ => syn::Ident::new("ctx", proc_macro2::Span::call_site()),
253 },
254 _ => syn::Ident::new("ctx", proc_macro2::Span::call_site()),
255 };
256 let start_task_stmt: syn::Stmt = syn::parse_quote! {
257 let _task = #ctx_ident.start_task(#task_name_str);
258 };
259 input_fn.block.stmts.insert(0, start_task_stmt);
260 }
261
262 let wrapper_name = syn::Ident::new(&format!("__runme_taskfn_{}", fn_name), fn_name.span());
264
265 let arg_metadata_name =
267 syn::Ident::new(&format!("__runme_argmeta_{}", fn_name), fn_name.span());
268
269 let taskdef_static_name =
272 syn::Ident::new(&format!("__RNME_TASKDEF_{}", fn_name), fn_name.span());
273
274 let has_return_type = !matches!(input_fn.sig.output, ReturnType::Default);
276
277 let typed_params: Vec<(syn::Ident, syn::Type)> = input_fn
284 .sig
285 .inputs
286 .iter()
287 .skip(1)
288 .filter_map(|arg| match arg {
289 FnArg::Typed(pat_type) => match pat_type.pat.as_ref() {
290 Pat::Ident(pat_ident) => Some((pat_ident.ident.clone(), (*pat_type.ty).clone())),
291 _ => None,
292 },
293 FnArg::Receiver(_) => None,
294 })
295 .collect();
296 let shim_param_decls: Vec<proc_macro2::TokenStream> = typed_params
297 .iter()
298 .map(|(name, ty)| quote! { #name: #ty })
299 .collect();
300 let shim_param_idents: Vec<syn::Ident> =
301 typed_params.iter().map(|(name, _)| name.clone()).collect();
302
303 input_fn.sig.ident = body_name.clone();
309 input_fn.vis = syn::Visibility::Inherited;
313
314 let (parse_block, fn_call, arg_metadata_tokens) = match &arg_form {
324 ArgForm::ZeroArgs => {
325 let parse = quote! {};
326 let call = quote! { #body_name(ctx) };
327 let metadata = quote! {
328 fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
329 None
330 }
331 };
332 (parse, call, metadata)
333 }
334 ArgForm::SimpleArgs(params) => {
335 let (parse_stmts, call_args, cmd_build) =
336 generate_simple_args(fn_name_str.clone(), params);
337 let parse = parse_stmts;
338 let call = quote! { #body_name(ctx, #(#call_args),*) };
339 let metadata = quote! {
340 fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
341 Some({ #cmd_build })
342 }
343 };
344 (parse, call, metadata)
345 }
346 ArgForm::ParserStruct {
347 param_name: _,
348 param_type,
349 } => {
350 let parse = quote! {
351 let __parsed = match <#param_type as ::rnme::clap::Parser>::try_parse_from(
352 ::std::iter::once(::std::string::String::from(#fn_name_str))
353 .chain(__args.iter().cloned())
354 ) {
355 Ok(v) => v,
356 Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
357 Err(::rnme::error::TaskError::from_display(e))
358 )),
359 };
360 };
361 let call = quote! { #body_name(ctx, __parsed) };
362 let metadata = quote! {
363 fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
364 Some(<#param_type as ::rnme::clap::CommandFactory>::command())
365 }
366 };
367 (parse, call, metadata)
368 }
369 };
370
371 let wrapper = match (is_async, has_return_type) {
379 (true, true) => {
380 quote! {
382 fn #wrapper_name<'__runme_lt>(ctx: &'__runme_lt ::rnme::task::TaskContext, __args: &[String]) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::rnme::error::TaskError>> + Send + '__runme_lt>> {
383 #parse_block
384 ::std::boxed::Box::pin(async move { #fn_call .await })
385 }
386 }
387 }
388 (true, false) => {
389 quote! {
391 fn #wrapper_name<'__runme_lt>(ctx: &'__runme_lt ::rnme::task::TaskContext, __args: &[String]) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::rnme::error::TaskError>> + Send + '__runme_lt>> {
392 #parse_block
393 ::std::boxed::Box::pin(async move {
394 #fn_call .await;
395 Ok(())
396 })
397 }
398 }
399 }
400 (false, true) => {
401 quote! {
403 fn #wrapper_name<'__runme_lt>(ctx: &'__runme_lt ::rnme::task::TaskContext, __args: &[String]) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::rnme::error::TaskError>> + Send + '__runme_lt>> {
404 #parse_block
405 let result = #fn_call;
406 ::std::boxed::Box::pin(::std::future::ready(result))
407 }
408 }
409 }
410 (false, false) => {
411 quote! {
413 fn #wrapper_name<'__runme_lt>(ctx: &'__runme_lt ::rnme::task::TaskContext, __args: &[String]) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::rnme::error::TaskError>> + Send + '__runme_lt>> {
414 #parse_block
415 #fn_call;
416 ::std::boxed::Box::pin(::std::future::ready(Ok(())))
417 }
418 }
419 }
420 };
421
422 let shim_body_expr = match (is_async, has_return_type) {
428 (true, true) => quote! {
429 #body_name(body_ctx, #(#shim_param_idents),*).await
430 },
431 (true, false) => quote! {
432 #body_name(body_ctx, #(#shim_param_idents),*).await;
433 ::std::result::Result::Ok(())
434 },
435 (false, true) => quote! {
436 #body_name(body_ctx, #(#shim_param_idents),*)
437 },
438 (false, false) => quote! {
439 #body_name(body_ctx, #(#shim_param_idents),*);
440 ::std::result::Result::Ok(())
441 },
442 };
443
444 let shim = quote! {
451 #[must_use = "task builders do nothing until `.await` or `.spawn()` — \
452 a bare call constructs the builder and drops it"]
453 pub fn #fn_name(
454 ctx: &::rnme::task::TaskContext,
455 #(#shim_param_decls,)*
456 ) -> ::rnme::execution::builder::TaskBuilder {
457 ::rnme::execution::builder::TaskBuilder::from_factory(
458 ctx,
459 &#taskdef_static_name,
460 ::std::boxed::Box::new(move |body_ctx: &::rnme::task::TaskContext| {
461 ::std::boxed::Box::pin(async move {
462 #shim_body_expr
463 })
464 }),
465 )
466 }
467 };
468
469 let expanded = quote! {
476 #input_fn
477
478 #wrapper
479
480 #arg_metadata_tokens
481
482 #[allow(non_upper_case_globals)]
483 pub static #taskdef_static_name: ::rnme::task::TaskDef = ::rnme::task::TaskDef {
484 name: #fn_name_str,
485 description: #desc_tokens,
486 group: __RNME_GROUP,
487 dir: __RNME_DIR,
488 func: ::rnme::task::TaskFnKind::Static(#wrapper_name),
489 arg_metadata: #arg_metadata_name,
490 ui_hint: #ui_hint_tokens,
491 };
492
493 ::rnme::inventory::submit! {
494 ::rnme::task::TaskDefRef(&#taskdef_static_name)
495 }
496
497 #shim
498 };
499
500 expanded.into()
501}
502
503#[proc_macro_attribute]
520pub fn task_template(attr: TokenStream, item: TokenStream) -> TokenStream {
521 let mut input_fn = parse_macro_input!(item as ItemFn);
522 let fn_name = input_fn.sig.ident.clone();
523 let fn_name_str = fn_name.to_string();
524 let is_async = input_fn.sig.asyncness.is_some();
525
526 let body_name = syn::Ident::new(&format!("__rnme_body_{}", fn_name), fn_name.span());
530 let wrapper_name = syn::Ident::new(&format!("__runme_taskfn_{}", fn_name), fn_name.span());
531 let arg_metadata_name =
532 syn::Ident::new(&format!("__runme_argmeta_{}", fn_name), fn_name.span());
533 let stamp_macro_name =
534 syn::Ident::new(&format!("__rnme_stamp_{}", fn_name), fn_name.span());
535
536 let TaskFnMeta {
537 desc_tokens,
538 ui_hint_tokens,
539 arg_form,
540 } = match parse_task_attrs_and_meta(attr, &input_fn) {
541 Ok(m) => m,
542 Err(e) => return e.to_compile_error().into(),
543 };
544
545 let has_return_type = !matches!(input_fn.sig.output, ReturnType::Default);
546
547 let typed_params: Vec<(syn::Ident, syn::Type)> = input_fn
552 .sig
553 .inputs
554 .iter()
555 .skip(1)
556 .filter_map(|arg| match arg {
557 FnArg::Typed(pat_type) => match pat_type.pat.as_ref() {
558 Pat::Ident(pat_ident) => Some((pat_ident.ident.clone(), (*pat_type.ty).clone())),
559 _ => None,
560 },
561 FnArg::Receiver(_) => None,
562 })
563 .collect();
564 let shim_param_decls: Vec<proc_macro2::TokenStream> = typed_params
565 .iter()
566 .map(|(name, ty)| quote! { #name: #ty })
567 .collect();
568 let shim_param_idents: Vec<syn::Ident> =
569 typed_params.iter().map(|(name, _)| name.clone()).collect();
570
571 input_fn.sig.ident = body_name.clone();
578 input_fn.vis = syn::Visibility::Public(syn::Token));
579
580 let (parse_block, fn_call, arg_metadata_tokens) = match &arg_form {
585 ArgForm::ZeroArgs => {
586 let parse = quote! {};
587 let call = quote! { #body_name(ctx) };
588 let metadata = quote! {
589 pub fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
590 None
591 }
592 };
593 (parse, call, metadata)
594 }
595 ArgForm::SimpleArgs(params) => {
596 let (parse_stmts, call_args, cmd_build) =
597 generate_simple_args(fn_name_str.clone(), params);
598 let parse = parse_stmts;
599 let call = quote! { #body_name(ctx, #(#call_args),*) };
600 let metadata = quote! {
601 pub fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
602 Some({ #cmd_build })
603 }
604 };
605 (parse, call, metadata)
606 }
607 ArgForm::ParserStruct {
608 param_name: _,
609 param_type,
610 } => {
611 let parse = quote! {
612 let __parsed = match <#param_type as ::rnme::clap::Parser>::try_parse_from(
613 ::std::iter::once(::std::string::String::from(#fn_name_str))
614 .chain(__args.iter().cloned())
615 ) {
616 Ok(v) => v,
617 Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
618 Err(::rnme::error::TaskError::from_display(e))
619 )),
620 };
621 };
622 let call = quote! { #body_name(ctx, __parsed) };
623 let metadata = quote! {
624 pub fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
625 Some(<#param_type as ::rnme::clap::CommandFactory>::command())
626 }
627 };
628 (parse, call, metadata)
629 }
630 };
631
632 let wrapper = match (is_async, has_return_type) {
633 (true, true) => quote! {
634 pub fn #wrapper_name<'__runme_lt>(ctx: &'__runme_lt ::rnme::task::TaskContext, __args: &[String]) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::rnme::error::TaskError>> + Send + '__runme_lt>> {
635 #parse_block
636 ::std::boxed::Box::pin(async move { #fn_call .await })
637 }
638 },
639 (true, false) => quote! {
640 pub fn #wrapper_name<'__runme_lt>(ctx: &'__runme_lt ::rnme::task::TaskContext, __args: &[String]) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::rnme::error::TaskError>> + Send + '__runme_lt>> {
641 #parse_block
642 ::std::boxed::Box::pin(async move {
643 #fn_call .await;
644 Ok(())
645 })
646 }
647 },
648 (false, true) => quote! {
649 pub fn #wrapper_name<'__runme_lt>(ctx: &'__runme_lt ::rnme::task::TaskContext, __args: &[String]) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::rnme::error::TaskError>> + Send + '__runme_lt>> {
650 #parse_block
651 let result = #fn_call;
652 ::std::boxed::Box::pin(::std::future::ready(result))
653 }
654 },
655 (false, false) => quote! {
656 pub fn #wrapper_name<'__runme_lt>(ctx: &'__runme_lt ::rnme::task::TaskContext, __args: &[String]) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::std::result::Result<(), ::rnme::error::TaskError>> + Send + '__runme_lt>> {
657 #parse_block
658 #fn_call;
659 ::std::boxed::Box::pin(::std::future::ready(Ok(())))
660 }
661 },
662 };
663
664 let shim_body_expr = match (is_async, has_return_type) {
668 (true, true) => quote! {
669 $crate::#body_name(body_ctx, #(#shim_param_idents),*).await
670 },
671 (true, false) => quote! {
672 $crate::#body_name(body_ctx, #(#shim_param_idents),*).await;
673 ::std::result::Result::Ok(())
674 },
675 (false, true) => quote! {
676 $crate::#body_name(body_ctx, #(#shim_param_idents),*)
677 },
678 (false, false) => quote! {
679 $crate::#body_name(body_ctx, #(#shim_param_idents),*);
680 ::std::result::Result::Ok(())
681 },
682 };
683
684 let stamp_wrapper_name =
705 syn::Ident::new(&format!("__runme_taskfn_{}", fn_name), fn_name.span());
706 let stamp_taskdef_name =
707 syn::Ident::new(&format!("__RNME_TASKDEF_{}", fn_name), fn_name.span());
708
709 let stamp_macro = quote! {
710 #[macro_export]
711 #[doc(hidden)]
712 macro_rules! #stamp_macro_name {
713 () => {
714 #[allow(non_snake_case)]
719 fn #stamp_wrapper_name<'__runme_lt>(
720 ctx: &'__runme_lt ::rnme::task::TaskContext,
721 __args: &[::std::string::String],
722 ) -> ::std::pin::Pin<::std::boxed::Box<
723 dyn ::std::future::Future<
724 Output = ::std::result::Result<(), ::rnme::error::TaskError>,
725 > + ::std::marker::Send + '__runme_lt,
726 >> {
727 let __inner = $crate::#wrapper_name(ctx, __args);
728 ::std::boxed::Box::pin(async move {
729 let _task = ctx.start_task(#fn_name_str);
730 __inner.await
731 })
732 }
733
734 #[allow(non_upper_case_globals)]
735 pub static #stamp_taskdef_name: ::rnme::task::TaskDef = ::rnme::task::TaskDef {
736 name: #fn_name_str,
737 description: #desc_tokens,
738 group: __RNME_GROUP,
739 dir: __RNME_DIR,
740 func: ::rnme::task::TaskFnKind::Static(#stamp_wrapper_name),
741 arg_metadata: $crate::#arg_metadata_name,
742 ui_hint: #ui_hint_tokens,
743 };
744
745 ::rnme::inventory::submit! {
746 ::rnme::task::TaskDefRef(&#stamp_taskdef_name)
747 }
748
749 #[must_use = "task builders do nothing until `.await` or `.spawn()` — \
750 a bare call constructs the builder and drops it"]
751 pub fn #fn_name(
752 ctx: &::rnme::task::TaskContext,
753 #(#shim_param_decls,)*
754 ) -> ::rnme::execution::builder::TaskBuilder {
755 ::rnme::execution::builder::TaskBuilder::from_factory(
756 ctx,
757 &#stamp_taskdef_name,
758 ::std::boxed::Box::new(move |body_ctx: &::rnme::task::TaskContext| {
759 ::std::boxed::Box::pin(async move {
760 let _task = body_ctx.start_task(#fn_name_str);
761 #shim_body_expr
762 })
763 }),
764 )
765 }
766 };
767 }
768 };
769
770 let expanded = quote! {
771 #input_fn
772
773 #wrapper
774
775 #arg_metadata_tokens
776
777 #stamp_macro
778 };
779
780 expanded.into()
781}
782
783struct TaskFnMeta {
789 desc_tokens: proc_macro2::TokenStream,
790 ui_hint_tokens: proc_macro2::TokenStream,
791 arg_form: ArgForm,
792}
793
794fn parse_task_attrs_and_meta(
795 attr: TokenStream,
796 input_fn: &ItemFn,
797) -> Result<TaskFnMeta, syn::Error> {
798 let attr_parser = syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated;
800 let parsed_attrs = syn::parse::Parser::parse(attr_parser, attr)?;
801
802 let mut ui_hint: Option<proc_macro2::TokenStream> = None;
803 for meta in parsed_attrs {
804 match meta {
805 Meta::NameValue(MetaNameValue { path, value, .. }) => {
806 let key = path.get_ident().map(|i| i.to_string()).unwrap_or_default();
807 match key.as_str() {
808 "mode" => {
809 let mode_str = match &value {
810 Expr::Path(p) => match p.path.get_ident() {
811 Some(i) => i.to_string(),
812 None => {
813 return Err(syn::Error::new_spanned(
814 value,
815 "expected `cli` or `tui`",
816 ));
817 }
818 },
819 Expr::Lit(ExprLit {
820 lit: Lit::Str(s), ..
821 }) => s.value(),
822 _ => {
823 return Err(syn::Error::new_spanned(
824 value,
825 "expected `cli` or `tui` (bare ident or string literal)",
826 ));
827 }
828 };
829 ui_hint = Some(match mode_str.as_str() {
830 "cli" | "Cli" | "CLI" => {
831 quote! { Some(::rnme::task::UiHint::Cli) }
832 }
833 "tui" | "Tui" | "TUI" => {
834 quote! { Some(::rnme::task::UiHint::Tui) }
835 }
836 other => {
837 return Err(syn::Error::new_spanned(
838 value,
839 format!("unknown mode `{}` — expected `cli` or `tui`", other),
840 ));
841 }
842 });
843 }
844 "desc" | "description" => {
845 return Err(syn::Error::new_spanned(
846 path,
847 "task descriptions come from `///` doc comments — \
848 remove this attribute and write a `///` line above the fn",
849 ));
850 }
851 other => {
852 return Err(syn::Error::new_spanned(
853 path,
854 format!("unknown attribute: {}", other),
855 ));
856 }
857 }
858 }
859 other => {
860 return Err(syn::Error::new_spanned(other, "expected `key = value` format"));
861 }
862 }
863 }
864
865 let ui_hint_tokens = ui_hint.unwrap_or_else(|| quote! { None });
866
867 let doc_lines: Vec<String> = input_fn
869 .attrs
870 .iter()
871 .filter_map(|attr| {
872 if attr.path().is_ident("doc")
873 && let Meta::NameValue(MetaNameValue {
874 value:
875 Expr::Lit(ExprLit {
876 lit: Lit::Str(s), ..
877 }),
878 ..
879 }) = &attr.meta
880 {
881 return Some(s.value().trim().to_string());
882 }
883 None
884 })
885 .collect();
886 let desc_tokens = if doc_lines.is_empty() {
887 quote! { None }
888 } else {
889 let joined = doc_lines.join(" ");
890 quote! { Some(#joined) }
891 };
892
893 let arg_form = detect_arg_form(input_fn)?;
894
895 Ok(TaskFnMeta {
896 desc_tokens,
897 ui_hint_tokens,
898 arg_form,
899 })
900}
901
902fn generate_simple_args(
905 task_name: String,
906 params: &[SimpleParam],
907) -> (
908 proc_macro2::TokenStream,
909 Vec<proc_macro2::TokenStream>,
910 proc_macro2::TokenStream,
911) {
912 let mut arg_builders = Vec::new();
914 for param in params {
915 let name_str = param.name.to_string();
916 let long_name = name_str.replace('_', "-");
917 let arg_build = match ¶m.kind {
918 SimpleParamKind::Bool => {
919 quote! {
920 ::rnme::clap::Arg::new(#name_str)
921 .long(#long_name)
922 .action(::rnme::clap::ArgAction::SetTrue)
923 }
924 }
925 SimpleParamKind::Required => {
926 quote! {
927 ::rnme::clap::Arg::new(#name_str)
928 .long(#long_name)
929 .required(true)
930 .action(::rnme::clap::ArgAction::Set)
931 }
932 }
933 SimpleParamKind::Optional(_) => {
934 quote! {
935 ::rnme::clap::Arg::new(#name_str)
936 .long(#long_name)
937 .required(false)
938 .action(::rnme::clap::ArgAction::Set)
939 }
940 }
941 SimpleParamKind::Repeatable(_) => {
942 quote! {
943 ::rnme::clap::Arg::new(#name_str)
944 .long(#long_name)
945 .action(::rnme::clap::ArgAction::Append)
946 }
947 }
948 };
949 arg_builders.push(arg_build);
950 }
951
952 let cmd_build = quote! {
954 ::rnme::clap::Command::new(#task_name)
955 #(.arg(#arg_builders))*
956 };
957
958 let mut parse_stmts = Vec::new();
960 let mut call_args = Vec::new();
961
962 let parse_match = quote! {
965 let __clap_matches = match ({
966 #cmd_build
967 }).try_get_matches_from(
968 ::std::iter::once(::std::string::String::from(#task_name))
969 .chain(__args.iter().cloned())
970 ) {
971 Ok(m) => m,
972 Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
973 Err(::rnme::error::TaskError::from_display(e))
974 )),
975 };
976 };
977 parse_stmts.push(parse_match);
978
979 for param in params {
981 let param_name = ¶m.name;
982 let name_str = param.name.to_string();
983 let ty = ¶m.ty;
984
985 let extract = match ¶m.kind {
986 SimpleParamKind::Bool => {
987 quote! {
988 let #param_name: #ty = __clap_matches.get_flag(#name_str);
989 }
990 }
991 SimpleParamKind::Required => {
992 quote! {
993 let #param_name: #ty = match __clap_matches.get_one::<String>(#name_str) {
994 Some(v) => match v.parse::<#ty>() {
995 Ok(parsed) => parsed,
996 Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
997 Err(::rnme::error::TaskError::from_display(
998 format!("invalid value for --{}: {}", #name_str, e)
999 ))
1000 )),
1001 },
1002 None => return ::std::boxed::Box::pin(::std::future::ready(
1003 Err(::rnme::error::TaskError::from_display(
1004 format!("missing required argument: --{}", #name_str)
1005 ))
1006 )),
1007 };
1008 }
1009 }
1010 SimpleParamKind::Optional(inner) => {
1011 quote! {
1012 let #param_name: #ty = match __clap_matches.get_one::<String>(#name_str)
1013 .map(|v| v.parse::<#inner>())
1014 .transpose()
1015 {
1016 Ok(v) => v,
1017 Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
1018 Err(::rnme::error::TaskError::from_display(
1019 format!("invalid value for --{}: {}", #name_str, e)
1020 ))
1021 )),
1022 };
1023 }
1024 }
1025 SimpleParamKind::Repeatable(inner) => {
1026 quote! {
1027 let #param_name: #ty = match __clap_matches.get_many::<String>(#name_str)
1028 .map(|vals| vals.map(|v| v.parse::<#inner>()).collect::<Result<Vec<_>, _>>())
1029 .transpose()
1030 {
1031 Ok(v) => v.unwrap_or_default(),
1032 Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
1033 Err(::rnme::error::TaskError::from_display(
1034 format!("invalid value for --{}: {}", #name_str, e)
1035 ))
1036 )),
1037 };
1038 }
1039 }
1040 };
1041 parse_stmts.push(extract);
1042 call_args.push(quote! { #param_name });
1043 }
1044
1045 let parse_block = quote! { #(#parse_stmts)* };
1046 (parse_block, call_args, cmd_build)
1047}
1048
1049#[proc_macro]
1071pub fn import_task(input: TokenStream) -> TokenStream {
1072 let path: syn::Path = match syn::parse(input) {
1073 Ok(p) => p,
1074 Err(e) => return e.to_compile_error().into(),
1075 };
1076
1077 if path.segments.is_empty() {
1078 return syn::Error::new_spanned(&path, "expected a path like `lib_crate::task_name`")
1079 .to_compile_error()
1080 .into();
1081 }
1082
1083 let mut lib_path = path.clone();
1084 let task_seg = lib_path
1087 .segments
1088 .pop()
1089 .expect("at least one segment, checked above")
1090 .into_value();
1091
1092 if !task_seg.arguments.is_empty() {
1093 return syn::Error::new_spanned(
1094 &task_seg.arguments,
1095 "task name must not carry generic arguments",
1096 )
1097 .to_compile_error()
1098 .into();
1099 }
1100
1101 if lib_path.segments.is_empty() {
1102 return syn::Error::new_spanned(
1103 &path,
1104 "expected `lib_crate::task_name` — a library path followed by the task name",
1105 )
1106 .to_compile_error()
1107 .into();
1108 }
1109 lib_path.segments.pop_punct();
1111
1112 let task_ident = &task_seg.ident;
1113 let stamp_ident = syn::Ident::new(
1114 &format!("__rnme_stamp_{}", task_ident),
1115 task_ident.span(),
1116 );
1117
1118 let expanded = quote! {
1119 #lib_path :: #stamp_ident !();
1120 };
1121 expanded.into()
1122}
1123
1124#[proc_macro_attribute]
1151pub fn init(_attr: TokenStream, item: TokenStream) -> TokenStream {
1152 let input_fn = parse_macro_input!(item as ItemFn);
1153 let fn_name = &input_fn.sig.ident;
1154
1155 let has_ctx_arg = !input_fn.sig.inputs.is_empty();
1157
1158 let wrapper_name = syn::Ident::new(&format!("__runme_initfn_{}", fn_name), fn_name.span());
1160
1161 let wrapper = if has_ctx_arg {
1163 quote! {
1165 fn #wrapper_name(ctx: &mut ::rnme::init::InitContext) {
1166 #fn_name(ctx)
1167 }
1168 }
1169 } else {
1170 quote! {
1172 fn #wrapper_name(_ctx: &mut ::rnme::init::InitContext) {
1173 #fn_name()
1174 }
1175 }
1176 };
1177
1178 let expanded = quote! {
1179 #input_fn
1180
1181 #wrapper
1182
1183 ::rnme::inventory::submit! {
1184 ::rnme::init::InitDef {
1185 group: __RNME_GROUP,
1186 dir: __RNME_DIR,
1187 func: #wrapper_name,
1188 }
1189 }
1190 };
1191
1192 expanded.into()
1193}
1194
1195#[proc_macro]
1213pub fn cmd(input: TokenStream) -> TokenStream {
1214 match cmd_macro::expand_cmd(input.into()) {
1215 Ok(tokens) => tokens.into(),
1216 Err(e) => e.to_compile_error().into(),
1217 }
1218}