use std::mem;
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::{
ext::IdentExt as _,
parse::{Parse, ParseStream},
spanned::Spanned,
token,
};
use crate::common::{
default, diagnostic, filter_attrs,
parse::{
attr::{err, OptionExt as _},
ParseBufferExt as _, TypeExt as _,
},
path_eq_single, rename, scalar, Description, SpanContainer,
};
#[derive(Debug, Default)]
pub(crate) struct Attr {
pub(crate) name: Option<SpanContainer<syn::LitStr>>,
pub(crate) description: Option<SpanContainer<Description>>,
pub(crate) default: Option<SpanContainer<default::Value>>,
pub(crate) context: Option<SpanContainer<syn::Ident>>,
pub(crate) executor: Option<SpanContainer<syn::Ident>>,
}
impl Parse for Attr {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut out = Self::default();
while !input.is_empty() {
let ident = input.parse::<syn::Ident>()?;
match ident.to_string().as_str() {
"name" => {
input.parse::<token::Eq>()?;
let name = input.parse::<syn::LitStr>()?;
out.name
.replace(SpanContainer::new(ident.span(), Some(name.span()), name))
.none_or_else(|_| err::dup_arg(&ident))?
}
"desc" | "description" => {
input.parse::<token::Eq>()?;
let desc = input.parse::<Description>()?;
out.description
.replace(SpanContainer::new(ident.span(), Some(desc.span()), desc))
.none_or_else(|_| err::dup_arg(&ident))?
}
"default" => {
let val = input.parse::<default::Value>()?;
out.default
.replace(SpanContainer::new(ident.span(), Some(val.span()), val))
.none_or_else(|_| err::dup_arg(&ident))?
}
"ctx" | "context" | "Context" => {
let span = ident.span();
out.context
.replace(SpanContainer::new(span, Some(span), ident))
.none_or_else(|_| err::dup_arg(span))?
}
"exec" | "executor" => {
let span = ident.span();
out.executor
.replace(SpanContainer::new(span, Some(span), ident))
.none_or_else(|_| err::dup_arg(span))?
}
name => {
return Err(err::unknown_arg(&ident, name));
}
}
input.try_parse::<token::Comma>()?;
}
Ok(out)
}
}
impl Attr {
fn try_merge(self, mut another: Self) -> syn::Result<Self> {
Ok(Self {
name: try_merge_opt!(name: self, another),
description: try_merge_opt!(description: self, another),
default: try_merge_opt!(default: self, another),
context: try_merge_opt!(context: self, another),
executor: try_merge_opt!(executor: self, another),
})
}
pub(crate) fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result<Self> {
let attr = filter_attrs(name, attrs)
.map(|attr| attr.parse_args())
.try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?;
if let Some(context) = &attr.context {
if attr.name.is_some()
|| attr.description.is_some()
|| attr.default.is_some()
|| attr.executor.is_some()
{
return Err(syn::Error::new(
context.span(),
"`context` attribute argument is not composable with any other arguments",
));
}
}
if let Some(executor) = &attr.executor {
if attr.name.is_some()
|| attr.description.is_some()
|| attr.default.is_some()
|| attr.context.is_some()
{
return Err(syn::Error::new(
executor.span(),
"`executor` attribute argument is not composable with any other arguments",
));
}
}
Ok(attr)
}
fn ensure_no_regular_arguments(&self) -> syn::Result<()> {
if let Some(span) = &self.name {
return Err(Self::err_disallowed(&span, "name"));
}
if let Some(span) = &self.description {
return Err(Self::err_disallowed(&span, "description"));
}
if let Some(span) = &self.default {
return Err(Self::err_disallowed(&span, "default"));
}
Ok(())
}
#[must_use]
fn err_disallowed<S: Spanned>(span: &S, arg: &str) -> syn::Error {
syn::Error::new(
span.span(),
format!("attribute argument `#[graphql({arg} = ...)]` is not allowed here",),
)
}
}
#[derive(Debug)]
pub(crate) struct OnField {
pub(crate) ty: syn::Type,
pub(crate) name: String,
pub(crate) description: Option<Description>,
pub(crate) default: Option<default::Value>,
}
#[derive(Debug)]
pub(crate) enum OnMethod {
Regular(Box<OnField>),
Context(Box<syn::Type>),
Executor,
}
impl OnMethod {
#[must_use]
pub(crate) fn as_regular(&self) -> Option<&OnField> {
if let Self::Regular(arg) = self {
Some(&**arg)
} else {
None
}
}
#[must_use]
pub(crate) fn context_ty(&self) -> Option<&syn::Type> {
if let Self::Context(ty) = self {
Some(&**ty)
} else {
None
}
}
#[must_use]
pub(crate) fn method_mark_tokens(&self, scalar: &scalar::Type) -> Option<TokenStream> {
let ty = &self.as_regular()?.ty;
Some(quote_spanned! { ty.span() =>
<#ty as ::juniper::marker::IsInputType<#scalar>>::mark();
})
}
#[must_use]
pub(crate) fn method_meta_tokens(&self) -> Option<TokenStream> {
let arg = self.as_regular()?;
let (name, ty) = (&arg.name, &arg.ty);
let description = &arg.description;
let method = if let Some(val) = &arg.default {
quote_spanned! { val.span() =>
.arg_with_default::<#ty>(#name, &#val, info)
}
} else {
quote! { .arg::<#ty>(#name, info) }
};
Some(quote! { .argument(registry #method #description) })
}
#[must_use]
pub(crate) fn method_resolve_field_tokens(
&self,
scalar: &scalar::Type,
for_async: bool,
) -> TokenStream {
match self {
Self::Regular(arg) => {
let (name, ty) = (&arg.name, &arg.ty);
let err_text = format!("Missing argument `{name}`: {{}}");
let arg = quote! {
args.get::<#ty>(#name).and_then(|opt| opt.map_or_else(|| {
<#ty as ::juniper::FromInputValue<#scalar>>::from_implicit_null()
.map_err(|e| {
::juniper::IntoFieldError::<#scalar>::into_field_error(e)
.map_message(|m| format!(#err_text, m))
})
}, Ok))
};
if for_async {
quote! {
match #arg {
Ok(v) => v,
Err(e) => return Box::pin(async { Err(e) }),
}
}
} else {
quote! { #arg? }
}
}
Self::Context(_) => quote! {
::juniper::FromContext::from(executor.context())
},
Self::Executor => quote! { &executor },
}
}
pub(crate) fn parse(
argument: &mut syn::PatType,
renaming: &rename::Policy,
scope: &diagnostic::Scope,
) -> Option<Self> {
let orig_attrs = argument.attrs.clone();
argument.attrs = mem::take(&mut argument.attrs)
.into_iter()
.filter(|attr| !path_eq_single(&attr.path, "graphql"))
.collect();
let attr = Attr::from_attrs("graphql", &orig_attrs)
.map_err(|e| proc_macro_error::emit_error!(e))
.ok()?;
if attr.context.is_some() {
return Some(Self::Context(Box::new(argument.ty.unreferenced().clone())));
}
if attr.executor.is_some() {
return Some(Self::Executor);
}
if let syn::Pat::Ident(name) = &*argument.pat {
let arg = match name.ident.unraw().to_string().as_str() {
"context" | "ctx" | "_context" | "_ctx" => {
Some(Self::Context(Box::new(argument.ty.unreferenced().clone())))
}
"executor" | "_executor" => Some(Self::Executor),
_ => None,
};
if arg.is_some() {
attr.ensure_no_regular_arguments()
.map_err(|e| scope.error(e).emit())
.ok()?;
return arg;
}
}
let name = if let Some(name) = attr.name.as_ref() {
name.as_ref().value()
} else if let syn::Pat::Ident(name) = &*argument.pat {
renaming.apply(&name.ident.unraw().to_string())
} else {
scope
.custom(
argument.pat.span(),
"method argument should be declared as a single identifier",
)
.note(String::from(
"use `#[graphql(name = ...)]` attribute to specify custom argument's \
name without requiring it being a single identifier",
))
.emit();
return None;
};
if name.starts_with("__") {
scope.no_double_underscore(
attr.name
.as_ref()
.map(SpanContainer::span_ident)
.unwrap_or_else(|| argument.pat.span()),
);
return None;
}
Some(Self::Regular(Box::new(OnField {
name,
ty: argument.ty.as_ref().clone(),
description: attr.description.map(SpanContainer::into_inner),
default: attr.default.map(SpanContainer::into_inner),
})))
}
}