use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{ItemImpl, parse::Parser, punctuated::Punctuated};
#[derive(Debug, Clone, Default, PartialEq)]
pub enum VersionSpec {
#[default]
Auto,
Disabled,
Explicit(String),
}
impl VersionSpec {
pub fn into_explicit(self) -> Option<String> {
match self {
VersionSpec::Explicit(v) => Some(v),
VersionSpec::Auto | VersionSpec::Disabled => None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AppMeta {
pub name: Option<String>,
pub description: Option<String>,
pub version: VersionSpec,
pub homepage: Option<String>,
}
fn parse_app_args(args: proc_macro2::TokenStream) -> syn::Result<AppMeta> {
let mut meta = AppMeta::default();
if args.is_empty() {
return Ok(meta);
}
let parser = Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated;
let items = parser.parse2(args)?;
const VALID: &[&str] = &["name", "description", "version", "homepage"];
for item in items {
match &item {
syn::Meta::NameValue(nv) if nv.path.is_ident("name") => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
meta.name = Some(s.value());
} else {
return Err(syn::Error::new_spanned(&nv.value, "`name` must be a string literal"));
}
}
syn::Meta::NameValue(nv) if nv.path.is_ident("description") => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
meta.description = Some(s.value());
} else {
return Err(syn::Error::new_spanned(&nv.value, "`description` must be a string literal"));
}
}
syn::Meta::NameValue(nv) if nv.path.is_ident("version") => {
match &nv.value {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => {
meta.version = VersionSpec::Explicit(s.value());
}
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Bool(b),
..
}) if !b.value => {
meta.version = VersionSpec::Disabled;
}
_ => {
return Err(syn::Error::new_spanned(
&nv.value,
"`version` must be a string literal or `false`",
));
}
}
}
syn::Meta::NameValue(nv) if nv.path.is_ident("homepage") => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
meta.homepage = Some(s.value());
} else {
return Err(syn::Error::new_spanned(&nv.value, "`homepage` must be a string literal"));
}
}
other => {
let ident = other
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
let suggestion = crate::did_you_mean(&ident, VALID)
.map(|s| format!(" — did you mean `{s}`?"))
.unwrap_or_default();
return Err(syn::Error::new_spanned(
other,
format!(
"unknown `#[app]` argument `{ident}`{suggestion}\n\
\n\
Valid arguments: name, description, version, homepage\n\
\n\
Example: #[app(name = \"myapp\", description = \"Does the thing\", version = \"1.0.0\")]"
),
));
}
}
}
Ok(meta)
}
pub fn expand_app(args: TokenStream2, item: ItemImpl) -> syn::Result<TokenStream2> {
let meta = parse_app_args(args.clone())?;
let meta_attr = build_meta_attr(&meta);
Ok(quote! {
#meta_attr
#item
})
}
pub fn build_meta_attr(meta: &AppMeta) -> TokenStream2 {
let mut parts = Vec::<TokenStream2>::new();
if let Some(name) = &meta.name {
parts.push(quote! { name = #name });
}
if let Some(desc) = &meta.description {
parts.push(quote! { description = #desc });
}
match &meta.version {
VersionSpec::Explicit(v) => parts.push(quote! { version = #v }),
VersionSpec::Disabled => parts.push(quote! { version = false }),
VersionSpec::Auto => {}
}
if let Some(hp) = &meta.homepage {
parts.push(quote! { homepage = #hp });
}
if parts.is_empty() {
quote! { #[__app_meta()] }
} else {
quote! { #[__app_meta(#(#parts),*)] }
}
}
pub fn extract_app_meta(attrs: &mut Vec<syn::Attribute>) -> AppMeta {
let mut result = AppMeta::default();
attrs.retain(|attr| {
if attr.path().is_ident("__app_meta") {
let tokens = match &attr.meta {
syn::Meta::List(list) => list.tokens.clone(),
_ => return true, };
if let Ok(parsed) = parse_app_args(tokens) {
result = parsed;
}
false } else {
true
}
});
result
}
pub fn expand_app_meta_passthrough(_args: TokenStream2, item: ItemImpl) -> TokenStream2 {
quote! { #item }
}