From 0f2a65447194f353b3b72ec1e27b82f352dec241 Mon Sep 17 00:00:00 2001 From: Kevin Stenerson <2653498+kestred@users.noreply.github.com> Date: Sat, 27 Oct 2018 21:28:48 -0600 Subject: [PATCH] Infer graphql "deprecation" from #[deprecated(note = "...")] in derive (and macros) (#269) * Update object/iface macro with doc/deprecated attrs for fields * Use the note from `#[deprecated]` by default in derived GraphQLType * Update to support multiline raw-docstring format * Support bare deprecated attribute * Update arguments to support #[doc] for parity with previous ` as ` syntax --- changelog/master.md | 70 ++++ juniper/src/executor/mod.rs | 8 +- juniper/src/macros/common.rs | 87 ++++- juniper/src/macros/interface.rs | 8 +- juniper/src/macros/object.rs | 72 ++++- juniper/src/macros/tests/args.rs | 73 +++++ juniper/src/macros/tests/field.rs | 302 ++++++++++++++++++ juniper/src/schema/meta.rs | 117 ++++++- juniper/src/schema/schema.rs | 16 +- juniper_codegen/src/derive_enum.rs | 37 ++- juniper_codegen/src/derive_object.rs | 23 +- juniper_codegen/src/util.rs | 38 ++- .../src/codegen/derive_input_object.rs | 2 +- 13 files changed, 804 insertions(+), 49 deletions(-) diff --git a/changelog/master.md b/changelog/master.md index ef5b81ba..45017053 100644 --- a/changelog/master.md +++ b/changelog/master.md @@ -14,3 +14,73 @@ generic code. To retain the current behaviour use `DefaultScalarValue` as scalar value type [#251](https://github.com/graphql-rust/juniper/pull/251) + +- The `GraphQLObject` and `GraphQLEnum` derives will mark graphql fields as + `@deprecated` when struct fields or enum variants are marked with the + builtin `#[deprecated]` attribute. + + The deprecation reason can be set using the `note = ...` meta item + (e.g. `#[deprecated(note = "Replaced by betterField")]`). + The `since` attribute is ignored. + + [#269](https://github.com/graphql-rust/juniper/pull/269) + + +- There is an alternative syntax for setting a field's _description_ and + _deprecation reason_ in the `graphql_object!` and `graphql_interface!` macros. + + To __deprecate__ a graphql field: + ```rust + // Original syntax for setting deprecation reason + field deprecated "Reason" my_field() -> { ... } + + // New alternative syntax for deprecation reason. + #[deprecated(note = "Reason")] + field my_field() -> { ... } + + // You can now also deprecate without a reason. + #[deprecated] + field my_field() -> { ... } + ``` + + To set the __description__ of a graphql field: + ```rust + // Original syntax for field descriptions + field my_field() as "Description" -> { ... } + + // Original syntax for argument descriptions + field my_field( + floops: i32 as "The number of starfish to be returned. \ + Can't be more than 100.", + ) -> { + ... + } + + // New alternative syntax for field descriptions + #[doc = "Description"] + field my_field() -> { ... } + + // New alternative syntax for argument descriptions + field my_field( + #[doc = "The number of starfish to be returned. \ + Can't be more than 100."] + arg: i32, + ) -> { + ... + } + + // You can also use raw strings and const &'static str. + // + // Multiple docstrings will be collapsed into a single + // description separated by newlines. + #[doc = r#" + This is my field. + + Make sure not to flitz the bitlet. + Flitzing without a bitlet has undefined behaviour. + "] + #[doc = my_consts::ADDED_IN_VERSION_XYZ] + field my_field() -> { ... } + ``` + + [#269](https://github.com/graphql-rust/juniper/pull/269) diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 5dda3136..e06e73f7 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -15,8 +15,8 @@ use value::Value; use GraphQLError; use schema::meta::{ - Argument, EnumMeta, EnumValue, Field, InputObjectMeta, InterfaceMeta, ListMeta, MetaType, - NullableMeta, ObjectMeta, PlaceholderMeta, ScalarMeta, UnionMeta, + Argument, DeprecationStatus, EnumMeta, EnumValue, Field, InputObjectMeta, InterfaceMeta, + ListMeta, MetaType, NullableMeta, ObjectMeta, PlaceholderMeta, ScalarMeta, UnionMeta, }; use schema::model::{RootNode, SchemaType, TypeType}; @@ -729,7 +729,7 @@ where description: None, arguments: None, field_type: self.get_type::(info), - deprecation_reason: None, + deprecation_status: DeprecationStatus::Current, } } @@ -748,7 +748,7 @@ where description: None, arguments: None, field_type: self.get_type::(info), - deprecation_reason: None, + deprecation_status: DeprecationStatus::Current, } } diff --git a/juniper/src/macros/common.rs b/juniper/src/macros/common.rs index b37da3fa..f8c27e0e 100644 --- a/juniper/src/macros/common.rs +++ b/juniper/src/macros/common.rs @@ -398,7 +398,76 @@ macro_rules! __juniper_parse_field_list { ); }; - + ( + success_callback = $success_callback: ident, + additional_parser = {$($additional:tt)*}, + meta = {$($meta:tt)*}, + items = [$({$($items: tt)*},)*], + rest = $(#[doc = $desc: tt])* + #[deprecated $(( $(since = $since: tt,)* note = $reason: tt ))* ] + field $name: ident ( + $(&$executor: tt)* $(,)* + $($(#[doc = $arg_desc: expr])* $arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty),* $(,)* + ) -> $return_ty: ty $body: block + $($rest:tt)* + ) => { + __juniper_parse_field_list!( + success_callback = $success_callback, + additional_parser = {$($additional)*}, + meta = {$($meta)*}, + items = [$({$($items)*},)* { + name = $name, + body = $body, + return_ty = $return_ty, + args = [ + $({ + arg_name = $arg_name, + arg_ty = $arg_ty, + $(arg_default = $arg_default,)* + $(arg_docstring = $arg_desc,)* + },)* + ], + $(docstring = $desc,)* + deprecated = None$(.unwrap_or(Some($reason)))*, + $(executor_var = $executor,)* + },], + rest = $($rest)* + ); + }; + ( + success_callback = $success_callback: ident, + additional_parser = {$($additional:tt)*}, + meta = {$($meta:tt)*}, + items = [$({$($items: tt)*},)*], + rest = $(#[doc = $desc: tt])* + field $name: ident ( + $(&$executor: ident)* $(,)* + $($(#[doc = $arg_desc: expr])* $arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty),* $(,)* + ) -> $return_ty: ty $body: block + $($rest:tt)* + ) => { + __juniper_parse_field_list!( + success_callback = $success_callback, + additional_parser = {$($additional)*}, + meta = {$($meta)*}, + items = [$({$($items)*},)* { + name = $name, + body = $body, + return_ty = $return_ty, + args = [ + $({ + arg_name = $arg_name, + arg_ty = $arg_ty, + $(arg_default = $arg_default,)* + $(arg_docstring = $arg_desc,)* + },)* + ], + $(docstring = $desc,)* + $(executor_var = $executor,)* + },], + rest = $($rest)* + ); + }; ( success_callback = $success_callback: ident, additional_parser = {$($additional:tt)*}, @@ -406,7 +475,7 @@ macro_rules! __juniper_parse_field_list { items = [$({$($items: tt)*},)*], rest = field deprecated $reason:tt $name: ident ( $(&$executor: tt)* $(,)* - $($arg_name:ident $(= $default_value: tt)* : $arg_ty: ty $(as $arg_des: expr)*),* $(,)* + $($arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty $(as $arg_desc: expr)*),* $(,)* ) -> $return_ty: ty $(as $desc: tt)* $body: block $($rest:tt)* ) => { @@ -422,12 +491,12 @@ macro_rules! __juniper_parse_field_list { $({ arg_name = $arg_name, arg_ty = $arg_ty, + $(arg_default = $arg_default,)* $(arg_description = $arg_desc,)* - $(arg_default = $default_value,)* },)* ], $(decs = $desc,)* - deprecated = $reason, + deprecated = Some($reason), $(executor_var = $executor,)* },], rest = $($rest)* @@ -440,7 +509,7 @@ macro_rules! __juniper_parse_field_list { items = [$({$($items: tt)*},)*], rest = field $name: ident ( $(&$executor: ident)* $(,)* - $($arg_name:ident $(= $default_value: tt)* : $arg_ty: ty $(as $arg_desc: expr)*),* $(,)* + $($arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty $(as $arg_desc: expr)*),* $(,)* ) -> $return_ty: ty $(as $desc: tt)* $body: block $($rest:tt)* ) => { @@ -456,8 +525,8 @@ macro_rules! __juniper_parse_field_list { $({ arg_name = $arg_name, arg_ty = $arg_ty, + $(arg_default = $arg_default,)* $(arg_description = $arg_desc,)* - $(arg_default = $default_value,)* },)* ], $(decs = $desc,)* @@ -601,12 +670,14 @@ macro_rules! __juniper_create_arg { arg_ty = $arg_ty: ty, arg_name = $arg_name: ident, $(description = $arg_description: expr,)* + $(docstring = $arg_docstring: expr,)* ) => { $reg.arg::<$arg_ty>( &$crate::to_camel_case(stringify!($arg_name)), $info, ) $(.description($arg_description))* + $(.push_docstring($arg_docstring))* }; ( @@ -614,8 +685,9 @@ macro_rules! __juniper_create_arg { info = $info: ident, arg_ty = $arg_ty: ty, arg_name = $arg_name: ident, - $(description = $arg_description: expr,)* default = $arg_default: expr, + $(description = $arg_description: expr,)* + $(docstring = $arg_docstring: expr,)* ) => { $reg.arg_with_default::<$arg_ty>( &$crate::to_camel_case(stringify!($arg_name)), @@ -623,5 +695,6 @@ macro_rules! __juniper_create_arg { $info, ) $(.description($arg_description))* + $(.push_docstring($arg_docstring))* }; } diff --git a/juniper/src/macros/interface.rs b/juniper/src/macros/interface.rs index 7f79944c..c1e2a419 100644 --- a/juniper/src/macros/interface.rs +++ b/juniper/src/macros/interface.rs @@ -117,10 +117,12 @@ macro_rules! graphql_interface { args = [$({ arg_name = $arg_name : ident, arg_ty = $arg_ty: ty, - $(arg_description = $arg_description: expr,)* $(arg_default = $arg_default: expr,)* + $(arg_description = $arg_description: expr,)* + $(arg_docstring = $arg_docstring: expr,)* },)*], $(decs = $fn_description: expr,)* + $(docstring = $docstring: expr,)* $(deprecated = $deprecated: expr,)* $(executor_var = $executor: ident,)* },)*], @@ -151,6 +153,7 @@ macro_rules! graphql_interface { info ) $(.description($fn_description))* + $(.push_docstring($docstring))* $(.deprecated($deprecated))* $(.argument( __juniper_create_arg!( @@ -158,8 +161,9 @@ macro_rules! graphql_interface { info = info, arg_ty = $arg_ty, arg_name = $arg_name, - $(description = $arg_description,)* $(default = $arg_default,)* + $(description = $arg_description,)* + $(docstring = $arg_docstring,)* ) ))*, )*]; diff --git a/juniper/src/macros/object.rs b/juniper/src/macros/object.rs index 5261b15a..c077deae 100644 --- a/juniper/src/macros/object.rs +++ b/juniper/src/macros/object.rs @@ -63,6 +63,45 @@ graphql_object!(User: () |&self| { # fn main() { } ``` +**Alternatively,** descriptions can be added with the builtin `doc` attribute. +Consecutive `#[doc = "..."]` attributes will be collapsed into a single description +where the docstrings are separated by newlines. + +```rust +# #[macro_use] extern crate juniper; +struct User { id: String, name: String, group_ids: Vec } + +graphql_object!(User: () |&self| { + description: "A user in the database" + + #[doc = "The user's unique identifier"] + field id() -> &String { + &self.id + } + + #[doc = "The user's name"] + field name() -> &String { + &self.name + } + + #[doc = r#" + Test if a user is member of a group. + + This may return a flitzbit if the floop is twizled. + Make sure not to rumblejumble the cog-rotater. + "#] + #[doc = "Added in vX.Y.44"] + field member_of_group( + #[doc = "The group id you want to test membership against"] + group_id: String, + ) -> bool { + self.group_ids.iter().any(|gid| gid == &group_id) + } +}); + +# fn main() { } +``` + ## Generics and lifetimes You can expose generic or pointer types by prefixing the type with the necessary @@ -232,6 +271,24 @@ Defines a field on the object. The name is converted to camel case, e.g. `user_name` is exposed as `userName`. The `as "Field description"` adds the string as documentation on the field. +A field's description and deprecation can also be set using the +builtin `doc` and `deprecated` attributes. + +```text +#[doc = "Field description"] +field name(args...) -> Type { } + +#[deprecated] // no reason required +field name(args...) -> Type { } + +#[deprecated(note = "Reason")] +field name(args...) -> Type { } + +#[doc = "Field description"] +#[deprecated(note = "Reason")] // deprecated must come after doc +field deprecated "Reason" name(args...) -> Type { } +``` + ### Field arguments ```text @@ -269,6 +326,13 @@ arg_name = (Point { x: 1, y: 2 }): Point arg_name = ("default".to_owned()): String ``` +A description can also be provided using the builtin `doc` attribute. + +```text +#[doc = "Argument description"] +arg_name: ArgType +``` + [1]: struct.Executor.html */ @@ -295,10 +359,12 @@ macro_rules! graphql_object { args = [$({ arg_name = $arg_name : ident, arg_ty = $arg_ty: ty, - $(arg_description = $arg_description: expr,)* $(arg_default = $arg_default: expr,)* + $(arg_description = $arg_description: expr,)* + $(arg_docstring = $arg_docstring: expr,)* },)*], $(decs = $fn_description: expr,)* + $(docstring = $docstring: expr,)* $(deprecated = $deprecated: expr,)* $(executor_var = $executor: ident,)* },)*], @@ -325,6 +391,7 @@ macro_rules! graphql_object { info ) $(.description($fn_description))* + $(.push_docstring($docstring))* $(.deprecated($deprecated))* $(.argument( __juniper_create_arg!( @@ -332,8 +399,9 @@ macro_rules! graphql_object { info = info, arg_ty = $arg_ty, arg_name = $arg_name, - $(description = $arg_description,)* $(default = $arg_default,)* + $(description = $arg_description,)* + $(docstring = $arg_docstring,)* ) ))*, )*]; diff --git a/juniper/src/macros/tests/args.rs b/juniper/src/macros/tests/args.rs index 09c65899..fbe42aa8 100644 --- a/juniper/src/macros/tests/args.rs +++ b/juniper/src/macros/tests/args.rs @@ -49,6 +49,13 @@ graphql_object!(Root: () |&self| { arg2: i32 as "The second arg", ) -> i32 { 0 } + field attr_arg_descr(#[doc = "The arg"] arg: i32) -> i32 { 0 } + field attr_arg_descr_collapse( + #[doc = "The arg"] + #[doc = "and more details"] + arg: i32, + ) -> i32 { 0 } + field arg_with_default(arg = 123: i32) -> i32 { 0 } field multi_args_with_default( arg1 = 123: i32, @@ -499,6 +506,72 @@ fn introspect_field_multi_args_descr_trailing_comma() { }); } +#[test] +fn introspect_field_attr_arg_descr() { + run_args_info_query("attrArgDescr", |args| { + assert_eq!(args.len(), 1); + + assert!( + args.contains(&Value::object( + vec![ + ("name", Value::scalar("arg")), + ("description", Value::scalar("The arg")), + ("defaultValue", Value::null()), + ( + "type", + Value::object( + vec![ + ("name", Value::null()), + ( + "ofType", + Value::object( + vec![("name", Value::scalar("Int"))].into_iter().collect(), + ), + ), + ].into_iter() + .collect(), + ), + ), + ].into_iter() + .collect(), + )) + ); + }); +} + +#[test] +fn introspect_field_attr_arg_descr_collapse() { + run_args_info_query("attrArgDescrCollapse", |args| { + assert_eq!(args.len(), 1); + + assert!( + args.contains(&Value::object( + vec![ + ("name", Value::scalar("arg")), + ("description", Value::scalar("The arg\nand more details")), + ("defaultValue", Value::null()), + ( + "type", + Value::object( + vec![ + ("name", Value::null()), + ( + "ofType", + Value::object( + vec![("name", Value::scalar("Int"))].into_iter().collect(), + ), + ), + ].into_iter() + .collect(), + ), + ), + ].into_iter() + .collect(), + )) + ); + }); +} + #[test] fn introspect_field_arg_with_default() { run_args_info_query("argWithDefault", |args| { diff --git a/juniper/src/macros/tests/field.rs b/juniper/src/macros/tests/field.rs index 862e2cb1..82b428d4 100644 --- a/juniper/src/macros/tests/field.rs +++ b/juniper/src/macros/tests/field.rs @@ -31,6 +31,31 @@ graphql_object!(Root: () |&self| { field deprecated "Deprecation reason" deprecated_descr() -> i32 as "Field description" { 0 } + #[doc = "Field description"] + field attr_description() -> i32 { 0 } + + #[doc = "Field description"] + #[doc = "with `collapse_docs` behavior"] // https://doc.rust-lang.org/rustdoc/the-doc-attribute.html + field attr_description_collapse() -> i32 { 0 } + + #[doc = r#" + Get the i32 representation of 0. + + - This comment is longer. + - These two lines are rendered as bullets by GraphiQL. + "#] + field attr_description_long() -> i32 { 0 } + + #[deprecated] + field attr_deprecated() -> i32 { 0 } + + #[deprecated(note = "Deprecation reason")] + field attr_deprecated_reason() -> i32 { 0 } + + #[doc = "Field description"] + #[deprecated(note = "Deprecation reason")] + field attr_deprecated_descr() -> i32 { 0 } + field with_field_result() -> FieldResult { Ok(0) } field with_return() -> i32 { return 0; } @@ -51,6 +76,31 @@ graphql_interface!(Interface: () |&self| { field deprecated "Deprecation reason" deprecated_descr() -> i32 as "Field description" { 0 } + #[doc = "Field description"] + field attr_description() -> i32 { 0 } + + #[doc = "Field description"] + #[doc = "with `collapse_docs` behavior"] // https://doc.rust-lang.org/rustdoc/the-doc-attribute.html + field attr_description_collapse() -> i32 { 0 } + + #[doc = r#" + Get the i32 representation of 0. + + - This comment is longer. + - These two lines are rendered as bullets by GraphiQL. + "#] + field attr_description_long() -> i32 { 0 } + + #[deprecated] + field attr_deprecated() -> i32 { 0 } + + #[deprecated(note = "Deprecation reason")] + field attr_deprecated_reason() -> i32 { 0 } + + #[doc = "Field description"] + #[deprecated(note = "Deprecation reason")] + field attr_deprecated_descr() -> i32 { 0 } + instance_resolvers: |&_| { Root => Some(Root {}), } @@ -280,3 +330,255 @@ fn introspect_interface_field_deprecated_descr() { ); }); } + +#[test] +fn introspect_object_field_attr_description() { + run_field_info_query("Root", "attrDescription", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDescription")) + ); + assert_eq!( + field.get_field_value("description"), + Some(&Value::scalar("Field description")) + ); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(false)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::null()) + ); + }); +} + +#[test] +fn introspect_interface_field_attr_description() { + run_field_info_query("Interface", "attrDescription", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDescription")) + ); + assert_eq!( + field.get_field_value("description"), + Some(&Value::scalar("Field description")) + ); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(false)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::null()) + ); + }); +} + +#[test] +fn introspect_object_field_attr_description_long() { + run_field_info_query("Root", "attrDescriptionLong", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDescriptionLong")) + ); + assert_eq!( + field.get_field_value("description"), + Some(&Value::scalar("Get the i32 representation of 0.\n\n- This comment is longer.\n- These two lines are rendered as bullets by GraphiQL.")) + ); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(false)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::null()) + ); + }); +} + +#[test] +fn introspect_interface_field_attr_description_long() { + run_field_info_query("Interface", "attrDescriptionLong", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDescriptionLong")) + ); + assert_eq!( + field.get_field_value("description"), + Some(&Value::scalar("Get the i32 representation of 0.\n\n- This comment is longer.\n- These two lines are rendered as bullets by GraphiQL.")) + ); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(false)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::null()) + ); + }); +} + +#[test] +fn introspect_object_field_attr_description_collapse() { + run_field_info_query("Root", "attrDescriptionCollapse", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDescriptionCollapse")) + ); + assert_eq!( + field.get_field_value("description"), + Some(&Value::scalar("Field description\nwith `collapse_docs` behavior")) + ); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(false)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::null()) + ); + }); +} + +#[test] +fn introspect_interface_field_attr_description_collapse() { + run_field_info_query("Interface", "attrDescriptionCollapse", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDescriptionCollapse")) + ); + assert_eq!( + field.get_field_value("description"), + Some(&Value::scalar("Field description\nwith `collapse_docs` behavior")) + ); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(false)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::null()) + ); + }); +} + +#[test] +fn introspect_object_field_attr_deprecated() { + run_field_info_query("Root", "attrDeprecated", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDeprecated")) + ); + assert_eq!(field.get_field_value("description"), Some(&Value::null())); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(true)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::null()) + ); + }); +} + +#[test] +fn introspect_interface_field_attr_deprecated() { + run_field_info_query("Interface", "attrDeprecated", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDeprecated")) + ); + assert_eq!(field.get_field_value("description"), Some(&Value::null())); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(true)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::null()) + ); + }); +} + +#[test] +fn introspect_object_field_attr_deprecated_reason() { + run_field_info_query("Root", "attrDeprecatedReason", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDeprecatedReason")) + ); + assert_eq!(field.get_field_value("description"), Some(&Value::null())); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(true)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::scalar("Deprecation reason")) + ); + }); +} + +#[test] +fn introspect_interface_field_attr_deprecated_reason() { + run_field_info_query("Interface", "attrDeprecatedReason", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDeprecatedReason")) + ); + assert_eq!(field.get_field_value("description"), Some(&Value::null())); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(true)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::scalar("Deprecation reason")) + ); + }); +} + +#[test] +fn introspect_object_field_attr_deprecated_descr() { + run_field_info_query("Root", "attrDeprecatedDescr", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDeprecatedDescr")) + ); + assert_eq!( + field.get_field_value("description"), + Some(&Value::scalar("Field description")) + ); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(true)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::scalar("Deprecation reason")) + ); + }); +} + +#[test] +fn introspect_interface_field_attr_deprecated_descr() { + run_field_info_query("Interface", "attrDeprecatedDescr", |field| { + assert_eq!( + field.get_field_value("name"), + Some(&Value::scalar("attrDeprecatedDescr")) + ); + assert_eq!( + field.get_field_value("description"), + Some(&Value::scalar("Field description")) + ); + assert_eq!( + field.get_field_value("isDeprecated"), + Some(&Value::scalar(true)) + ); + assert_eq!( + field.get_field_value("deprecationReason"), + Some(&Value::scalar("Deprecation reason")) + ); + }); +} diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index f437f888..93a4095d 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -9,6 +9,33 @@ use schema::model::SchemaType; use types::base::TypeKind; use value::{DefaultScalarValue, ParseScalarValue, ScalarRefValue, ScalarValue}; +/// Whether an item is deprecated, with context. +#[derive(Debug, PartialEq, Hash, Clone)] +pub enum DeprecationStatus { + /// The field/variant is not deprecated. + Current, + /// The field/variant is deprecated, with an optional reason + Deprecated(Option), +} + +impl DeprecationStatus { + /// If this deprecation status indicates the item is deprecated. + pub fn is_deprecated(&self) -> bool { + match self { + &DeprecationStatus::Current => false, + &DeprecationStatus::Deprecated(_) => true, + } + } + + /// An optional reason for the deprecation, or none if `Current`. + pub fn reason(&self) -> Option<&String> { + match self { + &DeprecationStatus::Current => None, + &DeprecationStatus::Deprecated(ref reason) => reason.as_ref(), + } + } +} + /// Scalar type metadata pub struct ScalarMeta<'a, S> { #[doc(hidden)] @@ -135,7 +162,7 @@ pub struct Field<'a, S> { #[doc(hidden)] pub field_type: Type<'a>, #[doc(hidden)] - pub deprecation_reason: Option, + pub deprecation_status: DeprecationStatus, } /// Metadata for an argument to a field @@ -163,10 +190,8 @@ pub struct EnumValue { /// Note: this is not the description of the enum itself; it's the /// description of this enum _value_. pub description: Option, - /// The optional deprecation reason - /// - /// If this is `Some`, the field will be considered `isDeprecated`. - pub deprecation_reason: Option, + /// Whether the field is deprecated or not, with an optional reason. + pub deprecation_status: DeprecationStatus, } impl<'a, S> MetaType<'a, S> { @@ -580,6 +605,28 @@ impl<'a, S> Field<'a, S> { self } + /// Adds a (multi)line doc string to the description of the field. + /// Any leading or trailing newlines will be removed. + /// + /// If the docstring contains newlines, repeated leading tab and space characters + /// will be removed from the beginning of each line. + /// + /// If the description hasn't been set, the description is set to the provided line. + /// Otherwise, the doc string is added to the current description after a newline. + pub fn push_docstring(mut self, multiline: &str) -> Field<'a, S> { + let docstring = clean_docstring(multiline); + match &mut self.description { + &mut Some(ref mut desc) => { + desc.push('\n'); + desc.push_str(&docstring); + } + desc @ &mut None => { + *desc = Some(docstring.to_string()); + } + } + self + } + /// Add an argument to the field /// /// Arguments are unordered and can't contain duplicates by name. @@ -596,11 +643,11 @@ impl<'a, S> Field<'a, S> { self } - /// Set the deprecation reason + /// Set the field to be deprecated with an optional reason. /// /// This overwrites the deprecation reason if any was previously set. - pub fn deprecated(mut self, reason: &str) -> Self { - self.deprecation_reason = Some(reason.to_owned()); + pub fn deprecated(mut self, reason: Option<&str>) -> Self { + self.deprecation_status = DeprecationStatus::Deprecated(reason.map(|s| s.to_owned())); self } } @@ -624,6 +671,28 @@ impl<'a, S> Argument<'a, S> { self } + /// Adds a (multi)line doc string to the description of the field. + /// Any leading or trailing newlines will be removed. + /// + /// If the docstring contains newlines, repeated leading tab and space characters + /// will be removed from the beginning of each line. + /// + /// If the description hasn't been set, the description is set to the provided line. + /// Otherwise, the doc string is added to the current description after a newline. + pub fn push_docstring(mut self, multiline: &str) -> Argument<'a, S> { + let docstring = clean_docstring(multiline); + match &mut self.description { + &mut Some(ref mut desc) => { + desc.push('\n'); + desc.push_str(&docstring); + } + desc @ &mut None => { + *desc = Some(docstring.to_string()); + } + } + self + } + /// Set the default value of the argument /// /// This overwrites the description if any was previously set. @@ -639,7 +708,7 @@ impl EnumValue { EnumValue { name: name.to_owned(), description: None, - deprecation_reason: None, + deprecation_status: DeprecationStatus::Current, } } @@ -651,11 +720,11 @@ impl EnumValue { self } - /// Set the deprecation reason for the enum value + /// Set the enum value to be deprecated with an optional reason. /// /// This overwrites the deprecation reason if any was previously set. - pub fn deprecated(mut self, reason: &str) -> EnumValue { - self.deprecation_reason = Some(reason.to_owned()); + pub fn deprecated(mut self, reason: Option<&str>) -> Self { + self.deprecation_status = DeprecationStatus::Deprecated(reason.map(|s| s.to_owned())); self } } @@ -696,3 +765,27 @@ where { >::from_input_value(v).is_some() } + +fn clean_docstring<'a>(multiline: &'a str) -> Cow<'a, str> { + let trim_start = multiline.split('\n') + .skip(1) + .filter_map(|ln| ln.chars().position(|ch| ch != ' ' && ch != '\t')) + .min(); + if let Some(trim) = trim_start { + let trimmed = multiline + .split('\n') + .map(|ln| { + if !ln.starts_with(' ') && !ln.starts_with('\t') { + ln // skip trimming the first line + } else if ln.len() >= trim { + &ln[trim..] + } else { + "" + } + }) + .collect::>(); + Cow::from(trimmed.join("\n").trim_matches('\n').to_owned()) + } else { + Cow::from(multiline.trim_matches('\n')) + } +} diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index f5d34001..a2bac671 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -132,7 +132,7 @@ graphql_object!(<'a> TypeType<'a, S>: SchemaType<'a, S> as "__Type" TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) => Some(fields .iter() - .filter(|f| include_deprecated || f.deprecation_reason.is_none()) + .filter(|f| include_deprecated || !f.deprecation_status.is_deprecated()) .filter(|f| !f.name.starts_with("__")) .collect()), _ => None, @@ -201,7 +201,7 @@ graphql_object!(<'a> TypeType<'a, S>: SchemaType<'a, S> as "__Type" TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) => Some(values .iter() - .filter(|f| include_deprecated || f.deprecation_reason.is_none()) + .filter(|f| include_deprecated || !f.deprecation_status.is_deprecated()) .collect()), _ => None, } @@ -228,11 +228,11 @@ graphql_object!(<'a> Field<'a, S>: SchemaType<'a, S> as "__Field" } field is_deprecated() -> bool { - self.deprecation_reason.is_some() + self.deprecation_status.is_deprecated() } - field deprecation_reason() -> &Option { - &self.deprecation_reason + field deprecation_reason() -> Option<&String> { + self.deprecation_status.reason() } }); @@ -266,11 +266,11 @@ graphql_object!(EnumValue: () as "__EnumValue" where Scalar = |&self| { } field is_deprecated() -> bool { - self.deprecation_reason.is_some() + self.deprecation_status.is_deprecated() } - field deprecation_reason() -> &Option { - &self.deprecation_reason + field deprecation_reason() -> Option<&String> { + self.deprecation_status.reason() } }); diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs index e5d82b34..b6f196aa 100644 --- a/juniper_codegen/src/derive_enum.rs +++ b/juniper_codegen/src/derive_enum.rs @@ -44,7 +44,7 @@ impl EnumAttrs { continue; } panic!(format!( - "Unknown attribute for #[derive(GraphQLEnum)]: {:?}", + "Unknown enum attribute for #[derive(GraphQLEnum)]: {:?}", item )); } @@ -57,7 +57,7 @@ impl EnumAttrs { struct EnumVariantAttrs { name: Option, description: Option, - deprecation: Option, + deprecation: Option, } impl EnumVariantAttrs { @@ -67,6 +67,9 @@ impl EnumVariantAttrs { // Check doc comments for description. res.description = get_doc_comment(&variant.attrs); + // Check builtin deprecated attribute for deprecation. + res.deprecation = get_deprecated(&variant.attrs); + // Check attributes for name and description. if let Some(items) = get_graphql_attr(&variant.attrs) { for item in items { @@ -90,13 +93,24 @@ impl EnumVariantAttrs { continue; } if let Some(AttributeValue::String(val)) = - keyed_item_value(&item, "deprecated", AttributeValidation::String) + keyed_item_value(&item, "deprecation", AttributeValidation::String) { - res.deprecation = Some(val); + res.deprecation = Some(DeprecationAttr { reason: Some(val) }); continue; } + match keyed_item_value(&item, "deprecated", AttributeValidation::String) { + Some(AttributeValue::String(val)) => { + res.deprecation = Some(DeprecationAttr { reason: Some(val) }); + continue; + } + Some(AttributeValue::Bare) => { + res.deprecation = Some(DeprecationAttr { reason: None }); + continue; + } + None => {} + } panic!(format!( - "Unknown attribute for #[derive(GraphQLEnum)]: {:?}", + "Unknown variant attribute for #[derive(GraphQLEnum)]: {:?}", item )); } @@ -151,14 +165,21 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> TokenStream { None => quote!{ None }, }; let depr = match var_attrs.deprecation { - Some(s) => quote!{ Some(#s.to_string()) }, - None => quote!{ None }, + Some(DeprecationAttr { reason: Some(s) }) => quote!{ + _juniper::meta::DeprecationStatus::Deprecated(Some(#s.to_string())) + }, + Some(DeprecationAttr { reason: None }) => quote!{ + _juniper::meta::DeprecationStatus::Deprecated(None) + }, + None => quote!{ + _juniper::meta::DeprecationStatus::Current + }, }; values.extend(quote!{ _juniper::meta::EnumValue{ name: #name.to_string(), description: #descr, - deprecation_reason: #depr, + deprecation_status: #depr, }, }); diff --git a/juniper_codegen/src/derive_object.rs b/juniper_codegen/src/derive_object.rs index 76be8498..8f01b66b 100644 --- a/juniper_codegen/src/derive_object.rs +++ b/juniper_codegen/src/derive_object.rs @@ -47,7 +47,7 @@ impl ObjAttrs { continue; } panic!(format!( - "Unknown object attribute for #[derive(GraphQLObject)]: {:?}", + "Unknown struct attribute for #[derive(GraphQLObject)]: {:?}", item )); } @@ -60,7 +60,7 @@ impl ObjAttrs { struct ObjFieldAttrs { name: Option, description: Option, - deprecation: Option, + deprecation: Option, skip: bool, } @@ -71,6 +71,9 @@ impl ObjFieldAttrs { // Check doc comments for description. res.description = get_doc_comment(&variant.attrs); + // Check builtin deprecated attribute for deprecation. + res.deprecation = get_deprecated(&variant.attrs); + // Check attributes. if let Some(items) = get_graphql_attr(&variant.attrs) { for item in items { @@ -96,9 +99,20 @@ impl ObjFieldAttrs { if let Some(AttributeValue::String(val)) = keyed_item_value(&item, "deprecation", AttributeValidation::String) { - res.deprecation = Some(val); + res.deprecation = Some(DeprecationAttr { reason: Some(val) }); continue; } + match keyed_item_value(&item, "deprecated", AttributeValidation::String) { + Some(AttributeValue::String(val)) => { + res.deprecation = Some(DeprecationAttr { reason: Some(val) }); + continue; + } + Some(AttributeValue::Bare) => { + res.deprecation = Some(DeprecationAttr { reason: None }); + continue; + } + None => {} + } if let Some(_) = keyed_item_value(&item, "skip", AttributeValidation::Bare) { res.skip = true; continue; @@ -167,7 +181,8 @@ pub fn impl_object(ast: &syn::DeriveInput) -> TokenStream { }; let build_deprecation = match field_attrs.deprecation { - Some(s) => quote!{ field.deprecated(#s) }, + Some(DeprecationAttr { reason: Some(s) }) => quote!{ field.deprecated(Some(#s)) }, + Some(DeprecationAttr { reason: None }) => quote!{ field.deprecated(None) }, None => quote!{ field }, }; diff --git a/juniper_codegen/src/util.rs b/juniper_codegen/src/util.rs index 676a98cb..ebbf0379 100644 --- a/juniper_codegen/src/util.rs +++ b/juniper_codegen/src/util.rs @@ -1,5 +1,5 @@ use regex::Regex; -use syn::{Attribute, Lit, Meta, MetaNameValue, NestedMeta}; +use syn::{Attribute, Lit, Meta, MetaList, MetaNameValue, NestedMeta}; pub enum AttributeValidation { Any, @@ -12,6 +12,42 @@ pub enum AttributeValue { String(String), } +pub struct DeprecationAttr { + pub reason: Option, +} + +pub fn get_deprecated(attrs: &Vec) -> Option { + for attr in attrs { + match attr.interpret_meta() { + Some(Meta::List(ref list)) if list.ident == "deprecated" => { + return Some(get_deprecated_meta_list(list)); + } + Some(Meta::Word(ref ident)) if ident == "deprecated" => { + return Some(DeprecationAttr { reason: None }); + } + _ => {} + } + } + None +} + +fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr { + for meta in &list.nested { + match meta { + &NestedMeta::Meta(Meta::NameValue(ref nv)) if nv.ident == "note" => { + match &nv.lit { + &Lit::Str(ref strlit) => { + return DeprecationAttr { reason: Some(strlit.value().to_string()) }; + } + _ => panic!("deprecated attribute note value only has string literal"), + } + } + _ => {} + } + } + DeprecationAttr { reason: None } +} + // Gets doc comment. pub fn get_doc_comment(attrs: &Vec) -> Option { if let Some(items) = get_doc_attr(attrs) { diff --git a/juniper_tests/src/codegen/derive_input_object.rs b/juniper_tests/src/codegen/derive_input_object.rs index 068068fa..cd255981 100644 --- a/juniper_tests/src/codegen/derive_input_object.rs +++ b/juniper_tests/src/codegen/derive_input_object.rs @@ -78,7 +78,7 @@ impl<'a> GraphQLType for &'a Fake { &[juniper::meta::EnumValue { name: "fake".to_string(), description: None, - deprecation_reason: None, + deprecation_status: juniper::meta::DeprecationStatus::Current, }], ); meta.into_meta()