diff --git a/docs/book/content/types/objects/complex_fields.md b/docs/book/content/types/objects/complex_fields.md index 902b442ee..f72147183 100644 --- a/docs/book/content/types/objects/complex_fields.md +++ b/docs/book/content/types/objects/complex_fields.md @@ -127,21 +127,11 @@ struct Person {} #[juniper::object] impl Person { - #[graphql( - arguments( - arg1( - // Set a default value which will be injected if not present. - // The default can be any valid Rust expression, including a function call, etc. - default = true, - // Set a description. - description = "The first argument..." - ), - arg2( - default = 0, - ) - ) - )] - fn field1(&self, arg1: bool, arg2: i32) -> String { + fn field1( + &self, + #[graphql(default = true, description = "The first argument...")] arg1: bool, + #[graphql(default = 0)] arg2: i32, + ) -> String { format!("{} {}", arg1, arg2) } } diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 0020dceb8..07163b964 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -1,5 +1,33 @@ # master +### New way to customize arguments + +See [#441](https://github.com/graphql-rust/juniper/pull/441). + +You can now customize arguments by annotating them with `#[graphql(...)]` directly. Example: + +```rust +#[juniper::object] +impl Query { + fn some_field_with_a_description( + #[graphql( + name = newNameForArg, + description = "My argument description", + default = false, + )] + arg: bool + ) -> bool { + // ... + } +} +``` + +The old style `#[graphql(arguments(...))]` is no longer supported. + +Note that this requires Rust 1.39. + +### Other changes + - Correctly handle raw identifiers in field and argument names. # [[0.14.1] 2019-10-24](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.14.1) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 8a3191687..8c8d00e37 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -47,3 +47,4 @@ uuid = { version = "0.7", optional = true } [dev-dependencies] bencher = "0.1.2" serde_json = { version = "1.0.2" } +assert-json-diff = "1.0.1" diff --git a/juniper/src/executor_tests/introspection/mod.rs b/juniper/src/executor_tests/introspection/mod.rs index 00fa01406..d7accb100 100644 --- a/juniper/src/executor_tests/introspection/mod.rs +++ b/juniper/src/executor_tests/introspection/mod.rs @@ -63,13 +63,11 @@ impl Root { Sample::One } - #[graphql(arguments( - first(description = "The first number",), - second(description = "The second number", default = 123,), - ))] - /// A sample scalar field on the object - fn sample_scalar(first: i32, second: i32) -> Scalar { + fn sample_scalar( + #[graphql(description = "The first number")] first: i32, + #[graphql(description = "The second number", default = 123)] second: i32, + ) -> Scalar { Scalar(first + second) } } diff --git a/juniper/src/executor_tests/variables.rs b/juniper/src/executor_tests/variables.rs index 8024588a0..8689587e9 100644 --- a/juniper/src/executor_tests/variables.rs +++ b/juniper/src/executor_tests/variables.rs @@ -78,14 +78,9 @@ impl TestType { format!("{:?}", input) } - #[graphql( - arguments( - input( - default = "Hello World".to_string(), - ) - ) - )] - fn field_with_default_argument_value(input: String) -> String { + fn field_with_default_argument_value( + #[graphql(default = "Hello World".to_string())] input: String, + ) -> String { format!("{:?}", input) } @@ -158,7 +153,10 @@ fn inline_complex_input() { |result: &Object| { assert_eq!( result.get_field_value("fieldWithObjectInput"), - Some(&Value::scalar(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#))); + Some(&Value::scalar( + r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"# + )) + ); }, ); } @@ -170,7 +168,10 @@ fn inline_parse_single_value_to_list() { |result: &Object| { assert_eq!( result.get_field_value("fieldWithObjectInput"), - Some(&Value::scalar(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#))); + Some(&Value::scalar( + r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"# + )) + ); }, ); } @@ -182,7 +183,10 @@ fn inline_runs_from_input_value_on_scalar() { |result: &Object| { assert_eq!( result.get_field_value("fieldWithObjectInput"), - Some(&Value::scalar(r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"#))); + Some(&Value::scalar( + r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"# + )) + ); }, ); } @@ -208,7 +212,10 @@ fn variable_complex_input() { |result: &Object| { assert_eq!( result.get_field_value("fieldWithObjectInput"), - Some(&Value::scalar(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#))); + Some(&Value::scalar( + r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"# + )) + ); }, ); } @@ -234,7 +241,10 @@ fn variable_parse_single_value_to_list() { |result: &Object| { assert_eq!( result.get_field_value("fieldWithObjectInput"), - Some(&Value::scalar(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#))); + Some(&Value::scalar( + r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"# + )) + ); }, ); } @@ -259,7 +269,10 @@ fn variable_runs_from_input_value_on_scalar() { |result: &Object| { assert_eq!( result.get_field_value("fieldWithObjectInput"), - Some(&Value::scalar(r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"#))); + Some(&Value::scalar( + r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"# + )) + ); }, ); } @@ -306,12 +319,13 @@ fn variable_error_on_incorrect_type() { let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err(); - assert_eq!(error, ValidationError(vec![ - RuleError::new( + assert_eq!( + error, + ValidationError(vec![RuleError::new( r#"Variable "$input" got invalid value. Expected "TestInputObject", found not an object."#, &[SourcePosition::new(8, 0, 8)], - ), - ])); + ),]) + ); } #[test] @@ -366,16 +380,19 @@ fn variable_multiple_errors_with_nesting() { let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err(); - assert_eq!(error, ValidationError(vec![ - RuleError::new( - r#"Variable "$input" got invalid value. In field "na": In field "c": Expected "String!", found null."#, - &[SourcePosition::new(8, 0, 8)], - ), - RuleError::new( - r#"Variable "$input" got invalid value. In field "nb": Expected "String!", found null."#, - &[SourcePosition::new(8, 0, 8)], - ), - ])); + assert_eq!( + error, + ValidationError(vec![ + RuleError::new( + r#"Variable "$input" got invalid value. In field "na": In field "c": Expected "String!", found null."#, + &[SourcePosition::new(8, 0, 8)], + ), + RuleError::new( + r#"Variable "$input" got invalid value. In field "nb": Expected "String!", found null."#, + &[SourcePosition::new(8, 0, 8)], + ), + ]) + ); } #[test] @@ -733,12 +750,13 @@ fn does_not_allow_lists_of_non_null_to_contain_null() { let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err(); - assert_eq!(error, ValidationError(vec![ - RuleError::new( + assert_eq!( + error, + ValidationError(vec![RuleError::new( r#"Variable "$input" got invalid value. In element #1: Expected "String!", found null."#, &[SourcePosition::new(8, 0, 8)], - ), - ])); + ),]) + ); } #[test] @@ -759,12 +777,13 @@ fn does_not_allow_non_null_lists_of_non_null_to_contain_null() { let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err(); - assert_eq!(error, ValidationError(vec![ - RuleError::new( + assert_eq!( + error, + ValidationError(vec![RuleError::new( r#"Variable "$input" got invalid value. In element #1: Expected "String!", found null."#, &[SourcePosition::new(8, 0, 8)], - ), - ])); + ),]) + ); } #[test] @@ -820,12 +839,13 @@ fn does_not_allow_invalid_types_to_be_used_as_values() { let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err(); - assert_eq!(error, ValidationError(vec![ - RuleError::new( + assert_eq!( + error, + ValidationError(vec![RuleError::new( r#"Variable "$input" expected value of type "TestType!" which cannot be used as an input type."#, &[SourcePosition::new(8, 0, 8)], - ), - ])); + ),]) + ); } #[test] @@ -842,12 +862,13 @@ fn does_not_allow_unknown_types_to_be_used_as_values() { let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err(); - assert_eq!(error, ValidationError(vec![ - RuleError::new( + assert_eq!( + error, + ValidationError(vec![RuleError::new( r#"Variable "$input" expected value of type "UnknownType!" which cannot be used as an input type."#, &[SourcePosition::new(8, 0, 8)], - ), - ])); + ),]) + ); } #[test] diff --git a/juniper/src/http/graphiql.rs b/juniper/src/http/graphiql.rs index 590909cb2..7b9e1ebe3 100644 --- a/juniper/src/http/graphiql.rs +++ b/juniper/src/http/graphiql.rs @@ -41,7 +41,8 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String { "#; - format!(r#" + format!( + r#" @@ -62,5 +63,6 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String { "#, graphql_url = graphql_endpoint_url, stylesheet_source = stylesheet_source, - fetcher_source = fetcher_source) + fetcher_source = fetcher_source + ) } diff --git a/juniper/src/integrations/serde.rs b/juniper/src/integrations/serde.rs index c91c52629..97b465096 100644 --- a/juniper/src/integrations/serde.rs +++ b/juniper/src/integrations/serde.rs @@ -450,7 +450,8 @@ mod tests { to_string(&ExecutionError::at_origin(FieldError::new( "foo error", Value::Object(obj), - ))).unwrap(), + ))) + .unwrap(), r#"{"message":"foo error","locations":[{"line":1,"column":1}],"path":[],"extensions":{"foo":"bar"}}"# ); } diff --git a/juniper/src/macros/tests/args.rs b/juniper/src/macros/tests/args.rs index 99915094f..3286b5bd2 100644 --- a/juniper/src/macros/tests/args.rs +++ b/juniper/src/macros/tests/args.rs @@ -52,84 +52,73 @@ impl Root { 0 } - #[graphql(arguments(arg(description = "The arg")))] - fn single_arg_descr(arg: i32) -> i32 { + fn single_arg_descr(#[graphql(description = "The arg")] arg: i32) -> i32 { 0 } - #[graphql(arguments( - arg1(description = "The first arg",), - arg2(description = "The second arg") - ))] - fn multi_args_descr(arg1: i32, arg2: i32) -> i32 { + fn multi_args_descr( + #[graphql(description = "The first arg")] arg1: i32, + #[graphql(description = "The second arg")] arg2: i32, + ) -> i32 { 0 } - #[graphql(arguments( - arg1(description = "The first arg",), - arg2(description = "The second arg") - ))] - fn multi_args_descr_trailing_comma(arg1: i32, arg2: i32) -> i32 { + fn multi_args_descr_trailing_comma( + #[graphql(description = "The first arg")] arg1: i32, + #[graphql(description = "The second arg")] arg2: i32, + ) -> i32 { 0 } - // TODO: enable once [RFC 2565](https://github.com/rust-lang/rust/issues/60406) is implemented - // fn attr_arg_descr(#[doc = "The arg"] arg: i32) -> i32 { 0 } - // fn attr_arg_descr_collapse( - // #[doc = "The arg"] - // #[doc = "and more details"] - // arg: i32, - // ) -> i32 { 0 } - - #[graphql(arguments(arg(default = 123,),))] - fn arg_with_default(arg: i32) -> i32 { + fn arg_with_default(#[graphql(default = 123)] arg: i32) -> i32 { 0 } - #[graphql(arguments(arg1(default = 123,), arg2(default = 456,)))] - fn multi_args_with_default(arg1: i32, arg2: i32) -> i32 { + fn multi_args_with_default( + #[graphql(default = 123)] arg1: i32, + #[graphql(default = 456)] arg2: i32, + ) -> i32 { 0 } - #[graphql(arguments(arg1(default = 123,), arg2(default = 456,),))] - fn multi_args_with_default_trailing_comma(arg1: i32, arg2: i32) -> i32 { + fn multi_args_with_default_trailing_comma( + #[graphql(default = 123)] arg1: i32, + #[graphql(default = 456)] arg2: i32, + ) -> i32 { 0 } - #[graphql(arguments(arg(default = 123, description = "The arg")))] - fn arg_with_default_descr(arg: i32) -> i32 { + fn arg_with_default_descr(#[graphql(default = 123, description = "The arg")] arg: i32) -> i32 { 0 } - #[graphql(arguments( - arg1(default = 123, description = "The first arg"), - arg2(default = 456, description = "The second arg") - ))] - fn multi_args_with_default_descr(arg1: i32, arg2: i32) -> i32 { + fn multi_args_with_default_descr( + #[graphql(default = 123, description = "The first arg")] arg1: i32, + #[graphql(default = 456, description = "The second arg")] arg2: i32, + ) -> i32 { 0 } - #[graphql(arguments( - arg1(default = 123, description = "The first arg",), - arg2(default = 456, description = "The second arg",) - ))] - fn multi_args_with_default_trailing_comma_descr(arg1: i32, arg2: i32) -> i32 { + fn multi_args_with_default_trailing_comma_descr( + #[graphql(default = 123, description = "The first arg")] arg1: i32, + #[graphql(default = 456, description = "The second arg")] arg2: i32, + ) -> i32 { 0 } - #[graphql( - arguments( - arg1( - default = "test".to_string(), - description = "A string default argument", - ), - arg2( - default = Point{ x: 1 }, - description = "An input object default argument", - ) - ), - )] - fn args_with_complex_default(arg1: String, arg2: Point) -> i32 { + fn args_with_complex_default( + #[graphql( + default = "test".to_string(), + description = "A string default argument", + )] + arg1: String, + + #[graphql( + default = Point{ x: 1 }, + description = "An input object default argument", + )] + arg2: Point, + ) -> i32 { 0 } } diff --git a/juniper/src/macros/tests/impl_object.rs b/juniper/src/macros/tests/impl_object.rs index bbcfdb18b..b0c12aba7 100644 --- a/juniper/src/macros/tests/impl_object.rs +++ b/juniper/src/macros/tests/impl_object.rs @@ -35,7 +35,7 @@ struct Query { #[crate::object_internal( scalar = crate::DefaultScalarValue, - name = "Query", + name = "Query", context = Context, )] /// Query Description. @@ -84,16 +84,18 @@ impl<'a> Query { arg1 } - #[graphql(arguments(default_arg(default = true)))] - fn default_argument(default_arg: bool) -> bool { + fn default_argument(#[graphql(default = true)] default_arg: bool) -> bool { default_arg } - #[graphql(arguments(arg(description = "my argument description")))] - fn arg_with_description(arg: bool) -> bool { + fn arg_with_description(#[graphql(description = "my argument description")] arg: bool) -> bool { arg } + fn renamed_argument(#[graphql(name = new_name)] old_name: bool) -> bool { + old_name + } + fn with_context_child(&self) -> WithContext { WithContext } @@ -120,12 +122,19 @@ impl Mutation { } } +fn juniper_value_to_serde_json_value( + value: &crate::Value, +) -> serde_json::Value { + serde_json::from_str(&serde_json::to_string(value).unwrap()).unwrap() +} + #[test] fn object_introspect() { let res = util::run_info_query::("Query"); - assert_eq!( - res, - crate::graphql_value!({ + + assert_json_diff::assert_json_include!( + actual: juniper_value_to_serde_json_value(&res), + expected: serde_json::json!({ "name": "Query", "description": "Query Description.", "fields": [ @@ -136,32 +145,32 @@ fn object_introspect() { }, { "name": "independent", - "description": None, + "description": None::, "args": [], }, { "name": "withExecutor", - "description": None, + "description": None::, "args": [], }, { "name": "withExecutorAndSelf", - "description": None, + "description": None::, "args": [], }, { "name": "withContext", - "description": None, + "description": None::, "args": [], }, { "name": "withContextAndSelf", - "description": None, + "description": None::, "args": [], }, { "name": "renamed", - "description": None, + "description": None::, "args": [], }, { @@ -176,24 +185,24 @@ fn object_introspect() { }, { "name": "hasArgument", - "description": None, + "description": None::, "args": [ { "name": "arg1", - "description": None, + "description": None::, "type": { - "name": None, + "name": None::, }, } ], }, { "name": "defaultArgument", - "description": None, + "description": None::, "args": [ { "name": "defaultArg", - "description": None, + "description": None::, "type": { "name": "Boolean", }, @@ -202,36 +211,49 @@ fn object_introspect() { }, { "name": "argWithDescription", - "description": None, + "description": None::, "args": [ { "name": "arg", "description": "my argument description", "type": { - "name": None + "name": None:: + }, + } + ], + }, + { + "name": "renamedArgument", + "description": None::, + "args": [ + { + "name": "newName", + "description": None::, + "type": { + "name": None:: }, } ], }, { "name": "withContextChild", - "description": None, + "description": None::, "args": [], }, { "name": "withLifetimeChild", - "description": None, + "description": None::, "args": [], }, { "name": "withMutArg", - "description": None, + "description": None::, "args": [ { "name": "arg", - "description": None, + "description": None::, "type": { - "name": None, + "name": None::, }, } ], diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index a273fa7ec..72dc93fe0 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -144,8 +144,10 @@ where } } - #[graphql(arguments(include_deprecated(default = false)))] - fn fields(&self, include_deprecated: bool) -> Option>> { + fn fields( + &self, + #[graphql(default = false)] include_deprecated: bool, + ) -> Option>> { match *self { TypeType::Concrete(&MetaType::Interface(InterfaceMeta { ref fields, .. })) | TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) => Some( @@ -230,8 +232,10 @@ where } } - #[graphql(arguments(include_deprecated(default = false)))] - fn enum_values(&self, include_deprecated: bool) -> Option> { + fn enum_values( + &self, + #[graphql(default = false)] include_deprecated: bool, + ) -> Option> { match *self { TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) => Some( values diff --git a/juniper/src/tests/schema.rs b/juniper/src/tests/schema.rs index 37a7a4fb8..731369821 100644 --- a/juniper/src/tests/schema.rs +++ b/juniper/src/tests/schema.rs @@ -107,20 +107,27 @@ pub struct Query; )] /// The root query object of the schema impl Query { - #[graphql(arguments(id(description = "id of the human")))] - fn human(database: &Database, id: String) -> Option<&dyn Human> { + fn human( + database: &Database, + #[graphql(description = "id of the human")] id: String, + ) -> Option<&dyn Human> { database.get_human(&id) } - #[graphql(arguments(id(description = "id of the droid")))] - fn droid(database: &Database, id: String) -> Option<&dyn Droid> { + fn droid( + database: &Database, + #[graphql(description = "id of the droid")] id: String, + ) -> Option<&dyn Droid> { database.get_droid(&id) } - #[graphql(arguments(episode( - description = "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode" - )))] - fn hero(database: &Database, episode: Option) -> Option<&dyn Character> { + fn hero( + database: &Database, + #[graphql( + description = "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode" + )] + episode: Option, + ) -> Option<&dyn Character> { Some(database.get_hero(episode).as_character()) } } diff --git a/juniper/src/validation/rules/scalar_leafs.rs b/juniper/src/validation/rules/scalar_leafs.rs index 55f6a47fb..fe5ac7778 100644 --- a/juniper/src/validation/rules/scalar_leafs.rs +++ b/juniper/src/validation/rules/scalar_leafs.rs @@ -52,7 +52,8 @@ fn no_allowed_error_message(field_name: &str, type_name: &str) -> String { fn required_error_message(field_name: &str, type_name: &str) -> String { format!( r#"Field "{}" of type "{}" must have a selection of subfields. Did you mean "{} {{ ... }}"?"#, - field_name, type_name, field_name) + field_name, type_name, field_name + ) } #[cfg(test)] diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index a841e66f4..88a861e91 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -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.1", path = "../juniper" } diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs index 12af7f414..839bd830a 100644 --- a/juniper_codegen/src/impl_object.rs +++ b/juniper_codegen/src/impl_object.rs @@ -1,5 +1,6 @@ use crate::util; use proc_macro::TokenStream; +use proc_macro_error::*; use quote::quote; /// Generate code for the juniper::object macro. @@ -101,14 +102,16 @@ 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, ) { Ok(attrs) => attrs, - Err(err) => panic!( + Err(err) => abort!( + err.span(), "Invalid #[graphql(...)] attribute on field {}:\n{}", - method.sig.ident, err + method.sig.ident, + err ), }; @@ -134,7 +137,14 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) -> panic!("Invalid token for function argument"); } }; - let arg_name = arg_ident.to_string(); + let arg_ident_name = arg_ident.to_string(); + + if let Some(field_arg) = util::parse_argument_attrs(&captured) { + // We insert with `arg_ident_name` as the key because the argument + // might have been renamed in the param attribute and we need to + // look it up for making `final_name` further down. + attrs.arguments.insert(arg_ident_name.clone(), field_arg); + } let context_type = definition.context.as_ref(); @@ -169,8 +179,13 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) -> // Regular argument. let ty = &captured.ty; - // TODO: respect graphql attribute overwrite. - let final_name = util::to_camel_case(&arg_name); + + let final_name = attrs + .argument(&arg_ident_name) + .and_then(|arg| arg.name.as_ref()) + .map(|name| util::to_camel_case(&name.to_string())) + .unwrap_or_else(|| util::to_camel_case(&arg_ident_name)); + let expect_text = format!("Internal error: missing argument {} - validation must have failed", &final_name); let mut_modifier = if is_mut { quote!(mut) } else { quote!() }; resolve_parts.push(quote!( @@ -179,11 +194,11 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) -> .expect(#expect_text); )); args.push(util::GraphQLTypeDefinitionFieldArg { - description: attrs.argument(&arg_name).and_then(|arg| { + description: attrs.argument(&arg_ident_name).and_then(|arg| { arg.description.as_ref().map(|d| d.value()) }), default: attrs - .argument(&arg_name) + .argument(&arg_ident_name) .and_then(|arg| arg.default.clone()), _type: ty.clone(), name: final_name, diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 471a8adc6..6f07a10bd 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -17,6 +17,7 @@ mod impl_object; mod util; use proc_macro::TokenStream; +use proc_macro_error::*; #[proc_macro_derive(GraphQLEnum, attributes(graphql))] pub fn derive_enum(input: TokenStream) -> TokenStream { @@ -289,25 +290,24 @@ 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. + // Customizing field arguments can be done like so: + fn args( + #[graphql( + description = "Argument description....", - #[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 { + // You can specify default values. + // A default can be any valid expression that yields the right type. + default = true, + + // You can also give the argument a different name in your GraphQL schema + name = newName + )] arg1: bool, + + #[graphql( + description = "arg2 description...", + default = false, + )] arg2: bool, + ) -> bool { arg1 && arg2 } } @@ -372,6 +372,7 @@ impl User { */ #[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() @@ -380,6 +381,7 @@ pub fn object(args: TokenStream, input: TokenStream) -> TokenStream { /// A proc macro for defining a GraphQL object. #[doc(hidden)] #[proc_macro_attribute] +#[proc_macro_error] pub fn object_internal(args: TokenStream, input: TokenStream) -> TokenStream { let gen = impl_object::build_object(args, input, true); gen.into() diff --git a/juniper_codegen/src/util.rs b/juniper_codegen/src/util.rs index 4df1b2955..f6046dcfd 100644 --- a/juniper_codegen/src/util.rs +++ b/juniper_codegen/src/util.rs @@ -1,8 +1,9 @@ +use proc_macro_error::abort; use quote::quote; use std::collections::HashMap; use syn::{ - parse, parse_quote, punctuated::Punctuated, Attribute, Lit, Meta, MetaList, MetaNameValue, - NestedMeta, Token, + parse, parse_quote, punctuated::Punctuated, spanned::Spanned, Attribute, Lit, Meta, MetaList, + MetaNameValue, NestedMeta, Token, }; /// Compares a path to a one-segment string value, @@ -385,45 +386,81 @@ impl ObjectAttributes { #[derive(Debug)] pub struct FieldAttributeArgument { - pub name: syn::Ident, + pub name: Option, pub default: Option, pub description: Option, } -impl parse::Parse for FieldAttributeArgument { - fn parse(input: parse::ParseStream) -> parse::Result { - let name = input.parse()?; +pub fn parse_argument_attrs(pat: &syn::PatType) -> Option { + 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::>(); - let mut arg = Self { - name, - default: None, - description: None, - }; + 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 content; - syn::parenthesized!(content in input); - while !content.is_empty() { - let name = content.parse::()?; - content.parse::()?; + let name = match &*pat.pat { + syn::Pat::Ident(i) => &i.ident, + other => abort!(other.span(), "Invalid token for function argument"), + }; - 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))); - } - } + let mut arg = FieldAttributeArgument { + name: None, + default: None, + description: None, + }; + + graphql_attr + .parse_args_with(|content: syn::parse::ParseStream| { + parse_field_attr_arg_contents(&content, &mut arg) + }) + .unwrap_or_else(|err| abort!(err.span(), "{}", err)); + + Some(arg) +} + +fn parse_field_attr_arg_contents( + content: syn::parse::ParseStream, + arg: &mut FieldAttributeArgument, +) -> parse::Result<()> { + while !content.is_empty() { + let name = content.parse::()?; + content.parse::()?; - // Discard trailing comma. - content.parse::().ok(); + match name.to_string().as_str() { + "description" => { + arg.description = Some(content.parse()?); + } + "default" => { + arg.default = Some(content.parse()?); + } + "name" => { + arg.name = content.parse()?; + } + other => { + return Err(content.error(format!("Invalid attribute argument key `{}`", other))); + } } - Ok(arg) + // Discard trailing comma. + content.parse::().ok(); } + + Ok(()) } #[derive(PartialEq, Eq, Clone, Copy, Debug)] @@ -437,7 +474,6 @@ enum FieldAttribute { Description(syn::LitStr), Deprecation(DeprecationAttr), Skip(syn::Ident), - Arguments(HashMap), } impl parse::Parse for FieldAttribute { @@ -476,24 +512,12 @@ impl parse::Parse for FieldAttribute { })) } "skip" => Ok(FieldAttribute::Skip(ident)), - "arguments" => { - let arg_content; - syn::parenthesized!(arg_content in input); - let args = Punctuated::::parse_terminated( - &arg_content, - )?; - let map = args - .into_iter() - .map(|arg| (arg.name.to_string(), arg)) - .collect(); - Ok(FieldAttribute::Arguments(map)) - } other => Err(input.error(format!("Unknown attribute: {}", other))), } } } -#[derive(Default)] +#[derive(Default, Debug)] pub struct FieldAttributes { pub name: Option, pub description: Option, @@ -513,6 +537,8 @@ impl parse::Parse for FieldAttributes { description: None, deprecation: None, skip: false, + // The arguments get set later via attrs on the argument items themselves in + // `parse_argument_attrs` arguments: Default::default(), }; @@ -530,9 +556,6 @@ impl parse::Parse for FieldAttributes { FieldAttribute::Skip(_) => { output.skip = true; } - FieldAttribute::Arguments(args) => { - output.arguments = args; - } } }