diff --git a/src/attr.rs b/src/attr.rs index 8f1945f1..1c02d748 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -1,96 +1,153 @@ -use crate::Result; +use crate::{error::compile_error_at, Result}; +use proc_macro2::Span; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, - Attribute, LitStr, Token, + spanned::Spanned, + Attribute, Ident, Lit, Path, Token, }; -pub(crate) enum CommandAttrName { - Prefix, - Description, - Rename, - ParseWith, - Separator, +pub(crate) fn fold_attrs( + attrs: &[Attribute], + filter: fn(&Attribute) -> bool, + parse: impl Fn(Attr) -> Result, + init: A, + f: impl Fn(A, R) -> Result, +) -> Result { + attrs + .iter() + .filter(|&a| filter(a)) + .flat_map(|attribute| { + // FIXME: don't allocate here + let attrs = + match attribute.parse_args_with(|input: &ParseBuffer| { + input.parse_terminated::<_, Token![,]>(Attr::parse) + }) { + Ok(ok) => ok, + Err(err) => return vec![Err(err.into())], + }; + + attrs.into_iter().map(&parse).collect() + }) + .try_fold(init, |acc, r| r.and_then(|r| f(acc, r))) } -impl Parse for CommandAttrName { - fn parse(input: ParseStream) -> syn::Result { - let name_arg: syn::Ident = input.parse()?; +/// An attribute key-value pair. +/// +/// For example: +/// ```text +/// #[blahblah(key = "puff", value = 12, nope)] +/// ^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^ +/// ``` +pub(crate) struct Attr { + pub key: Ident, + pub value: AttrValue, +} - match name_arg.to_string().as_str() { - "prefix" => Ok(CommandAttrName::Prefix), - "description" => Ok(CommandAttrName::Description), - "rename" => Ok(CommandAttrName::Rename), - "parse_with" => Ok(CommandAttrName::ParseWith), - "separator" => Ok(CommandAttrName::Separator), - _ => Err(syn::Error::new( - name_arg.span(), - "unexpected attribute name (expected one of `prefix`, \ - `description`, `rename`, `parse_with`, `separator`", - )), +/// Value of an attribute. +/// +/// For example: +/// ```text +/// #[blahblah(key = "puff", value = 12, nope)] +/// ^^^^^^ ^^ ^-- (None pseudo-value) +/// ``` +pub(crate) enum AttrValue { + Path(Path), + Lit(Lit), + None(Span), +} + +impl Parse for Attr { + fn parse(input: ParseStream) -> syn::Result { + let key = input.parse::()?; + + let value = match input.peek(Token![=]) { + true => { + input.parse::()?; + input.parse::()? + } + false => AttrValue::None(input.span()), + }; + + Ok(Self { key, value }) + } +} + +impl Attr { + pub(crate) fn span(&self) -> Span { + self.key.span().join(self.value.span()).unwrap_or(self.key.span()) + } +} + +impl AttrValue { + /// Unwraps this value if it's a string literal. + pub fn expect_string(self) -> Result { + self.expect("a string", |this| match this { + AttrValue::Lit(Lit::Str(s)) => Ok(s.value()), + _ => Err(this), + }) + } + + // /// Unwraps this value if it's a path. + // pub fn expect_path(self) -> Result { + // self.expect("a path", |this| match this { + // AttrValue::Path(p) => Ok(p), + // _ => Err(this), + // }) + // } + + fn expect( + self, + expected: &str, + f: impl FnOnce(Self) -> Result, + ) -> Result { + f(self).map_err(|this| { + compile_error_at( + &format!("expected {expected}, found {}", this.descr()), + this.span(), + ) + }) + } + + fn descr(&self) -> &'static str { + use Lit::*; + + match self { + Self::None(_) => "nothing", + Self::Lit(l) => match l { + Str(_) | ByteStr(_) => "a string", + Char(_) => "a character", + Byte(_) | Int(_) => "an integer", + Float(_) => "a floating point integer", + Bool(_) => "a boolean", + Verbatim(_) => ":shrug:", + }, + Self::Path(_) => "a path", + } + } + + /// Returns span of the value + /// + /// ```text + /// #[blahblah(key = "puff", value = 12, nope )] + /// ^^^^^^ ^^ ^ + /// ``` + fn span(&self) -> Span { + match self { + Self::Path(p) => p.span(), + Self::Lit(l) => l.span(), + Self::None(sp) => *sp, } } } -pub(crate) struct CommandAttr { - pub name: CommandAttrName, - pub value: String, -} - -impl Parse for CommandAttr { +impl Parse for AttrValue { fn parse(input: ParseStream) -> syn::Result { - let name = input.parse::()?; + let this = match input.peek(Lit) { + true => Self::Lit(input.parse()?), + false => Self::Path(input.parse()?), + }; - // FIXME: this should support value-less attrs, as well as - // non-string-literal values - input.parse::()?; - let value = input.parse::()?.value(); - - Ok(Self { name, value }) - } -} - -pub(crate) struct CommandAttrs(Vec); - -impl CommandAttrs { - pub fn from_attributes(attributes: &[Attribute]) -> Result { - let mut attrs = Vec::new(); - - for attribute in attributes.iter().filter(is_command_attribute) { - let attrs_ = attribute.parse_args_with(|input: &ParseBuffer| { - input.parse_terminated::<_, Token![,]>(CommandAttr::parse) - })?; - - attrs.extend(attrs_); - } - - Ok(Self(attrs)) - } -} - -impl<'a> IntoIterator for &'a CommandAttrs { - type Item = &'a CommandAttr; - - type IntoIter = std::slice::Iter<'a, CommandAttr>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } -} - -impl IntoIterator for CommandAttrs { - type Item = CommandAttr; - - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -fn is_command_attribute(a: &&Attribute) -> bool { - match a.path.get_ident() { - Some(ident) => ident == "command", - _ => false, + Ok(this) } } diff --git a/src/bot_commands.rs b/src/bot_commands.rs index 85ea4a09..02b64e63 100644 --- a/src/bot_commands.rs +++ b/src/bot_commands.rs @@ -1,5 +1,5 @@ use crate::{ - attr::CommandAttrs, command::Command, command_enum::CommandEnum, + command::Command, command_attr::CommandAttrs, command_enum::CommandEnum, compile_error, fields_parse::impl_parse_args, unzip::Unzip, Result, }; diff --git a/src/command.rs b/src/command.rs index b06038f1..769c83cc 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,9 +1,6 @@ use crate::{ - attr::{self, CommandAttr, CommandAttrName}, - command_enum::CommandEnum, - fields_parse::ParserType, - rename_rules::RenameRule, - Result, + command_attr::CommandAttrs, command_enum::CommandEnum, + fields_parse::ParserType, rename_rules::RenameRule, Result, }; pub(crate) struct Command { @@ -14,8 +11,7 @@ pub(crate) struct Command { } impl Command { - pub fn try_from(attrs: attr::CommandAttrs, name: &str) -> Result { - let attrs = parse_attrs(attrs)?; + pub fn try_from(attrs: CommandAttrs, name: &str) -> Result { let CommandAttrs { prefix, description, @@ -24,7 +20,7 @@ impl Command { separator: _, } = attrs; - let name = rename_rule.apply(name); + let name = rename_rule.unwrap_or(RenameRule::Identity).apply(name); Ok(Self { prefix, description, parser, name }) } @@ -60,33 +56,3 @@ impl Command { self.description != Some("off".to_owned()) } } - -pub(crate) struct CommandAttrs { - pub prefix: Option, - pub description: Option, - pub rename_rule: RenameRule, - pub parser: Option, - pub separator: Option, -} - -pub(crate) fn parse_attrs(attrs: attr::CommandAttrs) -> Result { - let mut prefix = None; - let mut description = None; - let mut rename_rule = RenameRule::Identity; - let mut parser = None; - let mut separator = None; - - for CommandAttr { name, value } in attrs { - match name { - CommandAttrName::Prefix => prefix = Some(value), - CommandAttrName::Description => description = Some(value), - CommandAttrName::Rename => rename_rule = RenameRule::parse(&value)?, - CommandAttrName::ParseWith => { - parser = Some(ParserType::parse(&value)) - } - CommandAttrName::Separator => separator = Some(value), - } - } - - Ok(CommandAttrs { prefix, description, rename_rule, parser, separator }) -} diff --git a/src/command_attr.rs b/src/command_attr.rs new file mode 100644 index 00000000..4550f679 --- /dev/null +++ b/src/command_attr.rs @@ -0,0 +1,115 @@ +use crate::{ + attr::{fold_attrs, Attr}, + error::compile_error_at, + fields_parse::ParserType, + rename_rules::RenameRule, + Result, +}; + +use proc_macro2::Span; +use syn::Attribute; + +/// Attributes for `BotCommands` derive macro. +pub(crate) struct CommandAttrs { + pub prefix: Option, + pub description: Option, + pub rename_rule: Option, + pub parser: Option, + pub separator: Option, +} + +/// An attribute for `BotCommands` derive macro. +pub(crate) struct CommandAttr { + kind: CommandAttrKind, + sp: Span, +} + +pub(crate) enum CommandAttrKind { + Prefix(String), + Description(String), + Rename(RenameRule), + ParseWith(ParserType), + Separator(String), +} + +impl CommandAttrs { + pub fn from_attributes(attributes: &[Attribute]) -> Result { + use CommandAttrKind::*; + + fold_attrs( + attributes, + is_command_attribute, + CommandAttr::parse, + Self { + prefix: None, + description: None, + rename_rule: None, + parser: None, + separator: None, + }, + |mut this, attr| { + fn insert( + opt: &mut Option, + x: T, + sp: Span, + ) -> Result<()> { + match opt { + slot @ None => { + *slot = Some(x); + Ok(()) + } + Some(_) => { + Err(compile_error_at("duplicate attribute", sp)) + } + } + } + + match attr.kind { + Prefix(p) => insert(&mut this.prefix, p, attr.sp), + Description(d) => insert(&mut this.description, d, attr.sp), + Rename(r) => insert(&mut this.rename_rule, r, attr.sp), + ParseWith(p) => insert(&mut this.parser, p, attr.sp), + Separator(s) => insert(&mut this.separator, s, attr.sp), + }?; + + Ok(this) + }, + ) + } +} + +impl CommandAttr { + fn parse(attr: Attr) -> Result { + use CommandAttrKind::*; + + let sp = attr.span(); + let Attr { key, value } = attr; + let kind = match &*key.to_string() { + "prefix" => Prefix(value.expect_string()?), + "description" => Description(value.expect_string()?), + "rename" => Rename( + value.expect_string().and_then(|r| RenameRule::parse(&r))?, + ), + "parse_with" => { + ParseWith(value.expect_string().map(|p| ParserType::parse(&p))?) + } + "separator" => Separator(value.expect_string()?), + _ => { + return Err(compile_error_at( + "unexpected attribute name (expected one of `prefix`, \ + `description`, `rename`, `parse_with` and `separator`", + key.span(), + )) + } + }; + + Ok(Self { kind, sp }) + } +} + +fn is_command_attribute(a: &Attribute) -> bool { + match a.path.get_ident() { + Some(ident) => ident == "command", + _ => false, + } +} diff --git a/src/command_enum.rs b/src/command_enum.rs index 14aa0381..ad3959a0 100644 --- a/src/command_enum.rs +++ b/src/command_enum.rs @@ -1,5 +1,5 @@ use crate::{ - attr, command::parse_attrs, fields_parse::ParserType, + command_attr::CommandAttrs, fields_parse::ParserType, rename_rules::RenameRule, Result, }; @@ -12,14 +12,17 @@ pub(crate) struct CommandEnum { } impl CommandEnum { - pub fn try_from(attrs: attr::CommandAttrs) -> Result { - let attrs = parse_attrs(attrs)?; + pub fn try_from(attrs: CommandAttrs) -> Result { + let CommandAttrs { + prefix, + description, + rename_rule, + parser, + separator, + } = attrs; + let mut parser = parser.unwrap_or(ParserType::Default); - let prefix = attrs.prefix; - let description = attrs.description; - let rename = attrs.rename_rule; - let separator = attrs.separator; - let mut parser = attrs.parser.unwrap_or(ParserType::Default); + // FIXME: Error on unused separator if let (ParserType::Split { separator }, Some(s)) = (&mut parser, &separator) { @@ -28,7 +31,7 @@ impl CommandEnum { Ok(Self { prefix, description, - rename_rule: rename, + rename_rule: rename_rule.unwrap_or(RenameRule::Identity), parser_type: parser, }) } diff --git a/src/error.rs b/src/error.rs index e2f98e98..3f1603bf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; pub(crate) type Result = std::result::Result; @@ -13,6 +13,36 @@ where Error(quote! { compile_error! { #data } }) } +pub(crate) fn compile_error_at(msg: &str, sp: Span) -> Error { + use proc_macro2::{ + Delimiter, Group, Ident, Literal, Punct, Spacing, TokenTree, + }; + use std::iter::FromIterator; + + // compile_error! { $msg } + let ts = TokenStream::from_iter(vec![ + TokenTree::Ident(Ident::new("compile_error", sp)), + TokenTree::Punct({ + let mut punct = Punct::new('!', Spacing::Alone); + punct.set_span(sp); + punct + }), + TokenTree::Group({ + let mut group = Group::new(Delimiter::Brace, { + TokenStream::from_iter(vec![TokenTree::Literal({ + let mut string = Literal::string(msg); + string.set_span(sp); + string + })]) + }); + group.set_span(sp); + group + }), + ]); + + Error(ts) +} + impl From for proc_macro2::TokenStream { fn from(Error(e): Error) -> Self { e diff --git a/src/fields_parse.rs b/src/fields_parse.rs index d3a4ab34..a9a2d0b2 100644 --- a/src/fields_parse.rs +++ b/src/fields_parse.rs @@ -9,6 +9,7 @@ pub(crate) enum ParserType { } impl ParserType { + // FIXME: use path for custom pub fn parse(data: &str) -> Self { match data { "default" => ParserType::Default, diff --git a/src/lib.rs b/src/lib.rs index e590ead3..bb0e0c04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ extern crate proc_macro; mod attr; mod bot_commands; mod command; +mod command_attr; mod command_enum; mod error; mod fields_parse;