use syn::{
Attribute, Path, PathSegment, Lit, LitBool, Ident,
token::Paren,
punctuated::Punctuated,
};
use quote::ToTokens;
use proc_macro2::{ Delimiter, Spacing, TokenTree, TokenStream };
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ExtMeta {
Path(Path),
List(Path, Paren, Punctuated<NestedExtMeta, Token![,]>),
KeyValue(Path, Token![=], Lit),
}
impl ExtMeta {
pub fn path(&self) -> &Path {
match *self {
ExtMeta::Path(ref path) => path,
ExtMeta::List(ref path, ..) => path,
ExtMeta::KeyValue(ref path, ..) => path,
}
}
pub fn path_str(&self) -> String {
self.path().colon_sep_str()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum NestedExtMeta {
Meta(ExtMeta),
Literal(Lit),
}
impl From<ExtMeta> for NestedExtMeta {
fn from(meta: ExtMeta) -> Self {
NestedExtMeta::Meta(meta)
}
}
impl From<Lit> for NestedExtMeta {
fn from(lit: Lit) -> Self {
NestedExtMeta::Literal(lit)
}
}
pub trait PathExt {
fn colon_sep_str(&self) -> String;
fn dot_sep_str(&self) -> String;
}
impl PathExt for Path {
fn colon_sep_str(&self) -> String {
let mut ts = TokenStream::new();
self.to_tokens(&mut ts);
ts.to_string()
}
fn dot_sep_str(&self) -> String {
let idents: Vec<_> = self.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect();
idents.join(".")
}
}
pub trait AttributeExt {
fn parse_ext_meta(&self) -> Option<ExtMeta>;
}
impl AttributeExt for Attribute {
fn parse_ext_meta(&self) -> Option<ExtMeta> {
let path = if self.path.segments.is_empty() {
return None;
} else {
&self.path
};
let tts: Vec<_> = self.tts.clone().into_iter().collect();
meta_from_path_and_token_trees(path, &tts)
}
}
fn meta_from_path_and_token_trees(path: &Path, tts: &[TokenTree]) -> Option<ExtMeta> {
if tts.is_empty() {
return Some(ExtMeta::Path(path.clone()));
}
if tts.len() == 1 {
if let Some(meta) = extract_meta_list(path.clone(), &tts[0]) {
return Some(meta);
}
}
if tts.len() == 2 {
if let Some(meta) = extract_name_value(path.clone(), &tts[0], &tts[1]) {
return Some(meta);
}
}
None
}
fn extract_meta_list(path: Path, tt: &TokenTree) -> Option<ExtMeta> {
let g = match *tt {
TokenTree::Group(ref g) => g,
_ => return None,
};
if g.delimiter() != Delimiter::Parenthesis {
return None;
}
let tokens: Vec<_> = g.stream().clone().into_iter().collect();
let nested = list_of_nested_meta_items_from_tokens(&tokens)?;
Some(ExtMeta::List(path, Paren(g.span()), nested))
}
fn extract_name_value(path: Path, eq: &TokenTree, lit: &TokenTree) -> Option<ExtMeta> {
let eq_punct = match *eq {
TokenTree::Punct(ref o) => o,
_ => return None,
};
if eq_punct.spacing() != Spacing::Alone {
return None;
}
if eq_punct.as_char() != '=' {
return None;
}
match *lit {
TokenTree::Literal(ref l) if !l.to_string().starts_with('/') => {
Some(ExtMeta::KeyValue(
path,
Token]),
Lit::new(l.clone()),
))
}
TokenTree::Ident(ref v) => match &v.to_string()[..] {
v @ "true" | v @ "false" => Some(ExtMeta::KeyValue(
path,
Token]),
Lit::Bool(LitBool {
value: v == "true",
span: lit.span(),
}),
)),
_ => None,
},
_ => None,
}
}
fn nested_meta_item_from_tokens(tts: &[TokenTree]) -> Option<NestedExtMeta> {
match *tts.first()? {
TokenTree::Literal(ref lit) => {
if tts.len() == 1 && !lit.to_string().starts_with('/') {
Some(NestedExtMeta::Literal(Lit::new(lit.clone())))
} else {
None
}
}
TokenTree::Ident(_) => {
let (path, rest) = path_from_prefix_of_token_trees(tts)?;
meta_from_path_and_token_trees(&path, rest).map(NestedExtMeta::Meta)
}
_ => None
}
}
fn list_of_nested_meta_items_from_tokens(
mut tts: &[TokenTree],
) -> Option<Punctuated<NestedExtMeta, Token![,]>> {
let mut nested_meta_items = Punctuated::new();
loop {
let mut comma = None;
let mut i = 0;
while i < tts.len() {
if let TokenTree::Punct(ref op) = tts[i] {
if op.as_char() == ',' && op.spacing() == Spacing::Alone {
comma = Some(Token]));
i += 1;
break;
}
}
i += 1;
}
let until_next_comma = if comma.is_some() {
&tts[..i - 1]
} else {
&tts[..i]
};
tts = &tts[i..];
if until_next_comma.is_empty() {
if comma.is_some() {
break None; } else {
break Some(nested_meta_items);
}
}
let nested = nested_meta_item_from_tokens(until_next_comma)?;
nested_meta_items.push_value(nested);
if let Some(comma) = comma {
nested_meta_items.push_punct(comma);
}
}
}
fn path_from_prefix_of_token_trees(tts: &[TokenTree]) -> Option<(Path, &[TokenTree])> {
let mut i = 0;
let mut path = Path {
leading_colon: None,
segments: Punctuated::new(),
};
while i < tts.len() {
if let TokenTree::Ident(ref ident) = tts[i] {
path.segments.push(PathSegment {
ident: Ident::new(&ident.to_string(), ident.span()),
arguments: Default::default(),
});
if let (
Some(&TokenTree::Punct(ref op_left)),
Some(&TokenTree::Punct(ref op_right)),
) = (tts.get(i + 1), tts.get(i + 2)) {
if op_left.as_char() == ':'
&& op_left.spacing() == Spacing::Joint
&& op_right.as_char() == ':'
&& op_right.spacing() == Spacing::Alone
{
i += 3;
continue;
}
}
} else {
break;
}
i += 1;
}
if path.segments.is_empty() {
None
} else {
Some((path, &tts[i..]))
}
}