pub(crate) mod application;
pub(crate) mod command;
pub(crate) use application::ApplicationGenerator;
pub(crate) use command::CommandGenerator;
use darling::FromMeta;
use darling::ast::NestedMeta;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Error, Expr, Ident, ItemFn, Lit, Meta, Result, Type};
use crate::inventory::inventory_name;
pub(crate) trait Generator {
fn ident(&self) -> Ident;
fn attrs(&self) -> &Attributes;
fn input(&self) -> &ItemFn;
fn args_type(&self) -> Box<Type>;
fn resolve_function_body(&self) -> TokenStream;
fn is_root(&self) -> bool {
self.attrs().root()
}
fn initialization_function_name(&self) -> Ident {
format_ident!("{}_init", self.ident())
}
fn resolve_function_name(&self) -> Ident {
format_ident!("{}_resolve", self.ident())
}
fn command_new(&self) -> TokenStream {
build_command(
&self.ident(),
&self.args_type(),
extract_function_documentation(self.input()).as_ref(),
self.attrs(),
)
}
fn initialization_function(&self) -> TokenStream {
let function_name = self.initialization_function_name();
let command_new = self.command_new();
let inventory_name = inventory_name();
quote! {
pub fn #function_name() -> clawless::clap::Command {
let mut command = #command_new;
for subcommand in clawless::inventory::iter::<#inventory_name> {
command = command.subcommand((subcommand.init)());
}
command
}
}
}
fn resolve_function(&self) -> TokenStream {
let resolve_function_name = self.resolve_function_name();
let resolve_function_body = self.resolve_function_body();
let inventory_name = inventory_name();
quote! {
pub fn #resolve_function_name(matches: clawless::clap::ArgMatches) -> clawless::resolved_leaf::ResolvedLeaf {
for subcommand in clawless::inventory::iter::<#inventory_name> {
if let Some(sub_matches) = matches.subcommand_matches(subcommand.name) {
return (subcommand.resolve)(sub_matches.clone());
}
}
#resolve_function_body
}
}
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, FromMeta)]
pub(crate) struct Attributes {
#[darling(default)]
require_subcommand: bool,
#[darling(default)]
root: bool,
#[darling(default, multiple)]
alias: Vec<String>,
}
impl Attributes {
pub(crate) fn require_subcommand(&self) -> bool {
self.require_subcommand
}
pub(crate) fn root(&self) -> bool {
self.root
}
pub(crate) fn alias(&self) -> &[String] {
&self.alias
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) struct Documentation {
short: String,
long: String,
}
impl Documentation {
pub(crate) fn short(&self) -> &str {
&self.short
}
pub(crate) fn long(&self) -> &str {
&self.long
}
}
pub(crate) fn parse_attributes(attrs: TokenStream, macro_name: &str) -> Result<Attributes> {
let argument_list = NestedMeta::parse_meta_list(attrs.clone()).map_err(|e| {
Error::new_spanned(
attrs.clone(),
format!(
"invalid attribute syntax: {e}\n\n\
= help: use one of the supported attributes\n\n \
#[{macro_name}]\n \
#[{macro_name}(alias = \"g\")]\n \
#[{macro_name}(require_subcommand)]\n \
#[{macro_name}(alias = \"g\", require_subcommand)]"
),
)
})?;
Attributes::from_list(&argument_list).map_err(|e| {
Error::new_spanned(
attrs,
format!(
"{e}\n\n\
= help: supported attributes are `alias` and `require_subcommand`\n\n \
#[{macro_name}(alias = \"g\", require_subcommand)]"
),
)
})
}
pub(crate) fn extract_function_documentation(input_fn: &ItemFn) -> Option<Documentation> {
let mut docs = Vec::new();
for attr in input_fn.attrs.iter() {
if let Meta::NameValue(meta) = &attr.meta {
if !attr.meta.path().is_ident("doc") {
continue;
}
if let Expr::Lit(expr) = &meta.value
&& let Lit::Str(lit) = &expr.lit
{
docs.push(lit.value().trim().to_string());
}
}
}
if docs.is_empty() {
None
} else {
Some(Documentation {
short: docs[0].clone(),
long: docs.join("\n"),
})
}
}
fn build_command(
ident: &Ident,
args_type: &Type,
docs: Option<&Documentation>,
attrs: &Attributes,
) -> TokenStream {
let command_name = ident.to_string();
let mut command = quote! {
#args_type::augment_args(clawless::clap::Command::new(#command_name))
};
if attrs.root() {
command = quote! {
#command.about(clawless::clap::crate_description!())
};
} else if let Some(docs) = docs {
let short = docs.short();
let long = docs.long();
command = quote! {
#command.about(#short).long_about(#long)
};
}
if attrs.require_subcommand() {
command = quote! {
#command.arg_required_else_help(true)
};
}
let alias = attrs.alias();
if !alias.is_empty() {
command = quote! {
#command.visible_aliases([#(#alias),*])
};
}
command
}
#[cfg(test)]
mod tests {
#![allow(clippy::missing_panics_doc)]
use indoc::indoc;
use super::*;
#[test]
fn extract_function_documentation_with_single_line_comment() {
let input = quote! {
fn foo() {}
};
let input_fn = syn::parse2(input).unwrap();
let documentation = extract_function_documentation(&input_fn);
let documentation = documentation.expect("should have documentation");
assert_eq!("This is a test function", documentation.short());
assert_eq!("This is a test function", documentation.long());
}
#[test]
fn extract_function_documentation_with_multiple_line_comment() {
let comment = indoc! { r#"
This is a test comment
with multiple lines"#
};
let input = quote! {
fn foo() {}
};
let input_fn = syn::parse2(input).unwrap();
let documentation = extract_function_documentation(&input_fn);
let documentation = documentation.expect("should have documentation");
assert_eq!("This is a test comment", documentation.short());
assert_eq!(comment, documentation.long());
}
}