Skip to content

Commit 5a3cccf

Browse files
committed
Support customizing proc macro args with attrs directly on the args
Fixes graphql-rust#421
1 parent 3c8cf55 commit 5a3cccf

File tree

5 files changed

+152
-84
lines changed

5 files changed

+152
-84
lines changed

integration_tests/juniper_tests/src/codegen/proc_macro_param_attrs.rs

+45-45
Original file line numberDiff line numberDiff line change
@@ -52,56 +52,56 @@ static SCHEMA_INTROSPECTION_QUERY: &str = r#"
5252
// TODO: Test for `rename` attr
5353

5454
#[test]
55-
fn descriptions_applied_correctly() {
55+
fn old_descriptions_applied_correctly() {
5656
let schema = introspect_schema();
57-
5857
let query = schema.types.iter().find(|ty| ty.name == "Query").unwrap();
5958

6059
// old deprecated `#[graphql(arguments(...))]` style
61-
{
62-
let field = query
63-
.fields
64-
.iter()
65-
.find(|field| field.name == "fieldOldAttrs")
66-
.unwrap();
67-
68-
let arg1 = field.args.iter().find(|arg| arg.name == "arg1").unwrap();
69-
assert_eq!(&arg1.description, &Some("arg1 desc".to_string()));
70-
assert_eq!(
71-
&arg1.default_value,
72-
&Some(Value::String("true".to_string()))
73-
);
74-
75-
let arg2 = field.args.iter().find(|arg| arg.name == "arg2").unwrap();
76-
assert_eq!(&arg2.description, &Some("arg2 desc".to_string()));
77-
assert_eq!(
78-
&arg2.default_value,
79-
&Some(Value::String("false".to_string()))
80-
);
81-
}
60+
let field = query
61+
.fields
62+
.iter()
63+
.find(|field| field.name == "fieldOldAttrs")
64+
.unwrap();
65+
66+
let arg1 = field.args.iter().find(|arg| arg.name == "arg1").unwrap();
67+
assert_eq!(&arg1.description, &Some("arg1 desc".to_string()));
68+
assert_eq!(
69+
&arg1.default_value,
70+
&Some(Value::String("true".to_string()))
71+
);
72+
73+
let arg2 = field.args.iter().find(|arg| arg.name == "arg2").unwrap();
74+
assert_eq!(&arg2.description, &Some("arg2 desc".to_string()));
75+
assert_eq!(
76+
&arg2.default_value,
77+
&Some(Value::String("false".to_string()))
78+
);
79+
}
8280

83-
// new style with attrs directly on the args
84-
{
85-
let field = query
86-
.fields
87-
.iter()
88-
.find(|field| field.name == "fieldNewAttrs")
89-
.unwrap();
90-
91-
let arg1 = field.args.iter().find(|arg| arg.name == "arg1").unwrap();
92-
assert_eq!(&arg1.description, &Some("arg1 desc".to_string()));
93-
assert_eq!(
94-
&arg1.default_value,
95-
&Some(Value::String("true".to_string()))
96-
);
97-
98-
let arg2 = field.args.iter().find(|arg| arg.name == "arg2").unwrap();
99-
assert_eq!(&arg2.description, &Some("arg2 desc".to_string()));
100-
assert_eq!(
101-
&arg2.default_value,
102-
&Some(Value::String("false".to_string()))
103-
);
104-
}
81+
#[test]
82+
fn new_descriptions_applied_correctly() {
83+
let schema = introspect_schema();
84+
let query = schema.types.iter().find(|ty| ty.name == "Query").unwrap();
85+
86+
let field = query
87+
.fields
88+
.iter()
89+
.find(|field| field.name == "fieldNewAttrs")
90+
.unwrap();
91+
92+
let arg1 = field.args.iter().find(|arg| arg.name == "arg1").unwrap();
93+
assert_eq!(&arg1.description, &Some("arg1 desc".to_string()));
94+
assert_eq!(
95+
&arg1.default_value,
96+
&Some(Value::String("true".to_string()))
97+
);
98+
99+
let arg2 = field.args.iter().find(|arg| arg.name == "arg2").unwrap();
100+
assert_eq!(&arg2.description, &Some("arg2 desc".to_string()));
101+
assert_eq!(
102+
&arg2.default_value,
103+
&Some(Value::String("false".to_string()))
104+
);
105105
}
106106

107107
#[derive(Debug)]

juniper_codegen/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ proc-macro = true
1818
proc-macro2 = "1.0.1"
1919
syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] }
2020
quote = "1.0.2"
21+
proc-macro-error = "0.3.4"
2122

2223
[dev-dependencies]
2324
juniper = { version = "0.14.0", path = "../juniper" }

juniper_codegen/src/impl_object.rs

+61-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::util;
22
use proc_macro::TokenStream;
3+
use proc_macro_error::*;
34
use quote::quote;
5+
use syn::spanned::Spanned;
46

57
/// Generate code for the juniper::object macro.
68
pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream {
@@ -101,7 +103,7 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
101103
}
102104
};
103105

104-
let attrs = match util::FieldAttributes::from_attrs(
106+
let mut attrs = match util::FieldAttributes::from_attrs(
105107
method.attrs,
106108
util::FieldAttributeParseMode::Impl,
107109
) {
@@ -112,6 +114,16 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
112114
),
113115
};
114116

117+
if !attrs.arguments.is_empty() {
118+
let deprecation_warning = vec![
119+
"Setting arguments via #[graphql(arguments(...))] on the method",
120+
"is deprecrated. Instead use #[graphql(...)] as attributes directly",
121+
"on the arguments themselves.",
122+
]
123+
.join(" ");
124+
eprintln!("{}", deprecation_warning);
125+
}
126+
115127
let mut args = Vec::new();
116128
let mut resolve_parts = Vec::new();
117129

@@ -126,6 +138,12 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
126138
}
127139
}
128140
syn::FnArg::Typed(ref captured) => {
141+
if let Some(field_arg) = parse_argument_attrs(&captured) {
142+
attrs
143+
.arguments
144+
.insert(field_arg.name.to_string(), field_arg);
145+
}
146+
129147
let (arg_ident, is_mut) = match &*captured.pat {
130148
syn::Pat::Ident(ref pat_ident) => {
131149
(&pat_ident.ident, pat_ident.mutability.is_some())
@@ -224,3 +242,45 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
224242
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
225243
definition.into_tokens(juniper_crate_name).into()
226244
}
245+
246+
fn parse_argument_attrs(pat: &syn::PatType) -> Option<util::FieldAttributeArgument> {
247+
let graphql_attrs = pat
248+
.attrs
249+
.iter()
250+
.filter(|attr| {
251+
let name = attr.path.get_ident().map(|i| i.to_string());
252+
name == Some("graphql".to_string())
253+
})
254+
.collect::<Vec<_>>();
255+
256+
let graphql_attr = match graphql_attrs.len() {
257+
0 => return None,
258+
1 => &graphql_attrs[0],
259+
_ => {
260+
let last_attr = graphql_attrs.last().unwrap();
261+
abort!(
262+
last_attr.span(),
263+
"You cannot have multiple #[graphql] attributes on the same arg"
264+
);
265+
}
266+
};
267+
268+
let name = match &*pat.pat {
269+
syn::Pat::Ident(i) => &i.ident,
270+
other => unimplemented!("{:?}", other),
271+
};
272+
273+
let mut arg = util::FieldAttributeArgument {
274+
name: name.to_owned(),
275+
default: None,
276+
description: None,
277+
};
278+
279+
graphql_attr
280+
.parse_args_with(|content: syn::parse::ParseStream| {
281+
util::parse_field_attr_arg_contents(&content, &mut arg)
282+
})
283+
.unwrap_or_else(|err| abort!(err.span(), "{}", err));
284+
285+
Some(arg)
286+
}

juniper_codegen/src/lib.rs

+17-19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod derive_scalar_value;
1616
mod impl_object;
1717
mod util;
1818

19+
use proc_macro_error::*;
1920
use proc_macro::TokenStream;
2021

2122
#[proc_macro_derive(GraphQLEnum, attributes(graphql))]
@@ -289,25 +290,21 @@ impl InternalQuery {
289290
fn deprecated_field_simple() -> bool { true }
290291
291292
292-
// Customizing field arguments is a little awkward right now.
293-
// This will improve once [RFC 2564](https://github.com/rust-lang/rust/issues/60406)
294-
// is implemented, which will allow attributes on function parameters.
295-
296-
#[graphql(
297-
arguments(
298-
arg1(
299-
// You can specify default values.
300-
// A default can be any valid expression that yields the right type.
301-
default = true,
302-
description = "Argument description....",
303-
),
304-
arg2(
305-
default = false,
306-
description = "arg2 description...",
307-
),
308-
),
309-
)]
310-
fn args(arg1: bool, arg2: bool) -> bool {
293+
// Customizing field arguments can be done like so:
294+
// Note that attributes on arguments requires Rust 1.39
295+
fn args(
296+
#[graphql(
297+
// You can specify default values.
298+
// A default can be any valid expression that yields the right type.
299+
default = true,
300+
description = "Argument description....",
301+
)] arg1: bool,
302+
303+
#[graphql(
304+
default = false,
305+
description = "arg2 description...",
306+
)] arg2: bool,
307+
) -> bool {
311308
arg1 && arg2
312309
}
313310
}
@@ -354,6 +351,7 @@ impl Query {
354351
355352
*/
356353
#[proc_macro_attribute]
354+
#[proc_macro_error]
357355
pub fn object(args: TokenStream, input: TokenStream) -> TokenStream {
358356
let gen = impl_object::build_object(args, input, false);
359357
gen.into()

juniper_codegen/src/util.rs

+28-19
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ pub struct FieldAttributeArgument {
392392

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

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

403403
let content;
404404
syn::parenthesized!(content in input);
405-
while !content.is_empty() {
406-
let name = content.parse::<syn::Ident>()?;
407-
content.parse::<Token![=]>()?;
405+
parse_field_attr_arg_contents(&content, &mut arg)?;
408406

409-
match name.to_string().as_str() {
410-
"description" => {
411-
arg.description = Some(content.parse()?);
412-
}
413-
"default" => {
414-
arg.default = Some(content.parse()?);
415-
}
416-
other => {
417-
return Err(content.error(format!("Invalid attribute argument key {}", other)));
418-
}
419-
}
407+
Ok(arg)
408+
}
409+
}
420410

421-
// Discard trailing comma.
422-
content.parse::<Token![,]>().ok();
411+
pub fn parse_field_attr_arg_contents(
412+
content: syn::parse::ParseStream,
413+
arg: &mut FieldAttributeArgument,
414+
) -> parse::Result<()> {
415+
while !content.is_empty() {
416+
let name = content.parse::<syn::Ident>()?;
417+
content.parse::<Token![=]>()?;
418+
419+
match name.to_string().as_str() {
420+
"description" => {
421+
arg.description = Some(content.parse()?);
422+
}
423+
"default" => {
424+
arg.default = Some(content.parse()?);
425+
}
426+
other => {
427+
return Err(content.error(format!("Invalid attribute argument key `{}`", other)));
428+
}
423429
}
424430

425-
Ok(arg)
431+
// Discard trailing comma.
432+
content.parse::<Token![,]>().ok();
426433
}
434+
435+
Ok(())
427436
}
428437

429438
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
@@ -493,7 +502,7 @@ impl parse::Parse for FieldAttribute {
493502
}
494503
}
495504

496-
#[derive(Default)]
505+
#[derive(Default, Debug)]
497506
pub struct FieldAttributes {
498507
pub name: Option<String>,
499508
pub description: Option<String>,

0 commit comments

Comments
 (0)