#![doc(html_root_url = "https://docs.rs/gflags-derive/0.1.0")]
extern crate proc_macro;
use crate::FlagCase::{KebabCase, SnakeCase};
use proc_macro2::{Ident, Literal, Span, TokenStream, TokenTree};
use proc_macro_error::{abort, abort_call_site, proc_macro_error};
use quote::{format_ident, quote};
use std::collections::HashSet;
use syn::{
punctuated::Punctuated, Attribute, Data, DataStruct, Field, Fields, FieldsNamed,
GenericArgument, Lit, Meta, NestedMeta, Path, PathArguments, PathSegment, Token, Type,
};
#[derive(Debug, PartialEq)]
enum FlagCase {
SnakeCase,
KebabCase,
}
#[derive(Debug)]
struct Config {
prefix: String,
flag_case: FlagCase,
}
impl Default for Config {
fn default() -> Self {
Config {
prefix: "".to_string(),
flag_case: KebabCase,
}
}
}
fn impl_gflags_macro(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let fields: Vec<&Field> = match &ast.data {
Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named: fields, .. }),
..
}) => fields.into_iter().collect(),
_ => abort_call_site!("expected a struct with named fields"),
};
let config = config_from_attributes(&ast.attrs);
let mut flags: Vec<TokenStream> = vec![];
for field in fields {
let flag = flag_from_field(&config, field);
flags.push(flag);
}
let gen = quote! {
#(#flags)*
};
gen.into()
}
#[derive(Debug, Default)]
struct GFlagsAttribute {
skip: bool,
prefix: Option<String>,
flag_case: Option<FlagCase>,
ty: Option<TokenStream>,
visibility: Option<TokenStream>,
placeholder: Option<TokenStream>,
default: Option<TokenStream>,
}
impl From<Meta> for GFlagsAttribute {
fn from(meta: Meta) -> Self {
let meta = match meta {
Meta::List(meta) => meta,
_ => abort!(meta, "`#[gflags(...)]` expects a parameter list"),
};
if meta.nested.is_empty() {
abort!(meta, "`#[gflags(...)]` expects a non-empty parameter list");
}
let mut config = GFlagsAttribute::default();
let keywords: HashSet<&'static str> = [
"default",
"placeholder",
"prefix",
"skip",
"type",
"visibility",
]
.iter()
.cloned()
.collect();
for kv in meta.nested {
let kv = match kv {
NestedMeta::Meta(Meta::Path(path)) => {
let keyword = path.get_ident().expect("No ident found");
if !keywords.contains(&keyword.to_string().as_ref()) {
abort!(path, "Invalid keyword `{}`", keyword);
}
if path.is_ident("skip") {
config.skip = true;
break;
}
abort!(path, "Keyword `{}` requires a value", keyword);
}
NestedMeta::Meta(Meta::NameValue(kv)) => kv,
_ => abort!(kv, "`#[gflags(...)]` expects key=value pairs"),
};
if kv.path.is_ident("default") {
let lit = kv.lit;
config.default = Some(quote! { = #lit });
continue;
}
if kv.path.is_ident("placeholder") {
config.placeholder = match kv.lit {
Lit::Str(lit) => {
if lit.value().is_empty() {
abort!(
lit,
"`#[gflags(placeholder=...)]` expects a non-empty quoted string"
)
}
let tokens = lit.parse::<TokenStream>().unwrap();
Some(quote! { < #tokens > })
}
_ => abort!(
kv.lit,
"`#[gflags(placeholder=...)]` expects a quoted string"
),
};
continue;
}
if kv.path.is_ident("prefix") {
let mut prefix = match kv.lit {
Lit::Str(lit) => {
if lit.value().is_empty() {
abort!(
lit,
"`#[gflags(prefix=...)]` expects a non-empty quoted string"
);
}
lit.value()
}
_ => abort!(kv.lit, "`#[gflags(prefix=...)]` expects a quoted string"),
};
if prefix.ends_with('_') {
config.flag_case = Some(SnakeCase);
prefix.pop();
}
if prefix.ends_with('-') {
config.flag_case = Some(KebabCase);
prefix.pop();
}
config.prefix = Some(prefix);
continue;
}
if kv.path.is_ident("skip") {
abort!(kv.lit, "`#[gflags(skip)]` does not take a value");
}
if kv.path.is_ident("type") {
config.ty = match kv.lit {
Lit::Str(lit) => {
if lit.value().is_empty() {
abort!(
lit,
"`#[gflags(type=...)]` expects a non-empty quoted string"
);
}
Some(lit.parse().unwrap())
}
_ => abort!(kv.lit, "`#[gflags(type=...)]` expects a quoted string"),
};
continue;
}
if kv.path.is_ident("visibility") {
config.visibility = match kv.lit {
Lit::Str(lit) => {
if lit.value().is_empty() {
abort!(
lit,
"`#[gflags(visibility=...)]` expects a non-empty quoted string"
)
}
Some(lit.parse().unwrap())
}
_ => abort!(
kv.lit,
"`#[gflags(visibility=...)]` expects a quoted string"
),
};
continue;
}
abort!(
kv.path,
"Invalid keyword `{}`",
kv.path.get_ident().unwrap()
);
}
config
}
}
impl From<&[Attribute]> for GFlagsAttribute {
fn from(attrs: &[Attribute]) -> Self {
let mut config: Self = Default::default();
for attr in attrs {
match attr.parse_meta() {
Ok(meta) => {
if !meta.path().is_ident("gflags") {
continue;
}
let parsed_config = GFlagsAttribute::from(meta);
if parsed_config.skip {
config.skip = true
};
if parsed_config.default.is_some() {
config.default = parsed_config.default;
}
if parsed_config.placeholder.is_some() {
config.placeholder = parsed_config.placeholder;
}
if parsed_config.prefix.is_some() {
config.prefix = parsed_config.prefix;
}
if parsed_config.flag_case.is_some() {
config.flag_case = parsed_config.flag_case;
}
if parsed_config.ty.is_some() {
config.ty = parsed_config.ty;
}
if parsed_config.visibility.is_some() {
config.visibility = parsed_config.visibility;
}
}
Err(e) => abort!(attr, e),
}
}
config
}
}
fn config_from_attributes(attrs: &[Attribute]) -> Config {
let mut config: Config = Default::default();
let gfa = GFlagsAttribute::from(attrs);
if gfa.prefix.is_some() {
config.prefix = gfa.prefix.unwrap();
}
if gfa.flag_case.is_some() {
config.flag_case = gfa.flag_case.unwrap();
}
config
}
fn flag_from_field(config: &Config, field: &Field) -> TokenStream {
let gfa = GFlagsAttribute::from(field.attrs.as_ref());
if gfa.skip {
return TokenStream::new();
}
let flag_name = if config.flag_case == SnakeCase {
let ident = if !config.prefix.is_empty() {
format_ident!(
"{}_{}",
config.prefix,
field
.ident
.as_ref()
.expect("Unwrapping field.ident (prefix) failed")
)
} else {
field
.ident
.as_ref()
.expect("Unwrapping field.ident (no-prefix) failed")
.clone()
};
quote! {--#ident}
} else {
let span = Span::call_site();
let mut segments: Punctuated<Ident, Token![-]> = Punctuated::new();
if !config.prefix.is_empty() {
segments.push(Ident::new(&config.prefix, span));
}
let field = field.ident.as_ref().unwrap().to_string();
for part in field.split('_') {
segments.push(Ident::new(part, span));
}
quote! {--#segments}
};
let default = match gfa.default {
Some(default) => default,
_ => TokenStream::new(),
};
let placeholder = match gfa.placeholder {
Some(placeholder) => placeholder,
_ => TokenStream::new(),
};
let visibility = match gfa.visibility {
Some(visibility) => visibility,
_ => TokenStream::new(),
};
let ty = match gfa.ty {
Some(ty) => ty,
_ => match &field.ty {
Type::Path(ty) => {
let mut last = ty.path.segments.last().unwrap();
let mut ident = &last.ident;
let mut final_type = ty.clone();
if *ident == "Option" {
let option_type = syn::Type::from(final_type);
let new_ty = extract_type_from_option(&option_type);
match new_ty {
Some(Type::Path(new_ty)) => {
final_type = new_ty.clone();
last = final_type.path.segments.last().unwrap();
ident = &last.ident;
}
_ => abort!(&field.ty, "Unexpected type"),
}
}
if *ident == "String" {
quote! { &str }
} else {
quote! { #final_type }
}
}
_ => abort!(&field.ty, "Unexpected type"),
},
};
let mut docs: Vec<Literal> = vec![];
for attr in &field.attrs {
if !attr.path.is_ident("doc") {
continue;
}
let tokens = attr.tokens.clone();
for token in tokens {
if let TokenTree::Literal(l) = token {
docs.push(l);
}
}
}
let gen = quote! {
gflags::define! {
#( #[doc = #docs])*
#visibility #flag_name #placeholder: #ty #default
}
};
gen
}
fn extract_type_from_option(ty: &syn::Type) -> Option<&syn::Type> {
fn extract_type_path(ty: &syn::Type) -> Option<&Path> {
match *ty {
syn::Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
_ => None,
}
}
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
});
vec!["Option|", "std|option|Option|", "core|option|Option|"]
.into_iter()
.find(|s| idents_of_path == *s)
.and_then(|_| path.segments.last())
}
extract_type_path(ty)
.and_then(|path| extract_option_segment(path))
.and_then(|pair_path_segment| {
let type_params = &pair_path_segment.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,
})
}
#[proc_macro_derive(GFlags, attributes(gflags))]
#[proc_macro_error]
pub fn gflags_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = syn::parse(input).unwrap();
impl_gflags_macro(&ast)
}