use proc_macro2::{
Ident,
TokenStream as TokenStream2,
};
use quote::ToTokens;
use syn::{
ext::IdentExt as _,
parse::{
Parse,
ParseStream,
},
punctuated::Punctuated,
spanned::Spanned,
Token,
};
#[derive(Debug, PartialEq, Eq)]
pub struct AttributeArgs {
args: Punctuated<MetaNameValue, Token![,]>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct MetaNameValue {
pub name: syn::Path,
pub eq_token: syn::token::Eq,
pub value: PathOrLit,
}
#[derive(Debug, PartialEq, Eq)]
pub enum PathOrLit {
Path(syn::Path),
Lit(syn::Lit),
}
impl IntoIterator for AttributeArgs {
type Item = MetaNameValue;
type IntoIter = syn::punctuated::IntoIter<MetaNameValue>;
fn into_iter(self) -> Self::IntoIter {
self.args.into_iter()
}
}
impl Parse for AttributeArgs {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
Ok(Self {
args: Punctuated::parse_terminated(input)?,
})
}
}
impl Parse for MetaNameValue {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let path = input.call(Self::parse_meta_path)?;
Self::parse_meta_name_value_after_path(path, input)
}
}
impl ToTokens for PathOrLit {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
Self::Lit(lit) => lit.to_tokens(tokens),
Self::Path(path) => path.to_tokens(tokens),
}
}
}
impl ToTokens for MetaNameValue {
fn to_tokens(&self, tokens: &mut TokenStream2) {
self.name.to_tokens(tokens);
self.eq_token.to_tokens(tokens);
self.value.to_tokens(tokens);
}
}
impl MetaNameValue {
fn parse_meta_path(input: ParseStream) -> Result<syn::Path, syn::Error> {
Ok(syn::Path {
leading_colon: input.parse()?,
segments: {
let mut segments = Punctuated::new();
while input.peek(Ident::peek_any) {
let ident = Ident::parse_any(input)?;
segments.push_value(syn::PathSegment::from(ident));
if !input.peek(syn::Token![::]) {
break
}
let punct = input.parse()?;
segments.push_punct(punct);
}
if segments.is_empty() {
return Err(input.error("expected path"))
} else if segments.trailing_punct() {
return Err(input.error("expected path segment"))
}
segments
},
})
}
fn parse_meta_name_value_after_path(
name: syn::Path,
input: ParseStream,
) -> Result<MetaNameValue, syn::Error> {
let span = name.span();
Ok(MetaNameValue {
name,
eq_token: input.parse().map_err(|_error| {
format_err!(
span,
"ink! config options require an argument separated by '='",
)
})?,
value: input.parse()?,
})
}
}
impl Parse for PathOrLit {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
if input.fork().peek(syn::Lit) {
return input.parse::<syn::Lit>().map(PathOrLit::Lit)
}
if input.fork().peek(Ident::peek_any) || input.fork().peek(Token![::]) {
return input.parse::<syn::Path>().map(PathOrLit::Path)
}
Err(input.error("cannot parse into either literal or path"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
impl AttributeArgs {
pub fn new<I>(args: I) -> Self
where
I: IntoIterator<Item = MetaNameValue>,
{
Self {
args: args.into_iter().collect(),
}
}
}
#[test]
fn empty_works() {
assert_eq!(
syn::parse2::<AttributeArgs>(quote! {}).unwrap(),
AttributeArgs::new(vec![])
)
}
#[test]
fn literal_bool_value_works() {
assert_eq!(
syn::parse2::<AttributeArgs>(quote! { name = true }).unwrap(),
AttributeArgs::new(vec![MetaNameValue {
name: syn::parse_quote! { name },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Lit(syn::parse_quote! { true }),
}])
)
}
#[test]
fn literal_str_value_works() {
assert_eq!(
syn::parse2::<AttributeArgs>(quote! { name = "string literal" }).unwrap(),
AttributeArgs::new(vec![MetaNameValue {
name: syn::parse_quote! { name },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Lit(syn::parse_quote! { "string literal" }),
}])
)
}
#[test]
fn ident_value_works() {
assert_eq!(
syn::parse2::<AttributeArgs>(quote! { name = MyIdentifier }).unwrap(),
AttributeArgs::new(vec![MetaNameValue {
name: syn::parse_quote! { name },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Path(syn::parse_quote! { MyIdentifier }),
}])
)
}
#[test]
fn root_path_value_works() {
assert_eq!(
syn::parse2::<AttributeArgs>(quote! { name = ::this::is::my::Path }).unwrap(),
AttributeArgs::new(vec![MetaNameValue {
name: syn::parse_quote! { name },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Path(syn::parse_quote! { ::this::is::my::Path }),
}])
)
}
#[test]
fn relative_path_value_works() {
assert_eq!(
syn::parse2::<AttributeArgs>(quote! { name = this::is::my::relative::Path })
.unwrap(),
AttributeArgs::new(vec![MetaNameValue {
name: syn::parse_quote! { name },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Path(
syn::parse_quote! { this::is::my::relative::Path }
),
}])
)
}
#[test]
fn trailing_comma_works() {
let mut expected_args = Punctuated::new();
expected_args.push_value(MetaNameValue {
name: syn::parse_quote! { name },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Path(syn::parse_quote! { value }),
});
expected_args.push_punct(<Token![,]>::default());
assert_eq!(
syn::parse2::<AttributeArgs>(quote! { name = value, }).unwrap(),
AttributeArgs {
args: expected_args,
}
)
}
#[test]
fn many_mixed_works() {
assert_eq!(
syn::parse2::<AttributeArgs>(quote! {
name1 = ::root::Path,
name2 = false,
name3 = "string literal",
name4 = 42,
name5 = 7.7
})
.unwrap(),
AttributeArgs::new(vec![
MetaNameValue {
name: syn::parse_quote! { name1 },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Path(syn::parse_quote! { ::root::Path }),
},
MetaNameValue {
name: syn::parse_quote! { name2 },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Lit(syn::parse_quote! { false }),
},
MetaNameValue {
name: syn::parse_quote! { name3 },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Lit(syn::parse_quote! { "string literal" }),
},
MetaNameValue {
name: syn::parse_quote! { name4 },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Lit(syn::parse_quote! { 42 }),
},
MetaNameValue {
name: syn::parse_quote! { name5 },
eq_token: syn::parse_quote! { = },
value: PathOrLit::Lit(syn::parse_quote! { 7.7 }),
},
])
)
}
}