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
This commit is contained in:
Kevin Stenerson 2018-10-27 21:28:48 -06:00 committed by Christian Legnitto
parent f61fdb2063
commit 0f2a654471
13 changed files with 804 additions and 49 deletions

View file

@ -14,3 +14,73 @@
generic code. To retain the current behaviour use `DefaultScalarValue` as scalar value type generic code. To retain the current behaviour use `DefaultScalarValue` as scalar value type
[#251](https://github.com/graphql-rust/juniper/pull/251) [#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)

View file

@ -15,8 +15,8 @@ use value::Value;
use GraphQLError; use GraphQLError;
use schema::meta::{ use schema::meta::{
Argument, EnumMeta, EnumValue, Field, InputObjectMeta, InterfaceMeta, ListMeta, MetaType, Argument, DeprecationStatus, EnumMeta, EnumValue, Field, InputObjectMeta, InterfaceMeta,
NullableMeta, ObjectMeta, PlaceholderMeta, ScalarMeta, UnionMeta, ListMeta, MetaType, NullableMeta, ObjectMeta, PlaceholderMeta, ScalarMeta, UnionMeta,
}; };
use schema::model::{RootNode, SchemaType, TypeType}; use schema::model::{RootNode, SchemaType, TypeType};
@ -729,7 +729,7 @@ where
description: None, description: None,
arguments: None, arguments: None,
field_type: self.get_type::<T>(info), field_type: self.get_type::<T>(info),
deprecation_reason: None, deprecation_status: DeprecationStatus::Current,
} }
} }
@ -748,7 +748,7 @@ where
description: None, description: None,
arguments: None, arguments: None,
field_type: self.get_type::<I>(info), field_type: self.get_type::<I>(info),
deprecation_reason: None, deprecation_status: DeprecationStatus::Current,
} }
} }

View file

@ -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, success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*}, additional_parser = {$($additional:tt)*},
@ -406,7 +475,7 @@ macro_rules! __juniper_parse_field_list {
items = [$({$($items: tt)*},)*], items = [$({$($items: tt)*},)*],
rest = field deprecated $reason:tt $name: ident ( rest = field deprecated $reason:tt $name: ident (
$(&$executor: tt)* $(,)* $(&$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 ) -> $return_ty: ty $(as $desc: tt)* $body: block
$($rest:tt)* $($rest:tt)*
) => { ) => {
@ -422,12 +491,12 @@ macro_rules! __juniper_parse_field_list {
$({ $({
arg_name = $arg_name, arg_name = $arg_name,
arg_ty = $arg_ty, arg_ty = $arg_ty,
$(arg_default = $arg_default,)*
$(arg_description = $arg_desc,)* $(arg_description = $arg_desc,)*
$(arg_default = $default_value,)*
},)* },)*
], ],
$(decs = $desc,)* $(decs = $desc,)*
deprecated = $reason, deprecated = Some($reason),
$(executor_var = $executor,)* $(executor_var = $executor,)*
},], },],
rest = $($rest)* rest = $($rest)*
@ -440,7 +509,7 @@ macro_rules! __juniper_parse_field_list {
items = [$({$($items: tt)*},)*], items = [$({$($items: tt)*},)*],
rest = field $name: ident ( rest = field $name: ident (
$(&$executor: 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 ) -> $return_ty: ty $(as $desc: tt)* $body: block
$($rest:tt)* $($rest:tt)*
) => { ) => {
@ -456,8 +525,8 @@ macro_rules! __juniper_parse_field_list {
$({ $({
arg_name = $arg_name, arg_name = $arg_name,
arg_ty = $arg_ty, arg_ty = $arg_ty,
$(arg_default = $arg_default,)*
$(arg_description = $arg_desc,)* $(arg_description = $arg_desc,)*
$(arg_default = $default_value,)*
},)* },)*
], ],
$(decs = $desc,)* $(decs = $desc,)*
@ -601,12 +670,14 @@ macro_rules! __juniper_create_arg {
arg_ty = $arg_ty: ty, arg_ty = $arg_ty: ty,
arg_name = $arg_name: ident, arg_name = $arg_name: ident,
$(description = $arg_description: expr,)* $(description = $arg_description: expr,)*
$(docstring = $arg_docstring: expr,)*
) => { ) => {
$reg.arg::<$arg_ty>( $reg.arg::<$arg_ty>(
&$crate::to_camel_case(stringify!($arg_name)), &$crate::to_camel_case(stringify!($arg_name)),
$info, $info,
) )
$(.description($arg_description))* $(.description($arg_description))*
$(.push_docstring($arg_docstring))*
}; };
( (
@ -614,8 +685,9 @@ macro_rules! __juniper_create_arg {
info = $info: ident, info = $info: ident,
arg_ty = $arg_ty: ty, arg_ty = $arg_ty: ty,
arg_name = $arg_name: ident, arg_name = $arg_name: ident,
$(description = $arg_description: expr,)*
default = $arg_default: expr, default = $arg_default: expr,
$(description = $arg_description: expr,)*
$(docstring = $arg_docstring: expr,)*
) => { ) => {
$reg.arg_with_default::<$arg_ty>( $reg.arg_with_default::<$arg_ty>(
&$crate::to_camel_case(stringify!($arg_name)), &$crate::to_camel_case(stringify!($arg_name)),
@ -623,5 +695,6 @@ macro_rules! __juniper_create_arg {
$info, $info,
) )
$(.description($arg_description))* $(.description($arg_description))*
$(.push_docstring($arg_docstring))*
}; };
} }

View file

@ -117,10 +117,12 @@ macro_rules! graphql_interface {
args = [$({ args = [$({
arg_name = $arg_name : ident, arg_name = $arg_name : ident,
arg_ty = $arg_ty: ty, arg_ty = $arg_ty: ty,
$(arg_description = $arg_description: expr,)*
$(arg_default = $arg_default: expr,)* $(arg_default = $arg_default: expr,)*
$(arg_description = $arg_description: expr,)*
$(arg_docstring = $arg_docstring: expr,)*
},)*], },)*],
$(decs = $fn_description: expr,)* $(decs = $fn_description: expr,)*
$(docstring = $docstring: expr,)*
$(deprecated = $deprecated: expr,)* $(deprecated = $deprecated: expr,)*
$(executor_var = $executor: ident,)* $(executor_var = $executor: ident,)*
},)*], },)*],
@ -151,6 +153,7 @@ macro_rules! graphql_interface {
info info
) )
$(.description($fn_description))* $(.description($fn_description))*
$(.push_docstring($docstring))*
$(.deprecated($deprecated))* $(.deprecated($deprecated))*
$(.argument( $(.argument(
__juniper_create_arg!( __juniper_create_arg!(
@ -158,8 +161,9 @@ macro_rules! graphql_interface {
info = info, info = info,
arg_ty = $arg_ty, arg_ty = $arg_ty,
arg_name = $arg_name, arg_name = $arg_name,
$(description = $arg_description,)*
$(default = $arg_default,)* $(default = $arg_default,)*
$(description = $arg_description,)*
$(docstring = $arg_docstring,)*
) )
))*, ))*,
)*]; )*];

View file

@ -63,6 +63,45 @@ graphql_object!(User: () |&self| {
# fn main() { } # 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<String> }
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 ## Generics and lifetimes
You can expose generic or pointer types by prefixing the type with the necessary 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 `user_name` is exposed as `userName`. The `as "Field description"` adds the
string as documentation on the field. 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 ### Field arguments
```text ```text
@ -269,6 +326,13 @@ arg_name = (Point { x: 1, y: 2 }): Point
arg_name = ("default".to_owned()): String 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 [1]: struct.Executor.html
*/ */
@ -295,10 +359,12 @@ macro_rules! graphql_object {
args = [$({ args = [$({
arg_name = $arg_name : ident, arg_name = $arg_name : ident,
arg_ty = $arg_ty: ty, arg_ty = $arg_ty: ty,
$(arg_description = $arg_description: expr,)*
$(arg_default = $arg_default: expr,)* $(arg_default = $arg_default: expr,)*
$(arg_description = $arg_description: expr,)*
$(arg_docstring = $arg_docstring: expr,)*
},)*], },)*],
$(decs = $fn_description: expr,)* $(decs = $fn_description: expr,)*
$(docstring = $docstring: expr,)*
$(deprecated = $deprecated: expr,)* $(deprecated = $deprecated: expr,)*
$(executor_var = $executor: ident,)* $(executor_var = $executor: ident,)*
},)*], },)*],
@ -325,6 +391,7 @@ macro_rules! graphql_object {
info info
) )
$(.description($fn_description))* $(.description($fn_description))*
$(.push_docstring($docstring))*
$(.deprecated($deprecated))* $(.deprecated($deprecated))*
$(.argument( $(.argument(
__juniper_create_arg!( __juniper_create_arg!(
@ -332,8 +399,9 @@ macro_rules! graphql_object {
info = info, info = info,
arg_ty = $arg_ty, arg_ty = $arg_ty,
arg_name = $arg_name, arg_name = $arg_name,
$(description = $arg_description,)*
$(default = $arg_default,)* $(default = $arg_default,)*
$(description = $arg_description,)*
$(docstring = $arg_docstring,)*
) )
))*, ))*,
)*]; )*];

View file

@ -49,6 +49,13 @@ graphql_object!(Root: () |&self| {
arg2: i32 as "The second arg", arg2: i32 as "The second arg",
) -> i32 { 0 } ) -> 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 arg_with_default(arg = 123: i32) -> i32 { 0 }
field multi_args_with_default( field multi_args_with_default(
arg1 = 123: i32, 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] #[test]
fn introspect_field_arg_with_default() { fn introspect_field_arg_with_default() {
run_args_info_query("argWithDefault", |args| { run_args_info_query("argWithDefault", |args| {

View file

@ -31,6 +31,31 @@ graphql_object!(Root: () |&self| {
field deprecated "Deprecation reason" field deprecated "Deprecation reason"
deprecated_descr() -> i32 as "Field description" { 0 } 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<i32> { Ok(0) } field with_field_result() -> FieldResult<i32> { Ok(0) }
field with_return() -> i32 { return 0; } field with_return() -> i32 { return 0; }
@ -51,6 +76,31 @@ graphql_interface!(Interface: () |&self| {
field deprecated "Deprecation reason" field deprecated "Deprecation reason"
deprecated_descr() -> i32 as "Field description" { 0 } 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: |&_| { instance_resolvers: |&_| {
Root => Some(Root {}), 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"))
);
});
}

View file

@ -9,6 +9,33 @@ use schema::model::SchemaType;
use types::base::TypeKind; use types::base::TypeKind;
use value::{DefaultScalarValue, ParseScalarValue, ScalarRefValue, ScalarValue}; 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<String>),
}
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 /// Scalar type metadata
pub struct ScalarMeta<'a, S> { pub struct ScalarMeta<'a, S> {
#[doc(hidden)] #[doc(hidden)]
@ -135,7 +162,7 @@ pub struct Field<'a, S> {
#[doc(hidden)] #[doc(hidden)]
pub field_type: Type<'a>, pub field_type: Type<'a>,
#[doc(hidden)] #[doc(hidden)]
pub deprecation_reason: Option<String>, pub deprecation_status: DeprecationStatus,
} }
/// Metadata for an argument to a field /// 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 /// Note: this is not the description of the enum itself; it's the
/// description of this enum _value_. /// description of this enum _value_.
pub description: Option<String>, pub description: Option<String>,
/// The optional deprecation reason /// Whether the field is deprecated or not, with an optional reason.
/// pub deprecation_status: DeprecationStatus,
/// If this is `Some`, the field will be considered `isDeprecated`.
pub deprecation_reason: Option<String>,
} }
impl<'a, S> MetaType<'a, S> { impl<'a, S> MetaType<'a, S> {
@ -580,6 +605,28 @@ impl<'a, S> Field<'a, S> {
self 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 /// Add an argument to the field
/// ///
/// Arguments are unordered and can't contain duplicates by name. /// Arguments are unordered and can't contain duplicates by name.
@ -596,11 +643,11 @@ impl<'a, S> Field<'a, S> {
self 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. /// This overwrites the deprecation reason if any was previously set.
pub fn deprecated(mut self, reason: &str) -> Self { pub fn deprecated(mut self, reason: Option<&str>) -> Self {
self.deprecation_reason = Some(reason.to_owned()); self.deprecation_status = DeprecationStatus::Deprecated(reason.map(|s| s.to_owned()));
self self
} }
} }
@ -624,6 +671,28 @@ impl<'a, S> Argument<'a, S> {
self 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 /// Set the default value of the argument
/// ///
/// This overwrites the description if any was previously set. /// This overwrites the description if any was previously set.
@ -639,7 +708,7 @@ impl EnumValue {
EnumValue { EnumValue {
name: name.to_owned(), name: name.to_owned(),
description: None, description: None,
deprecation_reason: None, deprecation_status: DeprecationStatus::Current,
} }
} }
@ -651,11 +720,11 @@ impl EnumValue {
self 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. /// This overwrites the deprecation reason if any was previously set.
pub fn deprecated(mut self, reason: &str) -> EnumValue { pub fn deprecated(mut self, reason: Option<&str>) -> Self {
self.deprecation_reason = Some(reason.to_owned()); self.deprecation_status = DeprecationStatus::Deprecated(reason.map(|s| s.to_owned()));
self self
} }
} }
@ -696,3 +765,27 @@ where
{ {
<T as FromInputValue<S>>::from_input_value(v).is_some() <T as FromInputValue<S>>::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::<Vec<_>>();
Cow::from(trimmed.join("\n").trim_matches('\n').to_owned())
} else {
Cow::from(multiline.trim_matches('\n'))
}
}

View file

@ -132,7 +132,7 @@ graphql_object!(<'a> TypeType<'a, S>: SchemaType<'a, S> as "__Type"
TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) => TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) =>
Some(fields Some(fields
.iter() .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("__")) .filter(|f| !f.name.starts_with("__"))
.collect()), .collect()),
_ => None, _ => None,
@ -201,7 +201,7 @@ graphql_object!(<'a> TypeType<'a, S>: SchemaType<'a, S> as "__Type"
TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) => TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) =>
Some(values Some(values
.iter() .iter()
.filter(|f| include_deprecated || f.deprecation_reason.is_none()) .filter(|f| include_deprecated || !f.deprecation_status.is_deprecated())
.collect()), .collect()),
_ => None, _ => None,
} }
@ -228,11 +228,11 @@ graphql_object!(<'a> Field<'a, S>: SchemaType<'a, S> as "__Field"
} }
field is_deprecated() -> bool { field is_deprecated() -> bool {
self.deprecation_reason.is_some() self.deprecation_status.is_deprecated()
} }
field deprecation_reason() -> &Option<String> { field deprecation_reason() -> Option<&String> {
&self.deprecation_reason self.deprecation_status.reason()
} }
}); });
@ -266,11 +266,11 @@ graphql_object!(EnumValue: () as "__EnumValue" where Scalar = <S> |&self| {
} }
field is_deprecated() -> bool { field is_deprecated() -> bool {
self.deprecation_reason.is_some() self.deprecation_status.is_deprecated()
} }
field deprecation_reason() -> &Option<String> { field deprecation_reason() -> Option<&String> {
&self.deprecation_reason self.deprecation_status.reason()
} }
}); });

View file

@ -44,7 +44,7 @@ impl EnumAttrs {
continue; continue;
} }
panic!(format!( panic!(format!(
"Unknown attribute for #[derive(GraphQLEnum)]: {:?}", "Unknown enum attribute for #[derive(GraphQLEnum)]: {:?}",
item item
)); ));
} }
@ -57,7 +57,7 @@ impl EnumAttrs {
struct EnumVariantAttrs { struct EnumVariantAttrs {
name: Option<String>, name: Option<String>,
description: Option<String>, description: Option<String>,
deprecation: Option<String>, deprecation: Option<DeprecationAttr>,
} }
impl EnumVariantAttrs { impl EnumVariantAttrs {
@ -67,6 +67,9 @@ impl EnumVariantAttrs {
// Check doc comments for description. // Check doc comments for description.
res.description = get_doc_comment(&variant.attrs); 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. // Check attributes for name and description.
if let Some(items) = get_graphql_attr(&variant.attrs) { if let Some(items) = get_graphql_attr(&variant.attrs) {
for item in items { for item in items {
@ -90,13 +93,24 @@ impl EnumVariantAttrs {
continue; continue;
} }
if let Some(AttributeValue::String(val)) = 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; 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!( panic!(format!(
"Unknown attribute for #[derive(GraphQLEnum)]: {:?}", "Unknown variant attribute for #[derive(GraphQLEnum)]: {:?}",
item item
)); ));
} }
@ -151,14 +165,21 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> TokenStream {
None => quote!{ None }, None => quote!{ None },
}; };
let depr = match var_attrs.deprecation { let depr = match var_attrs.deprecation {
Some(s) => quote!{ Some(#s.to_string()) }, Some(DeprecationAttr { reason: Some(s) }) => quote!{
None => quote!{ None }, _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!{ values.extend(quote!{
_juniper::meta::EnumValue{ _juniper::meta::EnumValue{
name: #name.to_string(), name: #name.to_string(),
description: #descr, description: #descr,
deprecation_reason: #depr, deprecation_status: #depr,
}, },
}); });

View file

@ -47,7 +47,7 @@ impl ObjAttrs {
continue; continue;
} }
panic!(format!( panic!(format!(
"Unknown object attribute for #[derive(GraphQLObject)]: {:?}", "Unknown struct attribute for #[derive(GraphQLObject)]: {:?}",
item item
)); ));
} }
@ -60,7 +60,7 @@ impl ObjAttrs {
struct ObjFieldAttrs { struct ObjFieldAttrs {
name: Option<String>, name: Option<String>,
description: Option<String>, description: Option<String>,
deprecation: Option<String>, deprecation: Option<DeprecationAttr>,
skip: bool, skip: bool,
} }
@ -71,6 +71,9 @@ impl ObjFieldAttrs {
// Check doc comments for description. // Check doc comments for description.
res.description = get_doc_comment(&variant.attrs); res.description = get_doc_comment(&variant.attrs);
// Check builtin deprecated attribute for deprecation.
res.deprecation = get_deprecated(&variant.attrs);
// Check attributes. // Check attributes.
if let Some(items) = get_graphql_attr(&variant.attrs) { if let Some(items) = get_graphql_attr(&variant.attrs) {
for item in items { for item in items {
@ -96,9 +99,20 @@ impl ObjFieldAttrs {
if let Some(AttributeValue::String(val)) = if let Some(AttributeValue::String(val)) =
keyed_item_value(&item, "deprecation", AttributeValidation::String) keyed_item_value(&item, "deprecation", AttributeValidation::String)
{ {
res.deprecation = Some(val); res.deprecation = Some(DeprecationAttr { reason: Some(val) });
continue; 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) { if let Some(_) = keyed_item_value(&item, "skip", AttributeValidation::Bare) {
res.skip = true; res.skip = true;
continue; continue;
@ -167,7 +181,8 @@ pub fn impl_object(ast: &syn::DeriveInput) -> TokenStream {
}; };
let build_deprecation = match field_attrs.deprecation { 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 }, None => quote!{ field },
}; };

View file

@ -1,5 +1,5 @@
use regex::Regex; use regex::Regex;
use syn::{Attribute, Lit, Meta, MetaNameValue, NestedMeta}; use syn::{Attribute, Lit, Meta, MetaList, MetaNameValue, NestedMeta};
pub enum AttributeValidation { pub enum AttributeValidation {
Any, Any,
@ -12,6 +12,42 @@ pub enum AttributeValue {
String(String), String(String),
} }
pub struct DeprecationAttr {
pub reason: Option<String>,
}
pub fn get_deprecated(attrs: &Vec<Attribute>) -> Option<DeprecationAttr> {
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. // Gets doc comment.
pub fn get_doc_comment(attrs: &Vec<Attribute>) -> Option<String> { pub fn get_doc_comment(attrs: &Vec<Attribute>) -> Option<String> {
if let Some(items) = get_doc_attr(attrs) { if let Some(items) = get_doc_attr(attrs) {

View file

@ -78,7 +78,7 @@ impl<'a> GraphQLType for &'a Fake {
&[juniper::meta::EnumValue { &[juniper::meta::EnumValue {
name: "fake".to_string(), name: "fake".to_string(),
description: None, description: None,
deprecation_reason: None, deprecation_status: juniper::meta::DeprecationStatus::Current,
}], }],
); );
meta.into_meta() meta.into_meta()