mod cmd_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Expr, ExprLit, FnArg, GenericArgument, ItemFn, Lit, Meta, MetaNameValue, Pat, PathArguments,
ReturnType, Type, TypePath, parse_macro_input,
};
const KNOWN_PRIMITIVES: &[&str] = &[
"String", "bool", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128", "f32",
"f64", "usize", "isize",
];
enum ArgForm {
ZeroArgs,
SimpleArgs(Vec<SimpleParam>),
ParserStruct {
#[allow(dead_code)]
param_name: syn::Ident,
param_type: Box<syn::Type>,
},
}
struct SimpleParam {
name: syn::Ident,
ty: syn::Type,
kind: SimpleParamKind,
}
enum SimpleParamKind {
Bool,
Required,
Optional(syn::Type),
Repeatable(syn::Type),
}
fn type_ident_is(ty: &Type, name: &str) -> bool {
if let Type::Path(TypePath { path, .. }) = ty
&& let Some(seg) = path.segments.last()
{
return seg.ident == name;
}
false
}
fn extract_generic_inner(ty: &Type) -> Option<syn::Type> {
if let Type::Path(TypePath { path, .. }) = ty
&& let Some(seg) = path.segments.last()
&& let PathArguments::AngleBracketed(ref args) = seg.arguments
&& let Some(GenericArgument::Type(inner)) = args.args.first()
{
return Some(inner.clone());
}
None
}
fn is_known_primitive(ty: &Type) -> bool {
if let Type::Path(TypePath { path, .. }) = ty
&& let Some(seg) = path.segments.last()
{
let name = seg.ident.to_string();
if KNOWN_PRIMITIVES.contains(&name.as_str()) {
return true;
}
if (name == "Option" || name == "Vec") && extract_generic_inner(ty).is_some() {
return true;
}
}
false
}
fn classify_param(name: syn::Ident, ty: syn::Type) -> SimpleParam {
let kind = if type_ident_is(&ty, "bool") {
SimpleParamKind::Bool
} else if type_ident_is(&ty, "Option") {
let inner = extract_generic_inner(&ty).unwrap();
SimpleParamKind::Optional(inner)
} else if type_ident_is(&ty, "Vec") {
let inner = extract_generic_inner(&ty).unwrap();
SimpleParamKind::Repeatable(inner)
} else {
SimpleParamKind::Required
};
SimpleParam { name, ty, kind }
}
fn detect_arg_form(input_fn: &ItemFn) -> Result<ArgForm, syn::Error> {
let params: Vec<_> = input_fn
.sig
.inputs
.iter()
.skip(1) .collect();
if params.is_empty() {
return Ok(ArgForm::ZeroArgs);
}
if params.len() > 1 {
let mut simple_params = Vec::new();
for param in params {
let (name, ty) = extract_typed_param(param)?;
simple_params.push(classify_param(name, ty));
}
return Ok(ArgForm::SimpleArgs(simple_params));
}
let (name, ty) = extract_typed_param(params[0])?;
if is_known_primitive(&ty) {
let simple = classify_param(name, ty);
return Ok(ArgForm::SimpleArgs(vec![simple]));
}
Ok(ArgForm::ParserStruct {
param_name: name,
param_type: Box::new(ty),
})
}
fn extract_typed_param(arg: &FnArg) -> Result<(syn::Ident, syn::Type), syn::Error> {
match arg {
FnArg::Typed(pat_type) => {
let name = match pat_type.pat.as_ref() {
Pat::Ident(pat_ident) => pat_ident.ident.clone(),
other => {
return Err(syn::Error::new_spanned(
other,
"expected a simple identifier pattern for task parameter",
));
}
};
Ok((name, (*pat_type.ty).clone()))
}
FnArg::Receiver(r) => Err(syn::Error::new_spanned(
r,
"task functions cannot have a `self` parameter",
)),
}
}
#[proc_macro_attribute]
pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
let mut input_fn = parse_macro_input!(item as ItemFn);
let fn_name = input_fn.sig.ident.clone();
let fn_name_str = fn_name.to_string();
let is_async = input_fn.sig.asyncness.is_some();
let body_name = syn::Ident::new(&format!("__rnme_body_{}", fn_name), fn_name.span());
let TaskFnMeta {
desc_tokens,
ui_hint_tokens,
arg_form,
} = match parse_task_attrs_and_meta(attr, &input_fn) {
Ok(m) => m,
Err(e) => return e.to_compile_error().into(),
};
{
let task_name_str = &fn_name_str;
let ctx_ident = match input_fn.sig.inputs.first() {
Some(FnArg::Typed(pat_type)) => match pat_type.pat.as_ref() {
Pat::Ident(pat_ident) => pat_ident.ident.clone(),
_ => syn::Ident::new("ctx", proc_macro2::Span::call_site()),
},
_ => syn::Ident::new("ctx", proc_macro2::Span::call_site()),
};
let start_task_stmt: syn::Stmt = syn::parse_quote! {
let _task = #ctx_ident.start_task(#task_name_str);
};
input_fn.block.stmts.insert(0, start_task_stmt);
}
let wrapper_name = syn::Ident::new(&format!("__runme_taskfn_{}", fn_name), fn_name.span());
let arg_metadata_name =
syn::Ident::new(&format!("__runme_argmeta_{}", fn_name), fn_name.span());
let taskdef_static_name =
syn::Ident::new(&format!("__RNME_TASKDEF_{}", fn_name), fn_name.span());
let has_return_type = !matches!(input_fn.sig.output, ReturnType::Default);
let typed_params: Vec<(syn::Ident, syn::Type)> = input_fn
.sig
.inputs
.iter()
.skip(1)
.filter_map(|arg| match arg {
FnArg::Typed(pat_type) => match pat_type.pat.as_ref() {
Pat::Ident(pat_ident) => Some((pat_ident.ident.clone(), (*pat_type.ty).clone())),
_ => None,
},
FnArg::Receiver(_) => None,
})
.collect();
let shim_param_decls: Vec<proc_macro2::TokenStream> = typed_params
.iter()
.map(|(name, ty)| quote! { #name: #ty })
.collect();
let shim_param_idents: Vec<syn::Ident> =
typed_params.iter().map(|(name, _)| name.clone()).collect();
input_fn.sig.ident = body_name.clone();
input_fn.vis = syn::Visibility::Inherited;
let (parse_block, fn_call, arg_metadata_tokens) = match &arg_form {
ArgForm::ZeroArgs => {
let parse = quote! {};
let call = quote! { #body_name(ctx) };
let metadata = quote! {
fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
None
}
};
(parse, call, metadata)
}
ArgForm::SimpleArgs(params) => {
let (parse_stmts, call_args, cmd_build) =
generate_simple_args(fn_name_str.clone(), params);
let parse = parse_stmts;
let call = quote! { #body_name(ctx, #(#call_args),*) };
let metadata = quote! {
fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
Some({ #cmd_build })
}
};
(parse, call, metadata)
}
ArgForm::ParserStruct {
param_name: _,
param_type,
} => {
let parse = quote! {
let __parsed = match <#param_type as ::rnme::clap::Parser>::try_parse_from(
::std::iter::once(::std::string::String::from(#fn_name_str))
.chain(__args.iter().cloned())
) {
Ok(v) => v,
Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
Err(::rnme::error::TaskError::from_display(e))
)),
};
};
let call = quote! { #body_name(ctx, __parsed) };
let metadata = quote! {
fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
Some(<#param_type as ::rnme::clap::CommandFactory>::command())
}
};
(parse, call, metadata)
}
};
let wrapper = match (is_async, has_return_type) {
(true, true) => {
quote! {
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>> {
#parse_block
::std::boxed::Box::pin(async move { #fn_call .await })
}
}
}
(true, false) => {
quote! {
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>> {
#parse_block
::std::boxed::Box::pin(async move {
#fn_call .await;
Ok(())
})
}
}
}
(false, true) => {
quote! {
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>> {
#parse_block
let result = #fn_call;
::std::boxed::Box::pin(::std::future::ready(result))
}
}
}
(false, false) => {
quote! {
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>> {
#parse_block
#fn_call;
::std::boxed::Box::pin(::std::future::ready(Ok(())))
}
}
}
};
let shim_body_expr = match (is_async, has_return_type) {
(true, true) => quote! {
#body_name(body_ctx, #(#shim_param_idents),*).await
},
(true, false) => quote! {
#body_name(body_ctx, #(#shim_param_idents),*).await;
::std::result::Result::Ok(())
},
(false, true) => quote! {
#body_name(body_ctx, #(#shim_param_idents),*)
},
(false, false) => quote! {
#body_name(body_ctx, #(#shim_param_idents),*);
::std::result::Result::Ok(())
},
};
let shim = quote! {
#[must_use = "task builders do nothing until `.await` or `.spawn()` — \
a bare call constructs the builder and drops it"]
pub fn #fn_name(
ctx: &::rnme::task::TaskContext,
#(#shim_param_decls,)*
) -> ::rnme::execution::builder::TaskBuilder {
::rnme::execution::builder::TaskBuilder::from_factory(
ctx,
&#taskdef_static_name,
::std::boxed::Box::new(move |body_ctx: &::rnme::task::TaskContext| {
::std::boxed::Box::pin(async move {
#shim_body_expr
})
}),
)
}
};
let expanded = quote! {
#input_fn
#wrapper
#arg_metadata_tokens
#[allow(non_upper_case_globals)]
pub static #taskdef_static_name: ::rnme::task::TaskDef = ::rnme::task::TaskDef {
name: #fn_name_str,
description: #desc_tokens,
group: __RNME_GROUP,
dir: __RNME_DIR,
func: ::rnme::task::TaskFnKind::Static(#wrapper_name),
arg_metadata: #arg_metadata_name,
ui_hint: #ui_hint_tokens,
};
::rnme::inventory::submit! {
::rnme::task::TaskDefRef(&#taskdef_static_name)
}
#shim
};
expanded.into()
}
#[proc_macro_attribute]
pub fn task_template(attr: TokenStream, item: TokenStream) -> TokenStream {
let mut input_fn = parse_macro_input!(item as ItemFn);
let fn_name = input_fn.sig.ident.clone();
let fn_name_str = fn_name.to_string();
let is_async = input_fn.sig.asyncness.is_some();
let body_name = syn::Ident::new(&format!("__rnme_body_{}", fn_name), fn_name.span());
let wrapper_name = syn::Ident::new(&format!("__runme_taskfn_{}", fn_name), fn_name.span());
let arg_metadata_name =
syn::Ident::new(&format!("__runme_argmeta_{}", fn_name), fn_name.span());
let stamp_macro_name =
syn::Ident::new(&format!("__rnme_stamp_{}", fn_name), fn_name.span());
let TaskFnMeta {
desc_tokens,
ui_hint_tokens,
arg_form,
} = match parse_task_attrs_and_meta(attr, &input_fn) {
Ok(m) => m,
Err(e) => return e.to_compile_error().into(),
};
let has_return_type = !matches!(input_fn.sig.output, ReturnType::Default);
let typed_params: Vec<(syn::Ident, syn::Type)> = input_fn
.sig
.inputs
.iter()
.skip(1)
.filter_map(|arg| match arg {
FnArg::Typed(pat_type) => match pat_type.pat.as_ref() {
Pat::Ident(pat_ident) => Some((pat_ident.ident.clone(), (*pat_type.ty).clone())),
_ => None,
},
FnArg::Receiver(_) => None,
})
.collect();
let shim_param_decls: Vec<proc_macro2::TokenStream> = typed_params
.iter()
.map(|(name, ty)| quote! { #name: #ty })
.collect();
let shim_param_idents: Vec<syn::Ident> =
typed_params.iter().map(|(name, _)| name.clone()).collect();
input_fn.sig.ident = body_name.clone();
input_fn.vis = syn::Visibility::Public(syn::Token));
let (parse_block, fn_call, arg_metadata_tokens) = match &arg_form {
ArgForm::ZeroArgs => {
let parse = quote! {};
let call = quote! { #body_name(ctx) };
let metadata = quote! {
pub fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
None
}
};
(parse, call, metadata)
}
ArgForm::SimpleArgs(params) => {
let (parse_stmts, call_args, cmd_build) =
generate_simple_args(fn_name_str.clone(), params);
let parse = parse_stmts;
let call = quote! { #body_name(ctx, #(#call_args),*) };
let metadata = quote! {
pub fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
Some({ #cmd_build })
}
};
(parse, call, metadata)
}
ArgForm::ParserStruct {
param_name: _,
param_type,
} => {
let parse = quote! {
let __parsed = match <#param_type as ::rnme::clap::Parser>::try_parse_from(
::std::iter::once(::std::string::String::from(#fn_name_str))
.chain(__args.iter().cloned())
) {
Ok(v) => v,
Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
Err(::rnme::error::TaskError::from_display(e))
)),
};
};
let call = quote! { #body_name(ctx, __parsed) };
let metadata = quote! {
pub fn #arg_metadata_name() -> Option<::rnme::clap::Command> {
Some(<#param_type as ::rnme::clap::CommandFactory>::command())
}
};
(parse, call, metadata)
}
};
let wrapper = match (is_async, has_return_type) {
(true, true) => quote! {
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>> {
#parse_block
::std::boxed::Box::pin(async move { #fn_call .await })
}
},
(true, false) => quote! {
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>> {
#parse_block
::std::boxed::Box::pin(async move {
#fn_call .await;
Ok(())
})
}
},
(false, true) => quote! {
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>> {
#parse_block
let result = #fn_call;
::std::boxed::Box::pin(::std::future::ready(result))
}
},
(false, false) => quote! {
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>> {
#parse_block
#fn_call;
::std::boxed::Box::pin(::std::future::ready(Ok(())))
}
},
};
let shim_body_expr = match (is_async, has_return_type) {
(true, true) => quote! {
$crate::#body_name(body_ctx, #(#shim_param_idents),*).await
},
(true, false) => quote! {
$crate::#body_name(body_ctx, #(#shim_param_idents),*).await;
::std::result::Result::Ok(())
},
(false, true) => quote! {
$crate::#body_name(body_ctx, #(#shim_param_idents),*)
},
(false, false) => quote! {
$crate::#body_name(body_ctx, #(#shim_param_idents),*);
::std::result::Result::Ok(())
},
};
let stamp_wrapper_name =
syn::Ident::new(&format!("__runme_taskfn_{}", fn_name), fn_name.span());
let stamp_taskdef_name =
syn::Ident::new(&format!("__RNME_TASKDEF_{}", fn_name), fn_name.span());
let stamp_macro = quote! {
#[macro_export]
#[doc(hidden)]
macro_rules! #stamp_macro_name {
() => {
#[allow(non_snake_case)]
fn #stamp_wrapper_name<'__runme_lt>(
ctx: &'__runme_lt ::rnme::task::TaskContext,
__args: &[::std::string::String],
) -> ::std::pin::Pin<::std::boxed::Box<
dyn ::std::future::Future<
Output = ::std::result::Result<(), ::rnme::error::TaskError>,
> + ::std::marker::Send + '__runme_lt,
>> {
let __inner = $crate::#wrapper_name(ctx, __args);
::std::boxed::Box::pin(async move {
let _task = ctx.start_task(#fn_name_str);
__inner.await
})
}
#[allow(non_upper_case_globals)]
pub static #stamp_taskdef_name: ::rnme::task::TaskDef = ::rnme::task::TaskDef {
name: #fn_name_str,
description: #desc_tokens,
group: __RNME_GROUP,
dir: __RNME_DIR,
func: ::rnme::task::TaskFnKind::Static(#stamp_wrapper_name),
arg_metadata: $crate::#arg_metadata_name,
ui_hint: #ui_hint_tokens,
};
::rnme::inventory::submit! {
::rnme::task::TaskDefRef(&#stamp_taskdef_name)
}
#[must_use = "task builders do nothing until `.await` or `.spawn()` — \
a bare call constructs the builder and drops it"]
pub fn #fn_name(
ctx: &::rnme::task::TaskContext,
#(#shim_param_decls,)*
) -> ::rnme::execution::builder::TaskBuilder {
::rnme::execution::builder::TaskBuilder::from_factory(
ctx,
&#stamp_taskdef_name,
::std::boxed::Box::new(move |body_ctx: &::rnme::task::TaskContext| {
::std::boxed::Box::pin(async move {
let _task = body_ctx.start_task(#fn_name_str);
#shim_body_expr
})
}),
)
}
};
}
};
let expanded = quote! {
#input_fn
#wrapper
#arg_metadata_tokens
#stamp_macro
};
expanded.into()
}
struct TaskFnMeta {
desc_tokens: proc_macro2::TokenStream,
ui_hint_tokens: proc_macro2::TokenStream,
arg_form: ArgForm,
}
fn parse_task_attrs_and_meta(
attr: TokenStream,
input_fn: &ItemFn,
) -> Result<TaskFnMeta, syn::Error> {
let attr_parser = syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated;
let parsed_attrs = syn::parse::Parser::parse(attr_parser, attr)?;
let mut ui_hint: Option<proc_macro2::TokenStream> = None;
for meta in parsed_attrs {
match meta {
Meta::NameValue(MetaNameValue { path, value, .. }) => {
let key = path.get_ident().map(|i| i.to_string()).unwrap_or_default();
match key.as_str() {
"mode" => {
let mode_str = match &value {
Expr::Path(p) => match p.path.get_ident() {
Some(i) => i.to_string(),
None => {
return Err(syn::Error::new_spanned(
value,
"expected `cli` or `tui`",
));
}
},
Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) => s.value(),
_ => {
return Err(syn::Error::new_spanned(
value,
"expected `cli` or `tui` (bare ident or string literal)",
));
}
};
ui_hint = Some(match mode_str.as_str() {
"cli" | "Cli" | "CLI" => {
quote! { Some(::rnme::task::UiHint::Cli) }
}
"tui" | "Tui" | "TUI" => {
quote! { Some(::rnme::task::UiHint::Tui) }
}
other => {
return Err(syn::Error::new_spanned(
value,
format!("unknown mode `{}` — expected `cli` or `tui`", other),
));
}
});
}
"desc" | "description" => {
return Err(syn::Error::new_spanned(
path,
"task descriptions come from `///` doc comments — \
remove this attribute and write a `///` line above the fn",
));
}
other => {
return Err(syn::Error::new_spanned(
path,
format!("unknown attribute: {}", other),
));
}
}
}
other => {
return Err(syn::Error::new_spanned(other, "expected `key = value` format"));
}
}
}
let ui_hint_tokens = ui_hint.unwrap_or_else(|| quote! { None });
let doc_lines: Vec<String> = input_fn
.attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc")
&& let Meta::NameValue(MetaNameValue {
value:
Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}),
..
}) = &attr.meta
{
return Some(s.value().trim().to_string());
}
None
})
.collect();
let desc_tokens = if doc_lines.is_empty() {
quote! { None }
} else {
let joined = doc_lines.join(" ");
quote! { Some(#joined) }
};
let arg_form = detect_arg_form(input_fn)?;
Ok(TaskFnMeta {
desc_tokens,
ui_hint_tokens,
arg_form,
})
}
fn generate_simple_args(
task_name: String,
params: &[SimpleParam],
) -> (
proc_macro2::TokenStream,
Vec<proc_macro2::TokenStream>,
proc_macro2::TokenStream,
) {
let mut arg_builders = Vec::new();
for param in params {
let name_str = param.name.to_string();
let long_name = name_str.replace('_', "-");
let arg_build = match ¶m.kind {
SimpleParamKind::Bool => {
quote! {
::rnme::clap::Arg::new(#name_str)
.long(#long_name)
.action(::rnme::clap::ArgAction::SetTrue)
}
}
SimpleParamKind::Required => {
quote! {
::rnme::clap::Arg::new(#name_str)
.long(#long_name)
.required(true)
.action(::rnme::clap::ArgAction::Set)
}
}
SimpleParamKind::Optional(_) => {
quote! {
::rnme::clap::Arg::new(#name_str)
.long(#long_name)
.required(false)
.action(::rnme::clap::ArgAction::Set)
}
}
SimpleParamKind::Repeatable(_) => {
quote! {
::rnme::clap::Arg::new(#name_str)
.long(#long_name)
.action(::rnme::clap::ArgAction::Append)
}
}
};
arg_builders.push(arg_build);
}
let cmd_build = quote! {
::rnme::clap::Command::new(#task_name)
#(.arg(#arg_builders))*
};
let mut parse_stmts = Vec::new();
let mut call_args = Vec::new();
let parse_match = quote! {
let __clap_matches = match ({
#cmd_build
}).try_get_matches_from(
::std::iter::once(::std::string::String::from(#task_name))
.chain(__args.iter().cloned())
) {
Ok(m) => m,
Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
Err(::rnme::error::TaskError::from_display(e))
)),
};
};
parse_stmts.push(parse_match);
for param in params {
let param_name = ¶m.name;
let name_str = param.name.to_string();
let ty = ¶m.ty;
let extract = match ¶m.kind {
SimpleParamKind::Bool => {
quote! {
let #param_name: #ty = __clap_matches.get_flag(#name_str);
}
}
SimpleParamKind::Required => {
quote! {
let #param_name: #ty = match __clap_matches.get_one::<String>(#name_str) {
Some(v) => match v.parse::<#ty>() {
Ok(parsed) => parsed,
Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
Err(::rnme::error::TaskError::from_display(
format!("invalid value for --{}: {}", #name_str, e)
))
)),
},
None => return ::std::boxed::Box::pin(::std::future::ready(
Err(::rnme::error::TaskError::from_display(
format!("missing required argument: --{}", #name_str)
))
)),
};
}
}
SimpleParamKind::Optional(inner) => {
quote! {
let #param_name: #ty = match __clap_matches.get_one::<String>(#name_str)
.map(|v| v.parse::<#inner>())
.transpose()
{
Ok(v) => v,
Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
Err(::rnme::error::TaskError::from_display(
format!("invalid value for --{}: {}", #name_str, e)
))
)),
};
}
}
SimpleParamKind::Repeatable(inner) => {
quote! {
let #param_name: #ty = match __clap_matches.get_many::<String>(#name_str)
.map(|vals| vals.map(|v| v.parse::<#inner>()).collect::<Result<Vec<_>, _>>())
.transpose()
{
Ok(v) => v.unwrap_or_default(),
Err(e) => return ::std::boxed::Box::pin(::std::future::ready(
Err(::rnme::error::TaskError::from_display(
format!("invalid value for --{}: {}", #name_str, e)
))
)),
};
}
}
};
parse_stmts.push(extract);
call_args.push(quote! { #param_name });
}
let parse_block = quote! { #(#parse_stmts)* };
(parse_block, call_args, cmd_build)
}
#[proc_macro]
pub fn import_task(input: TokenStream) -> TokenStream {
let path: syn::Path = match syn::parse(input) {
Ok(p) => p,
Err(e) => return e.to_compile_error().into(),
};
if path.segments.is_empty() {
return syn::Error::new_spanned(&path, "expected a path like `lib_crate::task_name`")
.to_compile_error()
.into();
}
let mut lib_path = path.clone();
let task_seg = lib_path
.segments
.pop()
.expect("at least one segment, checked above")
.into_value();
if !task_seg.arguments.is_empty() {
return syn::Error::new_spanned(
&task_seg.arguments,
"task name must not carry generic arguments",
)
.to_compile_error()
.into();
}
if lib_path.segments.is_empty() {
return syn::Error::new_spanned(
&path,
"expected `lib_crate::task_name` — a library path followed by the task name",
)
.to_compile_error()
.into();
}
lib_path.segments.pop_punct();
let task_ident = &task_seg.ident;
let stamp_ident = syn::Ident::new(
&format!("__rnme_stamp_{}", task_ident),
task_ident.span(),
);
let expanded = quote! {
#lib_path :: #stamp_ident !();
};
expanded.into()
}
#[proc_macro_attribute]
pub fn init(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let has_ctx_arg = !input_fn.sig.inputs.is_empty();
let wrapper_name = syn::Ident::new(&format!("__runme_initfn_{}", fn_name), fn_name.span());
let wrapper = if has_ctx_arg {
quote! {
fn #wrapper_name(ctx: &mut ::rnme::init::InitContext) {
#fn_name(ctx)
}
}
} else {
quote! {
fn #wrapper_name(_ctx: &mut ::rnme::init::InitContext) {
#fn_name()
}
}
};
let expanded = quote! {
#input_fn
#wrapper
::rnme::inventory::submit! {
::rnme::init::InitDef {
group: __RNME_GROUP,
dir: __RNME_DIR,
func: #wrapper_name,
}
}
};
expanded.into()
}
#[proc_macro]
pub fn cmd(input: TokenStream) -> TokenStream {
match cmd_macro::expand_cmd(input.into()) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}