use proc_macro2::{Span, TokenStream};
use syn::{Error, ItemFn, LitStr, meta::ParseNestedMeta, spanned::Spanned};
pub fn parse(tokens_attrs: TokenStream, tokens_item: TokenStream) -> Result<Ast, Error> {
let attrs = Attributes::parse(tokens_attrs)?;
let handler = syn::parse2(tokens_item)?;
Ok(Ast::new(attrs, handler))
}
#[derive(Clone, Debug)]
pub struct Ast {
pub attrs: Attributes,
pub handler: ItemFn,
}
impl Ast {
pub fn new(attrs: Attributes, handler: ItemFn) -> Self {
Self { attrs, handler }
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Attributes {
pub name: Option<String>,
pub kind: HandlerKind,
}
impl Attributes {
fn builder() -> AttributesBuilder {
AttributesBuilder::default()
}
pub fn parse(tokens: TokenStream) -> Result<Self, Error> {
let mut attrs = Self::builder();
let attrs_span = tokens.span();
let attrs_parser = syn::meta::parser(|meta| Ok(attrs.parse(meta)?));
syn::parse::Parser::parse2(attrs_parser, tokens)?;
attrs
.build()
.map_err(|e| Error::new(attrs_span, e.to_string()))
}
}
#[cfg(test)]
impl Attributes {
pub(crate) fn query() -> Self {
Self {
kind: HandlerKind::Query,
name: None,
}
}
pub(crate) fn mutation() -> Self {
Self {
kind: HandlerKind::Mutation,
name: None,
}
}
pub(crate) fn subscription() -> Self {
Self {
kind: HandlerKind::Subscription,
name: None,
}
}
pub(crate) fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.name = Some(name.as_ref().to_string());
self
}
}
#[derive(Clone, Debug, Default)]
struct AttributesBuilder {
name: Option<String>,
kind: Option<HandlerKind>,
}
impl AttributesBuilder {
fn build(self) -> Result<Attributes, AttributesBuilderError> {
Ok(Attributes {
name: self.name,
kind: self.kind.ok_or(AttributesBuilderError::KindRequired)?,
})
}
fn parse(&mut self, meta: ParseNestedMeta) -> Result<(), AttributesParseError> {
if let Some(ident) = meta.path.get_ident() {
if let Ok(kind) = HandlerKind::try_from(ident.to_string().as_str()) {
if self.kind.is_some() {
return Err(AttributesParseError::KindProvided(ident.span()));
}
self.kind = Some(kind);
return Ok(());
}
}
if meta.path.is_ident("name") {
let path_span = meta.path.span();
let value = meta.value()?;
let name = value.parse::<LitStr>()?.value();
if self.name.is_some() {
return Err(AttributesParseError::NameProvided(path_span));
}
self.name = Some(name);
return Ok(());
}
Err(AttributesParseError::UnsupportedProperty(meta.path.span()))
}
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum AttributesBuilderError {
#[error("one of `query`/`mutation`/`subscription` is required")]
KindRequired,
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum AttributesParseError {
#[error("handler kind has already been provided")]
KindProvided(Span),
#[error("handler name has already been provided")]
NameProvided(Span),
#[error("unknown attribute")]
UnsupportedProperty(Span),
#[error(transparent)]
ParseError(#[from] Error),
}
impl From<AttributesParseError> for Error {
fn from(err: AttributesParseError) -> Self {
Error::new(
match err {
AttributesParseError::KindProvided(span) => span,
AttributesParseError::NameProvided(span) => span,
AttributesParseError::UnsupportedProperty(span) => span,
AttributesParseError::ParseError(error) => return error,
},
err.to_string(),
)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HandlerKind {
Query,
Mutation,
Subscription,
}
impl TryFrom<&str> for HandlerKind {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
Ok(match s {
"query" => Self::Query,
"mutation" => Self::Mutation,
"subscription" => Self::Subscription,
_ => return Err(()),
})
}
}
#[cfg(test)]
mod test {
use super::*;
use quote::quote;
use rstest::*;
#[rstest]
#[case::query(quote!(query), Attributes::query())]
#[case::mutation(quote!(mutation), Attributes::mutation())]
#[case::subscription(quote!(subscription), Attributes::subscription())]
#[case::kind_name(quote!(query, name = "other_name"), Attributes::query().with_name("other_name"))]
#[case::name_kind(quote!(name = "other_name", mutation), Attributes::mutation().with_name("other_name"))]
fn parse_attributes(#[case] tokens: TokenStream, #[case] expected: Attributes) {
let attrs = Attributes::parse(tokens).unwrap();
assert_eq!(attrs, expected);
}
#[rstest]
#[case::multiple_kind(quote!(query, mutation))]
#[case::no_kind(quote!(name = "other_name"))]
#[case::multiple_name(quote!(query, name = "name_1", name = "name_2"))]
fn parse_attributes_fail(#[case] tokens: TokenStream) {
assert!(Attributes::parse(tokens).is_err());
}
}