| // Copyright 2018 Guillaume Pinot (@TeXitoi) <[email protected]>, |
| // Kevin Knapp (@kbknapp) <[email protected]>, and |
| // Ana Hobden (@hoverbear) <[email protected]> |
| // |
| // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
| // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
| // option. This file may not be copied, modified, or distributed |
| // except according to those terms. |
| // |
| // This work was derived from Structopt (https://github.com/TeXitoi/structopt) |
| // commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the |
| // MIT/Apache 2.0 license. |
| |
| use crate::{ |
| parse::*, |
| utils::{process_doc_comment, Sp, Ty}, |
| }; |
| |
| use std::env; |
| |
| use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; |
| use proc_macro2::{self, Span, TokenStream}; |
| use proc_macro_error::abort; |
| use quote::{quote, quote_spanned, ToTokens}; |
| use syn::{ |
| self, ext::IdentExt, spanned::Spanned, Attribute, Expr, Field, Ident, LitStr, MetaNameValue, |
| Type, Variant, |
| }; |
| |
| /// Default casing style for generated arguments. |
| pub const DEFAULT_CASING: CasingStyle = CasingStyle::Kebab; |
| |
| /// Default casing style for environment variables |
| pub const DEFAULT_ENV_CASING: CasingStyle = CasingStyle::ScreamingSnake; |
| |
| #[derive(Clone)] |
| pub struct Attrs { |
| name: Name, |
| casing: Sp<CasingStyle>, |
| env_casing: Sp<CasingStyle>, |
| ty: Option<Type>, |
| doc_comment: Vec<Method>, |
| methods: Vec<Method>, |
| parser: Sp<Parser>, |
| verbatim_doc_comment: Option<Ident>, |
| next_display_order: Option<Method>, |
| next_help_heading: Option<Method>, |
| help_heading: Option<Method>, |
| is_enum: bool, |
| has_custom_parser: bool, |
| kind: Sp<Kind>, |
| } |
| |
| impl Attrs { |
| pub fn from_struct( |
| span: Span, |
| attrs: &[Attribute], |
| name: Name, |
| argument_casing: Sp<CasingStyle>, |
| env_casing: Sp<CasingStyle>, |
| ) -> Self { |
| let mut res = Self::new(span, name, None, argument_casing, env_casing); |
| res.push_attrs(attrs); |
| res.push_doc_comment(attrs, "about"); |
| |
| if res.has_custom_parser { |
| abort!( |
| res.parser.span(), |
| "`parse` attribute is only allowed on fields" |
| ); |
| } |
| match &*res.kind { |
| Kind::Subcommand(_) => abort!(res.kind.span(), "subcommand is only allowed on fields"), |
| Kind::Skip(_) => abort!(res.kind.span(), "skip is only allowed on fields"), |
| Kind::Arg(_) => res, |
| Kind::FromGlobal(_) => abort!(res.kind.span(), "from_global is only allowed on fields"), |
| Kind::Flatten => abort!(res.kind.span(), "flatten is only allowed on fields"), |
| Kind::ExternalSubcommand => abort!( |
| res.kind.span(), |
| "external_subcommand is only allowed on fields" |
| ), |
| } |
| } |
| |
| pub fn from_variant( |
| variant: &Variant, |
| struct_casing: Sp<CasingStyle>, |
| env_casing: Sp<CasingStyle>, |
| ) -> Self { |
| let name = variant.ident.clone(); |
| let mut res = Self::new( |
| variant.span(), |
| Name::Derived(name), |
| None, |
| struct_casing, |
| env_casing, |
| ); |
| res.push_attrs(&variant.attrs); |
| res.push_doc_comment(&variant.attrs, "about"); |
| |
| match &*res.kind { |
| Kind::Flatten => { |
| if res.has_custom_parser { |
| abort!( |
| res.parser.span(), |
| "parse attribute is not allowed for flattened entry" |
| ); |
| } |
| if res.has_explicit_methods() { |
| abort!( |
| res.kind.span(), |
| "methods are not allowed for flattened entry" |
| ); |
| } |
| |
| // ignore doc comments |
| res.doc_comment = vec![]; |
| } |
| |
| Kind::ExternalSubcommand => (), |
| |
| Kind::Subcommand(_) => { |
| if res.has_custom_parser { |
| abort!( |
| res.parser.span(), |
| "parse attribute is not allowed for subcommand" |
| ); |
| } |
| |
| use syn::Fields::*; |
| use syn::FieldsUnnamed; |
| let field_ty = match variant.fields { |
| Named(_) => { |
| abort!(variant.span(), "structs are not allowed for subcommand"); |
| } |
| Unit => abort!(variant.span(), "unit-type is not allowed for subcommand"), |
| Unnamed(FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => { |
| &unnamed[0].ty |
| } |
| Unnamed(..) => { |
| abort!( |
| variant, |
| "non single-typed tuple is not allowed for subcommand" |
| ) |
| } |
| }; |
| let ty = Ty::from_syn_ty(field_ty); |
| match *ty { |
| Ty::OptionOption => { |
| abort!( |
| field_ty, |
| "Option<Option<T>> type is not allowed for subcommand" |
| ); |
| } |
| Ty::OptionVec => { |
| abort!( |
| field_ty, |
| "Option<Vec<T>> type is not allowed for subcommand" |
| ); |
| } |
| _ => (), |
| } |
| |
| res.kind = Sp::new(Kind::Subcommand(ty), res.kind.span()); |
| } |
| Kind::Skip(_) => (), |
| Kind::FromGlobal(_) => { |
| abort!(res.kind.span(), "from_global is not supported on variants"); |
| } |
| Kind::Arg(_) => (), |
| } |
| |
| res |
| } |
| |
| pub fn from_arg_enum_variant( |
| variant: &Variant, |
| argument_casing: Sp<CasingStyle>, |
| env_casing: Sp<CasingStyle>, |
| ) -> Self { |
| let mut res = Self::new( |
| variant.span(), |
| Name::Derived(variant.ident.clone()), |
| None, |
| argument_casing, |
| env_casing, |
| ); |
| res.push_attrs(&variant.attrs); |
| res.push_doc_comment(&variant.attrs, "help"); |
| |
| if res.has_custom_parser { |
| abort!( |
| res.parser.span(), |
| "`parse` attribute is only allowed on fields" |
| ); |
| } |
| match &*res.kind { |
| Kind::Subcommand(_) => abort!(res.kind.span(), "subcommand is only allowed on fields"), |
| Kind::Skip(_) => res, |
| Kind::Arg(_) => res, |
| Kind::FromGlobal(_) => abort!(res.kind.span(), "from_global is only allowed on fields"), |
| Kind::Flatten => abort!(res.kind.span(), "flatten is only allowed on fields"), |
| Kind::ExternalSubcommand => abort!( |
| res.kind.span(), |
| "external_subcommand is only allowed on fields" |
| ), |
| } |
| } |
| |
| pub fn from_field( |
| field: &Field, |
| struct_casing: Sp<CasingStyle>, |
| env_casing: Sp<CasingStyle>, |
| ) -> Self { |
| let name = field.ident.clone().unwrap(); |
| let mut res = Self::new( |
| field.span(), |
| Name::Derived(name), |
| Some(field.ty.clone()), |
| struct_casing, |
| env_casing, |
| ); |
| res.push_attrs(&field.attrs); |
| res.push_doc_comment(&field.attrs, "help"); |
| |
| match &*res.kind { |
| Kind::Flatten => { |
| if res.has_custom_parser { |
| abort!( |
| res.parser.span(), |
| "parse attribute is not allowed for flattened entry" |
| ); |
| } |
| if res.has_explicit_methods() { |
| abort!( |
| res.kind.span(), |
| "methods are not allowed for flattened entry" |
| ); |
| } |
| |
| // ignore doc comments |
| res.doc_comment = vec![]; |
| } |
| |
| Kind::ExternalSubcommand => { |
| abort! { res.kind.span(), |
| "`external_subcommand` can be used only on enum variants" |
| } |
| } |
| |
| Kind::Subcommand(_) => { |
| if res.has_custom_parser { |
| abort!( |
| res.parser.span(), |
| "parse attribute is not allowed for subcommand" |
| ); |
| } |
| if res.has_explicit_methods() { |
| abort!( |
| res.kind.span(), |
| "methods in attributes are not allowed for subcommand" |
| ); |
| } |
| |
| let ty = Ty::from_syn_ty(&field.ty); |
| match *ty { |
| Ty::OptionOption => { |
| abort!( |
| field.ty, |
| "Option<Option<T>> type is not allowed for subcommand" |
| ); |
| } |
| Ty::OptionVec => { |
| abort!( |
| field.ty, |
| "Option<Vec<T>> type is not allowed for subcommand" |
| ); |
| } |
| _ => (), |
| } |
| |
| res.kind = Sp::new(Kind::Subcommand(ty), res.kind.span()); |
| } |
| Kind::Skip(_) => { |
| if res.has_explicit_methods() { |
| abort!( |
| res.kind.span(), |
| "methods are not allowed for skipped fields" |
| ); |
| } |
| } |
| Kind::FromGlobal(orig_ty) => { |
| let ty = Ty::from_syn_ty(&field.ty); |
| res.kind = Sp::new(Kind::FromGlobal(ty), orig_ty.span()); |
| } |
| Kind::Arg(orig_ty) => { |
| let mut ty = Ty::from_syn_ty(&field.ty); |
| if res.has_custom_parser { |
| match *ty { |
| Ty::Option | Ty::Vec | Ty::OptionVec => (), |
| _ => ty = Sp::new(Ty::Other, ty.span()), |
| } |
| } |
| |
| match *ty { |
| Ty::Bool => { |
| if res.is_positional() && !res.has_custom_parser { |
| abort!(field.ty, |
| "`bool` cannot be used as positional parameter with default parser"; |
| help = "if you want to create a flag add `long` or `short`"; |
| help = "If you really want a boolean parameter \ |
| add an explicit parser, for example `parse(try_from_str)`"; |
| note = "see also https://github.com/clap-rs/clap/blob/master/examples/derive_ref/custom-bool.md"; |
| ) |
| } |
| if res.is_enum { |
| abort!(field.ty, "`arg_enum` is meaningless for bool") |
| } |
| if let Some(m) = res.find_default_method() { |
| abort!(m.name, "default_value is meaningless for bool") |
| } |
| if let Some(m) = res.find_method("required") { |
| abort!(m.name, "required is meaningless for bool") |
| } |
| } |
| Ty::Option => { |
| if let Some(m) = res.find_default_method() { |
| abort!(m.name, "default_value is meaningless for Option") |
| } |
| } |
| Ty::OptionOption => { |
| if res.is_positional() { |
| abort!( |
| field.ty, |
| "Option<Option<T>> type is meaningless for positional argument" |
| ) |
| } |
| } |
| Ty::OptionVec => { |
| if res.is_positional() { |
| abort!( |
| field.ty, |
| "Option<Vec<T>> type is meaningless for positional argument" |
| ) |
| } |
| } |
| |
| _ => (), |
| } |
| res.kind = Sp::new(Kind::Arg(ty), orig_ty.span()); |
| } |
| } |
| |
| res |
| } |
| |
| fn new( |
| default_span: Span, |
| name: Name, |
| ty: Option<Type>, |
| casing: Sp<CasingStyle>, |
| env_casing: Sp<CasingStyle>, |
| ) -> Self { |
| Self { |
| name, |
| ty, |
| casing, |
| env_casing, |
| doc_comment: vec![], |
| methods: vec![], |
| parser: Parser::default_spanned(default_span), |
| verbatim_doc_comment: None, |
| next_display_order: None, |
| next_help_heading: None, |
| help_heading: None, |
| is_enum: false, |
| has_custom_parser: false, |
| kind: Sp::new(Kind::Arg(Sp::new(Ty::Other, default_span)), default_span), |
| } |
| } |
| |
| fn push_method(&mut self, name: Ident, arg: impl ToTokens) { |
| if name == "name" { |
| self.name = Name::Assigned(quote!(#arg)); |
| } else { |
| self.methods.push(Method::new(name, quote!(#arg))); |
| } |
| } |
| |
| fn push_attrs(&mut self, attrs: &[Attribute]) { |
| use ClapAttr::*; |
| |
| let parsed = parse_clap_attributes(attrs); |
| for attr in &parsed { |
| let attr = attr.clone(); |
| match attr { |
| Short(ident) => { |
| self.push_method(ident, self.name.clone().translate_char(*self.casing)); |
| } |
| |
| Long(ident) => { |
| self.push_method(ident, self.name.clone().translate(*self.casing)); |
| } |
| |
| Env(ident) => { |
| self.push_method(ident, self.name.clone().translate(*self.env_casing)); |
| } |
| |
| ArgEnum(_) => self.is_enum = true, |
| |
| FromGlobal(ident) => { |
| let ty = Sp::call_site(Ty::Other); |
| let kind = Sp::new(Kind::FromGlobal(ty), ident.span()); |
| self.set_kind(kind); |
| } |
| |
| Subcommand(ident) => { |
| let ty = Sp::call_site(Ty::Other); |
| let kind = Sp::new(Kind::Subcommand(ty), ident.span()); |
| self.set_kind(kind); |
| } |
| |
| ExternalSubcommand(ident) => { |
| let kind = Sp::new(Kind::ExternalSubcommand, ident.span()); |
| self.set_kind(kind); |
| } |
| |
| Flatten(ident) => { |
| let kind = Sp::new(Kind::Flatten, ident.span()); |
| self.set_kind(kind); |
| } |
| |
| Skip(ident, expr) => { |
| let kind = Sp::new(Kind::Skip(expr), ident.span()); |
| self.set_kind(kind); |
| } |
| |
| VerbatimDocComment(ident) => self.verbatim_doc_comment = Some(ident), |
| |
| DefaultValueT(ident, expr) => { |
| let ty = if let Some(ty) = self.ty.as_ref() { |
| ty |
| } else { |
| abort!( |
| ident, |
| "#[clap(default_value_t)] (without an argument) can be used \ |
| only on field level"; |
| |
| note = "see \ |
| https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") |
| }; |
| |
| let val = if let Some(expr) = expr { |
| quote!(#expr) |
| } else { |
| quote!(<#ty as ::std::default::Default>::default()) |
| }; |
| |
| let val = if parsed.iter().any(|a| matches!(a, ArgEnum(_))) { |
| quote_spanned!(ident.span()=> { |
| { |
| let val: #ty = #val; |
| clap::ArgEnum::to_possible_value(&val).unwrap().get_name() |
| } |
| }) |
| } else { |
| quote_spanned!(ident.span()=> { |
| clap::lazy_static::lazy_static! { |
| static ref DEFAULT_VALUE: &'static str = { |
| let val: #ty = #val; |
| let s = ::std::string::ToString::to_string(&val); |
| ::std::boxed::Box::leak(s.into_boxed_str()) |
| }; |
| } |
| *DEFAULT_VALUE |
| }) |
| }; |
| |
| let raw_ident = Ident::new("default_value", ident.span()); |
| self.methods.push(Method::new(raw_ident, val)); |
| } |
| |
| DefaultValueOsT(ident, expr) => { |
| let ty = if let Some(ty) = self.ty.as_ref() { |
| ty |
| } else { |
| abort!( |
| ident, |
| "#[clap(default_value_os_t)] (without an argument) can be used \ |
| only on field level"; |
| |
| note = "see \ |
| https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") |
| }; |
| |
| let val = if let Some(expr) = expr { |
| quote!(#expr) |
| } else { |
| quote!(<#ty as ::std::default::Default>::default()) |
| }; |
| |
| let val = if parsed.iter().any(|a| matches!(a, ArgEnum(_))) { |
| quote_spanned!(ident.span()=> { |
| { |
| let val: #ty = #val; |
| clap::ArgEnum::to_possible_value(&val).unwrap().get_name() |
| } |
| }) |
| } else { |
| quote_spanned!(ident.span()=> { |
| clap::lazy_static::lazy_static! { |
| static ref DEFAULT_VALUE: &'static ::std::ffi::OsStr = { |
| let val: #ty = #val; |
| let s: ::std::ffi::OsString = val.into(); |
| ::std::boxed::Box::leak(s.into_boxed_os_str()) |
| }; |
| } |
| *DEFAULT_VALUE |
| }) |
| }; |
| |
| let raw_ident = Ident::new("default_value_os", ident.span()); |
| self.methods.push(Method::new(raw_ident, val)); |
| } |
| |
| NextDisplayOrder(ident, expr) => { |
| self.next_display_order = Some(Method::new(ident, quote!(#expr))); |
| } |
| |
| HelpHeading(ident, expr) => { |
| self.help_heading = Some(Method::new(ident, quote!(#expr))); |
| } |
| NextHelpHeading(ident, expr) => { |
| self.next_help_heading = Some(Method::new(ident, quote!(#expr))); |
| } |
| |
| About(ident) => { |
| if let Some(method) = Method::from_env(ident, "CARGO_PKG_DESCRIPTION") { |
| self.methods.push(method); |
| } |
| } |
| |
| Author(ident) => { |
| if let Some(method) = Method::from_env(ident, "CARGO_PKG_AUTHORS") { |
| self.methods.push(method); |
| } |
| } |
| |
| Version(ident) => { |
| if let Some(method) = Method::from_env(ident, "CARGO_PKG_VERSION") { |
| self.methods.push(method); |
| } |
| } |
| |
| NameLitStr(name, lit) => { |
| self.push_method(name, lit); |
| } |
| |
| NameExpr(name, expr) => { |
| self.push_method(name, expr); |
| } |
| |
| MethodCall(name, args) => self.push_method(name, quote!(#(#args),*)), |
| |
| RenameAll(_, casing_lit) => { |
| self.casing = CasingStyle::from_lit(casing_lit); |
| } |
| |
| RenameAllEnv(_, casing_lit) => { |
| self.env_casing = CasingStyle::from_lit(casing_lit); |
| } |
| |
| Parse(ident, spec) => { |
| self.has_custom_parser = true; |
| self.parser = Parser::from_spec(ident, spec); |
| } |
| } |
| } |
| } |
| |
| fn push_doc_comment(&mut self, attrs: &[Attribute], name: &str) { |
| use syn::Lit::*; |
| use syn::Meta::*; |
| |
| let comment_parts: Vec<_> = attrs |
| .iter() |
| .filter(|attr| attr.path.is_ident("doc")) |
| .filter_map(|attr| { |
| if let Ok(NameValue(MetaNameValue { lit: Str(s), .. })) = attr.parse_meta() { |
| Some(s.value()) |
| } else { |
| // non #[doc = "..."] attributes are not our concern |
| // we leave them for rustc to handle |
| None |
| } |
| }) |
| .collect(); |
| |
| self.doc_comment = |
| process_doc_comment(comment_parts, name, self.verbatim_doc_comment.is_none()); |
| } |
| |
| fn set_kind(&mut self, kind: Sp<Kind>) { |
| if let Kind::Arg(_) = *self.kind { |
| self.kind = kind; |
| } else { |
| abort!( |
| kind.span(), |
| "`subcommand`, `flatten`, `external_subcommand` and `skip` cannot be used together" |
| ); |
| } |
| } |
| |
| pub fn find_method(&self, name: &str) -> Option<&Method> { |
| self.methods.iter().find(|m| m.name == name) |
| } |
| |
| pub fn find_default_method(&self) -> Option<&Method> { |
| self.methods |
| .iter() |
| .find(|m| m.name == "default_value" || m.name == "default_value_os") |
| } |
| |
| /// generate methods from attributes on top of struct or enum |
| pub fn initial_top_level_methods(&self) -> TokenStream { |
| let next_display_order = self.next_display_order.as_ref().into_iter(); |
| let next_help_heading = self.next_help_heading.as_ref().into_iter(); |
| let help_heading = self.help_heading.as_ref().into_iter(); |
| quote!( |
| #(#next_display_order)* |
| #(#next_help_heading)* |
| #(#help_heading)* |
| ) |
| } |
| |
| pub fn final_top_level_methods(&self) -> TokenStream { |
| let methods = &self.methods; |
| let doc_comment = &self.doc_comment; |
| |
| quote!( #(#doc_comment)* #(#methods)*) |
| } |
| |
| /// generate methods on top of a field |
| pub fn field_methods(&self, supports_long_help: bool) -> proc_macro2::TokenStream { |
| let methods = &self.methods; |
| let help_heading = self.help_heading.as_ref().into_iter(); |
| match supports_long_help { |
| true => { |
| let doc_comment = &self.doc_comment; |
| quote!( #(#doc_comment)* #(#help_heading)* #(#methods)* ) |
| } |
| false => { |
| let doc_comment = self |
| .doc_comment |
| .iter() |
| .filter(|mth| mth.name != "long_help"); |
| quote!( #(#doc_comment)* #(#help_heading)* #(#methods)* ) |
| } |
| } |
| } |
| |
| pub fn next_display_order(&self) -> TokenStream { |
| let next_display_order = self.next_display_order.as_ref().into_iter(); |
| quote!( #(#next_display_order)* ) |
| } |
| |
| pub fn next_help_heading(&self) -> TokenStream { |
| let next_help_heading = self.next_help_heading.as_ref().into_iter(); |
| let help_heading = self.help_heading.as_ref().into_iter(); |
| quote!( #(#next_help_heading)* #(#help_heading)* ) |
| } |
| |
| pub fn cased_name(&self) -> TokenStream { |
| self.name.clone().translate(*self.casing) |
| } |
| |
| pub fn value_name(&self) -> TokenStream { |
| self.name.clone().translate(CasingStyle::ScreamingSnake) |
| } |
| |
| pub fn parser(&self) -> &Sp<Parser> { |
| &self.parser |
| } |
| |
| pub fn kind(&self) -> Sp<Kind> { |
| self.kind.clone() |
| } |
| |
| pub fn is_enum(&self) -> bool { |
| self.is_enum |
| } |
| |
| pub fn ignore_case(&self) -> TokenStream { |
| let method = self.find_method("ignore_case"); |
| |
| if let Some(method) = method { |
| method.args.clone() |
| } else { |
| quote! { false } |
| } |
| } |
| |
| pub fn casing(&self) -> Sp<CasingStyle> { |
| self.casing.clone() |
| } |
| |
| pub fn env_casing(&self) -> Sp<CasingStyle> { |
| self.env_casing.clone() |
| } |
| |
| pub fn is_positional(&self) -> bool { |
| self.methods |
| .iter() |
| .all(|m| m.name != "long" && m.name != "short") |
| } |
| |
| pub fn has_explicit_methods(&self) -> bool { |
| self.methods |
| .iter() |
| .any(|m| m.name != "help" && m.name != "long_help") |
| } |
| } |
| |
| #[allow(clippy::large_enum_variant)] |
| #[derive(Clone)] |
| pub enum Kind { |
| Arg(Sp<Ty>), |
| FromGlobal(Sp<Ty>), |
| Subcommand(Sp<Ty>), |
| Flatten, |
| Skip(Option<Expr>), |
| ExternalSubcommand, |
| } |
| |
| #[derive(Clone)] |
| pub struct Method { |
| name: Ident, |
| args: TokenStream, |
| } |
| |
| impl Method { |
| pub fn new(name: Ident, args: TokenStream) -> Self { |
| Method { name, args } |
| } |
| |
| fn from_env(ident: Ident, env_var: &str) -> Option<Self> { |
| let mut lit = match env::var(env_var) { |
| Ok(val) => { |
| if val.is_empty() { |
| return None; |
| } |
| LitStr::new(&val, ident.span()) |
| } |
| Err(_) => { |
| abort!(ident, |
| "cannot derive `{}` from Cargo.toml", ident; |
| note = "`{}` environment variable is not set", env_var; |
| help = "use `{} = \"...\"` to set {} manually", ident, ident; |
| ); |
| } |
| }; |
| |
| if ident == "author" { |
| let edited = process_author_str(&lit.value()); |
| lit = LitStr::new(&edited, lit.span()); |
| } |
| |
| Some(Method::new(ident, quote!(#lit))) |
| } |
| } |
| |
| impl ToTokens for Method { |
| fn to_tokens(&self, ts: &mut proc_macro2::TokenStream) { |
| let Method { ref name, ref args } = self; |
| |
| let tokens = quote!( .#name(#args) ); |
| |
| tokens.to_tokens(ts); |
| } |
| } |
| |
| /// replace all `:` with `, ` when not inside the `<>` |
| /// |
| /// `"author1:author2:author3" => "author1, author2, author3"` |
| /// `"author1 <http://website1.com>:author2" => "author1 <http://website1.com>, author2" |
| fn process_author_str(author: &str) -> String { |
| let mut res = String::with_capacity(author.len()); |
| let mut inside_angle_braces = 0usize; |
| |
| for ch in author.chars() { |
| if inside_angle_braces > 0 && ch == '>' { |
| inside_angle_braces -= 1; |
| res.push(ch); |
| } else if ch == '<' { |
| inside_angle_braces += 1; |
| res.push(ch); |
| } else if inside_angle_braces == 0 && ch == ':' { |
| res.push_str(", "); |
| } else { |
| res.push(ch); |
| } |
| } |
| |
| res |
| } |
| |
| #[derive(Clone)] |
| pub struct Parser { |
| pub kind: Sp<ParserKind>, |
| pub func: TokenStream, |
| } |
| |
| impl Parser { |
| fn default_spanned(span: Span) -> Sp<Self> { |
| let kind = Sp::new(ParserKind::TryFromStr, span); |
| let func = quote_spanned!(span=> ::std::str::FromStr::from_str); |
| Sp::new(Parser { kind, func }, span) |
| } |
| |
| fn from_spec(parse_ident: Ident, spec: ParserSpec) -> Sp<Self> { |
| use self::ParserKind::*; |
| |
| let kind = match &*spec.kind.to_string() { |
| "from_str" => FromStr, |
| "try_from_str" => TryFromStr, |
| "from_os_str" => FromOsStr, |
| "try_from_os_str" => TryFromOsStr, |
| "from_occurrences" => FromOccurrences, |
| "from_flag" => FromFlag, |
| s => abort!(spec.kind.span(), "unsupported parser `{}`", s), |
| }; |
| |
| let func = match spec.parse_func { |
| None => match kind { |
| FromStr | FromOsStr => { |
| quote_spanned!(spec.kind.span()=> ::std::convert::From::from) |
| } |
| TryFromStr => quote_spanned!(spec.kind.span()=> ::std::str::FromStr::from_str), |
| TryFromOsStr => abort!( |
| spec.kind.span(), |
| "you must set parser for `try_from_os_str` explicitly" |
| ), |
| FromOccurrences => quote_spanned!(spec.kind.span()=> { |v| v as _ }), |
| FromFlag => quote_spanned!(spec.kind.span()=> ::std::convert::From::from), |
| }, |
| |
| Some(func) => match func { |
| Expr::Path(_) => quote!(#func), |
| _ => abort!(func, "`parse` argument must be a function path"), |
| }, |
| }; |
| |
| let kind = Sp::new(kind, spec.kind.span()); |
| let parser = Parser { kind, func }; |
| Sp::new(parser, parse_ident.span()) |
| } |
| } |
| |
| #[derive(Debug, PartialEq, Clone)] |
| pub enum ParserKind { |
| FromStr, |
| TryFromStr, |
| FromOsStr, |
| TryFromOsStr, |
| FromOccurrences, |
| FromFlag, |
| } |
| |
| /// Defines the casing for the attributes long representation. |
| #[derive(Copy, Clone, Debug, PartialEq)] |
| pub enum CasingStyle { |
| /// Indicate word boundaries with uppercase letter, excluding the first word. |
| Camel, |
| /// Keep all letters lowercase and indicate word boundaries with hyphens. |
| Kebab, |
| /// Indicate word boundaries with uppercase letter, including the first word. |
| Pascal, |
| /// Keep all letters uppercase and indicate word boundaries with underscores. |
| ScreamingSnake, |
| /// Keep all letters lowercase and indicate word boundaries with underscores. |
| Snake, |
| /// Keep all letters lowercase and remove word boundaries. |
| Lower, |
| /// Keep all letters uppercase and remove word boundaries. |
| Upper, |
| /// Use the original attribute name defined in the code. |
| Verbatim, |
| } |
| |
| impl CasingStyle { |
| fn from_lit(name: LitStr) -> Sp<Self> { |
| use self::CasingStyle::*; |
| |
| let normalized = name.value().to_upper_camel_case().to_lowercase(); |
| let cs = |kind| Sp::new(kind, name.span()); |
| |
| match normalized.as_ref() { |
| "camel" | "camelcase" => cs(Camel), |
| "kebab" | "kebabcase" => cs(Kebab), |
| "pascal" | "pascalcase" => cs(Pascal), |
| "screamingsnake" | "screamingsnakecase" => cs(ScreamingSnake), |
| "snake" | "snakecase" => cs(Snake), |
| "lower" | "lowercase" => cs(Lower), |
| "upper" | "uppercase" => cs(Upper), |
| "verbatim" | "verbatimcase" => cs(Verbatim), |
| s => abort!(name, "unsupported casing: `{}`", s), |
| } |
| } |
| } |
| |
| #[derive(Clone)] |
| pub enum Name { |
| Derived(Ident), |
| Assigned(TokenStream), |
| } |
| |
| impl Name { |
| pub fn translate(self, style: CasingStyle) -> TokenStream { |
| use CasingStyle::*; |
| |
| match self { |
| Name::Assigned(tokens) => tokens, |
| Name::Derived(ident) => { |
| let s = ident.unraw().to_string(); |
| let s = match style { |
| Pascal => s.to_upper_camel_case(), |
| Kebab => s.to_kebab_case(), |
| Camel => s.to_lower_camel_case(), |
| ScreamingSnake => s.to_shouty_snake_case(), |
| Snake => s.to_snake_case(), |
| Lower => s.to_snake_case().replace('_', ""), |
| Upper => s.to_shouty_snake_case().replace('_', ""), |
| Verbatim => s, |
| }; |
| quote_spanned!(ident.span()=> #s) |
| } |
| } |
| } |
| |
| pub fn translate_char(self, style: CasingStyle) -> TokenStream { |
| use CasingStyle::*; |
| |
| match self { |
| Name::Assigned(tokens) => quote!( (#tokens).chars().next().unwrap() ), |
| Name::Derived(ident) => { |
| let s = ident.unraw().to_string(); |
| let s = match style { |
| Pascal => s.to_upper_camel_case(), |
| Kebab => s.to_kebab_case(), |
| Camel => s.to_lower_camel_case(), |
| ScreamingSnake => s.to_shouty_snake_case(), |
| Snake => s.to_snake_case(), |
| Lower => s.to_snake_case(), |
| Upper => s.to_shouty_snake_case(), |
| Verbatim => s, |
| }; |
| |
| let s = s.chars().next().unwrap(); |
| quote_spanned!(ident.span()=> #s) |
| } |
| } |
| } |
| } |