Skip to content

Commit

Permalink
Support customizing proc macro args with attrs directly on the args
Browse files Browse the repository at this point in the history
  • Loading branch information
davidpdrsn committed Oct 21, 2019
1 parent 3c8cf55 commit 47c5484
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,56 +52,56 @@ static SCHEMA_INTROSPECTION_QUERY: &str = r#"
// TODO: Test for `rename` attr

#[test]
fn descriptions_applied_correctly() {
fn old_descriptions_applied_correctly() {
let schema = introspect_schema();

let query = schema.types.iter().find(|ty| ty.name == "Query").unwrap();

// old deprecated `#[graphql(arguments(...))]` style
{
let field = query
.fields
.iter()
.find(|field| field.name == "fieldOldAttrs")
.unwrap();

let arg1 = field.args.iter().find(|arg| arg.name == "arg1").unwrap();
assert_eq!(&arg1.description, &Some("arg1 desc".to_string()));
assert_eq!(
&arg1.default_value,
&Some(Value::String("true".to_string()))
);

let arg2 = field.args.iter().find(|arg| arg.name == "arg2").unwrap();
assert_eq!(&arg2.description, &Some("arg2 desc".to_string()));
assert_eq!(
&arg2.default_value,
&Some(Value::String("false".to_string()))
);
}
let field = query
.fields
.iter()
.find(|field| field.name == "fieldOldAttrs")
.unwrap();

let arg1 = field.args.iter().find(|arg| arg.name == "arg1").unwrap();
assert_eq!(&arg1.description, &Some("arg1 desc".to_string()));
assert_eq!(
&arg1.default_value,
&Some(Value::String("true".to_string()))
);

let arg2 = field.args.iter().find(|arg| arg.name == "arg2").unwrap();
assert_eq!(&arg2.description, &Some("arg2 desc".to_string()));
assert_eq!(
&arg2.default_value,
&Some(Value::String("false".to_string()))
);
}

// new style with attrs directly on the args
{
let field = query
.fields
.iter()
.find(|field| field.name == "fieldNewAttrs")
.unwrap();

let arg1 = field.args.iter().find(|arg| arg.name == "arg1").unwrap();
assert_eq!(&arg1.description, &Some("arg1 desc".to_string()));
assert_eq!(
&arg1.default_value,
&Some(Value::String("true".to_string()))
);

let arg2 = field.args.iter().find(|arg| arg.name == "arg2").unwrap();
assert_eq!(&arg2.description, &Some("arg2 desc".to_string()));
assert_eq!(
&arg2.default_value,
&Some(Value::String("false".to_string()))
);
}
#[test]
fn new_descriptions_applied_correctly() {
let schema = introspect_schema();
let query = schema.types.iter().find(|ty| ty.name == "Query").unwrap();

let field = query
.fields
.iter()
.find(|field| field.name == "fieldNewAttrs")
.unwrap();

let arg1 = field.args.iter().find(|arg| arg.name == "arg1").unwrap();
assert_eq!(&arg1.description, &Some("arg1 desc".to_string()));
assert_eq!(
&arg1.default_value,
&Some(Value::String("true".to_string()))
);

let arg2 = field.args.iter().find(|arg| arg.name == "arg2").unwrap();
assert_eq!(&arg2.description, &Some("arg2 desc".to_string()));
assert_eq!(
&arg2.default_value,
&Some(Value::String("false".to_string()))
);
}

#[derive(Debug)]
Expand Down
1 change: 1 addition & 0 deletions juniper_codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ proc-macro = true
proc-macro2 = "1.0.1"
syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] }
quote = "1.0.2"
proc-macro-error = "0.3.4"

[dev-dependencies]
juniper = { version = "0.14.0", path = "../juniper" }
Expand Down
62 changes: 61 additions & 1 deletion juniper_codegen/src/impl_object.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::util;
use proc_macro::TokenStream;
use proc_macro_error::*;
use quote::quote;
use syn::spanned::Spanned;

/// Generate code for the juniper::object macro.
pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream {
Expand Down Expand Up @@ -101,7 +103,7 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
}
};

let attrs = match util::FieldAttributes::from_attrs(
let mut attrs = match util::FieldAttributes::from_attrs(
method.attrs,
util::FieldAttributeParseMode::Impl,
) {
Expand All @@ -112,6 +114,16 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
),
};

if !attrs.arguments.is_empty() {
let deprecation_warning = vec![
"Setting arguments via #[graphql(arguments(...))] on the method",
"is deprecrated. Instead use #[graphql(...)] as attributes directly",
"on the arguments themselves.",
]
.join(" ");
eprintln!("{}", deprecation_warning);
}

let mut args = Vec::new();
let mut resolve_parts = Vec::new();

Expand All @@ -126,6 +138,12 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
}
}
syn::FnArg::Typed(ref captured) => {
if let Some(field_arg) = parse_argument_attrs(&captured) {
attrs
.arguments
.insert(field_arg.name.to_string(), field_arg);
}

let (arg_ident, is_mut) = match &*captured.pat {
syn::Pat::Ident(ref pat_ident) => {
(&pat_ident.ident, pat_ident.mutability.is_some())
Expand Down Expand Up @@ -224,3 +242,45 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_tokens(juniper_crate_name).into()
}

fn parse_argument_attrs(pat: &syn::PatType) -> Option<util::FieldAttributeArgument> {
let graphql_attrs = pat
.attrs
.iter()
.filter(|attr| {
let name = attr.path.get_ident().map(|i| i.to_string());
name == Some("graphql".to_string())
})
.collect::<Vec<_>>();

let graphql_attr = match graphql_attrs.len() {
0 => return None,
1 => &graphql_attrs[0],
_ => {
let last_attr = graphql_attrs.last().unwrap();
abort!(
last_attr.span(),
"You cannot have multiple #[graphql] attributes on the same arg"
);
}
};

let name = match &*pat.pat {
syn::Pat::Ident(i) => &i.ident,
other => unimplemented!("{:?}", other),
};

let mut arg = util::FieldAttributeArgument {
name: name.to_owned(),
default: None,
description: None,
};

graphql_attr
.parse_args_with(|content: syn::parse::ParseStream| {
util::parse_field_attr_arg_contents(&content, &mut arg)
})
.unwrap_or_else(|err| abort!(err.span(), "{}", err));

Some(arg)
}
36 changes: 17 additions & 19 deletions juniper_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod derive_scalar_value;
mod impl_object;
mod util;

use proc_macro_error::*;
use proc_macro::TokenStream;

#[proc_macro_derive(GraphQLEnum, attributes(graphql))]
Expand Down Expand Up @@ -289,25 +290,21 @@ impl InternalQuery {
fn deprecated_field_simple() -> bool { true }
// Customizing field arguments is a little awkward right now.
// This will improve once [RFC 2564](https://github.com/rust-lang/rust/issues/60406)
// is implemented, which will allow attributes on function parameters.
#[graphql(
arguments(
arg1(
// You can specify default values.
// A default can be any valid expression that yields the right type.
default = true,
description = "Argument description....",
),
arg2(
default = false,
description = "arg2 description...",
),
),
)]
fn args(arg1: bool, arg2: bool) -> bool {
// Customizing field arguments can be done like so:
// Note that attributes on arguments requires Rust 1.39
fn args(
#[graphql(
// You can specify default values.
// A default can be any valid expression that yields the right type.
default = true,
description = "Argument description....",
)] arg1: bool,
#[graphql(
default = false,
description = "arg2 description...",
)] arg2: bool,
) -> bool {
arg1 && arg2
}
}
Expand Down Expand Up @@ -354,6 +351,7 @@ impl Query {
*/
#[proc_macro_attribute]
#[proc_macro_error]
pub fn object(args: TokenStream, input: TokenStream) -> TokenStream {
let gen = impl_object::build_object(args, input, false);
gen.into()
Expand Down
47 changes: 28 additions & 19 deletions juniper_codegen/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ pub struct FieldAttributeArgument {

impl parse::Parse for FieldAttributeArgument {
fn parse(input: parse::ParseStream) -> parse::Result<Self> {
let name = input.parse()?;
let name = input.parse::<syn::Ident>()?;

let mut arg = Self {
name,
Expand All @@ -402,28 +402,37 @@ impl parse::Parse for FieldAttributeArgument {

let content;
syn::parenthesized!(content in input);
while !content.is_empty() {
let name = content.parse::<syn::Ident>()?;
content.parse::<Token![=]>()?;
parse_field_attr_arg_contents(&content, &mut arg)?;

match name.to_string().as_str() {
"description" => {
arg.description = Some(content.parse()?);
}
"default" => {
arg.default = Some(content.parse()?);
}
other => {
return Err(content.error(format!("Invalid attribute argument key {}", other)));
}
}
Ok(arg)
}
}

// Discard trailing comma.
content.parse::<Token![,]>().ok();
pub fn parse_field_attr_arg_contents(
content: syn::parse::ParseStream,
arg: &mut FieldAttributeArgument,
) -> parse::Result<()> {
while !content.is_empty() {
let name = content.parse::<syn::Ident>()?;
content.parse::<Token![=]>()?;

match name.to_string().as_str() {
"description" => {
arg.description = Some(content.parse()?);
}
"default" => {
arg.default = Some(content.parse()?);
}
other => {
return Err(content.error(format!("Invalid attribute argument key {}", other)));
}
}

Ok(arg)
// Discard trailing comma.
content.parse::<Token![,]>().ok();
}

Ok(())
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
Expand Down Expand Up @@ -493,7 +502,7 @@ impl parse::Parse for FieldAttribute {
}
}

#[derive(Default)]
#[derive(Default, Debug)]
pub struct FieldAttributes {
pub name: Option<String>,
pub description: Option<String>,
Expand Down

0 comments on commit 47c5484

Please sign in to comment.