#![feature(drain_filter)]
#![feature(box_patterns)]
use std::{cmp::max, collections::HashSet};
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Attribute, Ident, Item, ItemEnum, ItemMod, Variant};
#[proc_macro_attribute]
pub fn parser(_attr: TokenStream, input: TokenStream) -> TokenStream {
let mut module = parse_macro_input!(input as ItemMod);
let (_, content) = module.content.as_mut().expect("module must have a root");
let mut root = content
.drain_filter(|i| {
if let Item::Enum(i) = i {
i.attrs
.drain_filter(|a| a.path.segments.last().unwrap().ident.to_string().eq("root"))
.count()
> 0
} else {
false
}
})
.filter_map(|i| if let Item::Enum(e) = i { Some(e) } else { None })
.next()
.expect("module must have a root");
let name = root.ident.clone();
let mut max_dashes = 0;
let mut subcommands = vec![];
let mut has_subsegments = HashSet::new();
let x = root
.variants
.iter_mut()
.map(|v| {
let ident = v.ident.clone();
let kebab = v
.attrs
.drain_filter(|v| v.path.get_ident().unwrap() == "rename")
.next()
.and_then(|Attribute { tokens, .. }| {
let paren = syn::parse2::<syn::ExprParen>(tokens);
if let Ok(syn::ExprParen {
expr:
box syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}),
..
}) = paren
{
Some(lit.value())
} else {
None
}
})
.unwrap_or_else(|| v.ident.to_string().to_case(Case::Kebab));
if let Some(f) = v.fields.iter().next() {
let p = match &f.ty {
syn::Type::Path(p) => p,
_ => panic!("subcommand must be a struct"),
};
let optional = extract_type_from_option(&p.path);
let target = optional
.and_then(|o| match &o {
syn::Type::Path(p) => Some(&p.path),
_ => None,
})
.unwrap_or(&p.path)
.segments
.last()
.map(|s| &s.ident);
let subcommand = target
.and_then(|ident| get_enum_by_ident(content, ident))
.expect("enum variant must have a type");
let format = if v
.attrs
.drain_filter(|a| a.path.get_ident().unwrap() == "transparent")
.count()
> 0
{
fmt_transparent
} else {
subcommands.push(ident.clone());
if optional.is_none() {
has_subsegments.insert(kebab.clone());
}
fmt_regular
};
let subcommands = subcommand.variants.iter().map(|v| {
let kebab = format(v, &kebab);
let sub_name = subcommand.ident.clone();
let mut split = kebab.rsplit_once('-');
while let Some((l, _)) = split {
has_subsegments.insert(l.to_string());
split = l.rsplit_once('-');
}
let sub_ident = v.ident.clone();
if optional.is_some() {
quote! {
#kebab => #name::#ident(Some(#sub_name::#sub_ident)),
}
} else {
quote! {
#kebab => #name::#ident(#sub_name::#sub_ident),
}
}
});
if optional.is_some() {
let subcommands = subcommands.chain(std::iter::once(quote! {
#kebab => #name::#ident(None),
}));
quote! {
#(#subcommands)*
}
} else {
quote! {
#(#subcommands)*
}
}
} else {
max_dashes = max(max_dashes, kebab.chars().filter(|&c| c == '-').count());
quote! {#kebab => #name::#ident,}
}
})
.collect::<Vec<_>>();
let has_subsegments = has_subsegments.iter();
let subcommands_true = if subcommands.is_empty() {
quote! {}
} else {
quote! {#(#name::#subcommands(_))|* => true, }
};
TokenStream::from(quote! {
mod plugin {
#root
#(#content)*
impl std::str::FromStr for #name {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
PLUGINS.get(s).copied().ok_or(())
}
}
impl #name {
pub const fn max_dashes() -> usize {
#max_dashes
}
pub fn has_subcommand(&self) -> bool {
match self {
#subcommands_true
_ => false
}
}
pub fn has_subsegments(name: &str) -> bool {
[#(#has_subsegments),*].contains(&name)
}
}
static PLUGINS: phf::Map<&'static str, #name> = phf::phf_map! {
#(#x)*
};
}
})
}
fn get_enum_by_ident<'a>(content: &'a [Item], ident: &Ident) -> Option<&'a ItemEnum> {
content
.iter()
.filter_map(|i| if let Item::Enum(e) = i { Some(e) } else { None })
.find(|e| e.ident.eq(ident))
}
fn extract_type_from_option(path: &syn::Path) -> Option<&syn::Type> {
use syn::{GenericArgument, Path, PathArguments, PathSegment};
fn extract_option_segment(path: &Path) -> Option<&PathSegment> {
let idents_of_path = path.segments.iter().fold(String::new(), |mut acc, v| {
acc.push_str(&v.ident.to_string());
acc.push('|');
acc
});
["Option|", "std|option|Option|", "core|option|Option|"]
.into_iter()
.find(|s| idents_of_path == *s)
.and_then(|_| path.segments.last())
}
extract_option_segment(path)
.and_then(|path_seg| {
let type_params = &path_seg.arguments;
match *type_params {
PathArguments::AngleBracketed(ref params) => params.args.first(),
_ => None,
}
})
.and_then(|generic_arg| match *generic_arg {
GenericArgument::Type(ref ty) => Some(ty),
_ => None,
})
}
fn fmt_transparent(v: &Variant, _kebab: &str) -> String {
v.ident.to_string().to_case(Case::Kebab)
}
fn fmt_regular(v: &Variant, kebab: &str) -> String {
format!("{kebab}-{}", v.ident.to_string().to_case(Case::Kebab))
}