From 8426c97cfa3c82bf2fc4d098d2739dc889881f06 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Sun, 26 May 2019 11:09:27 +0200 Subject: [PATCH 1/3] (codegen) Implement newtype derive for scalars. This commit implements a newtype style custom derive for scalars via `#[derive(GraphQLScalarValue)]`, which now supports both deriving a base enum scalar type and newtypes. For newtypes, the `#[graphql(transparent)]` attribute is required. This commit: * implements the derive * adds integration tests * updates the book --- docs/book/content/types/scalars.md | 146 ++++++++++---- docs/book/tests/Cargo.toml | 5 +- .../src/codegen/derive_input_object.rs | 1 - .../juniper_tests/src/codegen/mod.rs | 1 + .../src/codegen/scalar_value_transparent.rs | 76 +++++++ juniper/CHANGELOG.md | 18 ++ juniper_codegen/src/derive_scalar_value.rs | 190 +++++++++++++++++- juniper_codegen/src/lib.rs | 46 ++++- 8 files changed, 436 insertions(+), 47 deletions(-) create mode 100644 integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs diff --git a/docs/book/content/types/scalars.md b/docs/book/content/types/scalars.md index 35d07bf7c..52d227e06 100644 --- a/docs/book/content/types/scalars.md +++ b/docs/book/content/types/scalars.md @@ -6,59 +6,131 @@ but this often requires coordination with the client library intended to consume the API you're building. Since any value going over the wire is eventually transformed into JSON, you're -also limited in the data types you can use. Typically, you represent your custom -scalars as strings. +also limited in the data types you can use. -In Juniper, you use the `graphql_scalar!` macro to create a custom scalar. In -this example, we're representing a user ID as a string wrapped in a custom type: +There are two ways to define custom scalars. +* For simple scalars that just wrap a primitive type, you can use the newtype pattern with +a custom derive. +* For more advanced use cases with custom validation, you can use +the `graphql_scalar!` macro. -```rust -use juniper::Value; -struct UserID(String); +## Built-in scalars -juniper::graphql_scalar!(UserID where Scalar = { - description: "An opaque identifier, represented as a string" +Juniper has built-in support for: - resolve(&self) -> Value { - Value::scalar(self.0.clone()) - } +* `i32` as `Int` +* `f64` as `Float` +* `String` and `&str` as `String` +* `bool` as `Boolean` +* `juniper::ID` as `ID`. This type is defined [in the + spec](http://facebook.github.io/graphql/#sec-ID) as a type that is serialized + as a string but can be parsed from both a string and an integer. - from_input_value(v: &InputValue) -> Option { - // If there's a parse error here, simply return None. Juniper will - // present an error to the client. - v.as_scalar_value::().map(|s| UserID(s.to_owned())) - } +**Third party types**: - from_str<'a>(value: ScalarToken<'a>) -> juniper::ParseScalarResult<'a, S> { - >::from_str(value) - } -}); +Juniper has built-in support for a few additional types from common third party +crates. They are enabled via features that are on by default. + +* uuid::Uuid +* chrono::DateTime +* url::Url + +## newtype pattern + +Often, you might need a custom scalar that just wraps an existing type. + +This can be done with the newtype pattern and a custom derive, similar to how +serde supports this pattern with `#[transparent]`. + +```rust +#[derive(juniper::GraphQLScalarValue)] +#[graphql(transparent)] +pub struct UserId(i32); #[derive(juniper::GraphQLObject)] struct User { - id: UserID, - username: String, + id: UserId, } # fn main() {} ``` -## Built-in scalars +That's it, you can now user `UserId` in your schema. -Juniper has built-in support for: +The macro also allows for more customization: -* `i32` as `Int` -* `f64` as `Float` -* `String` and `&str` as `String` -* `bool` as `Boolean` -* `juniper::ID` as `ID`. This type is defined [in the - spec](http://facebook.github.io/graphql/#sec-ID) as a type that is serialized - as a string but can be parsed from both a string and an integer. +```rust +/// You can use a doc comment to specify a description. +#[derive(juniper::GraphQLScalarValue)] +#[graphql( + transparent, + // Overwrite the GraphQL type name. + name = "MyUserId", + // Specify a custom description. + // A description in the attribute will overwrite a doc comment. + description = "My user id description", +)] +pub struct UserId(i32); + +# fn main() {} +``` + +## Custom scalars + +For more complex situations where you also need custom parsing or validation, +you can use the `graphql_scalar!` macro. + +Typically, you represent your custom scalars as strings. + +The example below implements a custom scalar for a custom `Date` type. + +Note: juniper already has built-in support for the `chrono::DateTime` type +via `chrono` feature, which is enabled by default and should be used for this +purpose. -### Non-standard scalars +The example below is used just for illustration. -Juniper has built-in support for UUIDs from the [uuid -crate](https://doc.rust-lang.org/uuid/uuid/index.html). This support is enabled -by default, but can be disabled if you want to reduce the number of dependencies -in your application. +**Note**: the example assumes that the `Date` type implements +`std::fmt::Display` and `std::str::FromStr`. + + +```rust +# mod date { +# pub struct Date; +# impl std::str::FromStr for Date{ +# type Err = String; fn from_str(_value: &str) -> Result { unimplemented!() } +# } +# // And we define how to represent date as a string. +# impl std::fmt::Display for Date { +# fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result { +# unimplemented!() +# } +# } +# } + +use juniper::{Value, ParseScalarResult, ParseScalarValue}; +use date::Date; + +juniper::graphql_scalar!(Date where Scalar = { + description: "Date" + + // Define how to convert your custom scalar into a primitive type. + resolve(&self) -> Value { + Value::scalar(self.to_string()) + } + + // Define how to parse a primitive type into your custom scalar. + from_input_value(v: &InputValue) -> Option { + v.as_scalar_value::() + .and_then(|s| s.parse().ok()) + } + + // Define how to parse a string value. + from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> { + >::from_str(value) + } +}); + +# fn main() {} +``` diff --git a/docs/book/tests/Cargo.toml b/docs/book/tests/Cargo.toml index 8f49bca3b..e642b42a8 100644 --- a/docs/book/tests/Cargo.toml +++ b/docs/book/tests/Cargo.toml @@ -9,11 +9,12 @@ build = "build.rs" juniper = { path = "../../../juniper" } juniper_iron = { path = "../../../juniper_iron" } -iron = "^0.5.0" -mount = "^0.3.0" +iron = "0.5.0" +mount = "0.4.0" skeptic = "0.13" serde_json = "1.0.39" +uuid = "0.7.4" [build-dependencies] skeptic = "0.13" diff --git a/integration_tests/juniper_tests/src/codegen/derive_input_object.rs b/integration_tests/juniper_tests/src/codegen/derive_input_object.rs index 938446eff..039e99edf 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_input_object.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_input_object.rs @@ -1,4 +1,3 @@ -#[cfg(test)] use fnv::FnvHashMap; use juniper::DefaultScalarValue; diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index ac20e205b..498258bd7 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -3,3 +3,4 @@ mod util; mod derive_enum; mod derive_input_object; mod derive_object; +mod scalar_value_transparent; diff --git a/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs b/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs new file mode 100644 index 000000000..886850cd1 --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs @@ -0,0 +1,76 @@ +use fnv::FnvHashMap; +use juniper::{DefaultScalarValue, FromInputValue, GraphQLType, InputValue, ToInputValue}; + +#[derive(juniper::GraphQLScalarValue, PartialEq, Eq, Debug)] +#[graphql(transparent)] +struct UserId(String); + +#[derive(juniper::GraphQLScalarValue, PartialEq, Eq, Debug)] +#[graphql(transparent, name = "MyUserId", description = "custom description...")] +struct CustomUserId(String); + +/// The doc comment... +#[derive(juniper::GraphQLScalarValue, PartialEq, Eq, Debug)] +#[graphql(transparent)] +struct IdWithDocComment(i32); + +#[derive(juniper::GraphQLObject)] +struct User { + id: UserId, + id_custom: CustomUserId, +} + +struct User2; + +#[juniper::object] +impl User2 { + fn id(&self) -> UserId { + UserId("id".to_string()) + } +} + +#[test] +fn test_scalar_value_simple() { + assert_eq!(::name(&()), Some("UserId")); + + let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default()); + let meta = UserId::meta(&(), &mut registry); + assert_eq!(meta.name(), Some("UserId")); + assert_eq!(meta.description(), None); + + let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap(); + let output: UserId = FromInputValue::from_input_value(&input).unwrap(); + assert_eq!(output, UserId("userId1".into()),); + + let id = UserId("111".into()); + let output = ToInputValue::::to_input_value(&id); + assert_eq!(output, InputValue::scalar("111"),); +} + +#[test] +fn test_scalar_value_custom() { + assert_eq!(::name(&()), Some("MyUserId")); + + let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default()); + let meta = CustomUserId::meta(&(), &mut registry); + assert_eq!(meta.name(), Some("MyUserId")); + assert_eq!( + meta.description(), + Some(&"custom description...".to_string()) + ); + + let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap(); + let output: CustomUserId = FromInputValue::from_input_value(&input).unwrap(); + assert_eq!(output, CustomUserId("userId1".into()),); + + let id = CustomUserId("111".into()); + let output = ToInputValue::::to_input_value(&id); + assert_eq!(output, InputValue::scalar("111"),); +} + +#[test] +fn test_scalar_value_doc_comment() { + let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default()); + let meta = IdWithDocComment::meta(&(), &mut registry); + assert_eq!(meta.description(), Some(&"The doc comment...".to_string())); +} diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 0b245d268..526b94f8c 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -1,5 +1,23 @@ # master +### newtype ScalarValue derive + +See [#345](https://github.com/graphql-rust/juniper/pull/345). + +The newtype pattern can now be used with the `GraphQLScalarValue` custom derive +to easily implement custom scalar values that just wrap another scalar, +similar to serdes `#[transparent]` functionality. + +Example: + +```rust +#[derive(juniper::GraphQLScalarValue)] +#[graphql(transparent)] +struct UserId(i32); +``` + +### Other Changes + - The `ID` scalar now implements Serde's `Serialize` and `Deserialize` # [[0.12.0] 2019-05-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.12.0) diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index f969fb3f4..61176347b 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -3,25 +3,203 @@ use proc_macro2::TokenStream; use quote::quote; use syn::{self, Data, Fields, Ident, Variant}; +use crate::util; + +#[derive(Debug)] +struct TransparentAttributes { + transparent: bool, + name: Option, + description: Option, +} + +impl syn::parse::Parse for TransparentAttributes { + fn parse(input: syn::parse::ParseStream) -> syn::parse::Result { + let mut output = Self { + transparent: false, + name: None, + description: None, + }; + + let mut content; + syn::parenthesized!(content in input); + + while !content.is_empty() { + let ident: syn::Ident = content.parse()?; + match ident.to_string().as_str() { + "name" => { + content.parse::()?; + let val = content.parse::()?; + output.name = Some(val.value()); + } + "description" => { + content.parse::()?; + let val = content.parse::()?; + output.description = Some(val.value()); + } + "transparent" => { + output.transparent = true; + } + other => { + return Err(content.error(format!("Unknown attribute: {}", other))); + } + } + if content.lookahead1().peek(syn::Token![,]) { + content.parse::()?; + } + } + + Ok(output) + } +} + +impl TransparentAttributes { + fn from_attrs(attrs: &Vec) -> syn::parse::Result { + match util::find_graphql_attr(attrs) { + Some(attr) => { + let mut parsed: TransparentAttributes = syn::parse(attr.tts.clone().into())?; + if parsed.description.is_none() { + parsed.description = util::get_doc_comment(attrs); + } + Ok(parsed) + } + None => { + panic!("Missing required attribute: #[graphql(transparent)]"); + } + } + } +} + pub fn impl_scalar_value(ast: &syn::DeriveInput, is_internal: bool) -> TokenStream { let ident = &ast.ident; - let variants = match ast.data { - Data::Enum(ref enum_data) => &enum_data.variants, + match ast.data { + Data::Enum(ref enum_data) => impl_scalar_enum(ident, enum_data, is_internal), + Data::Struct(ref struct_data) => impl_scalar_struct(ast, struct_data, is_internal), + Data::Union(_) => { + panic!("#[derive(GraphQLScalarValue)] may not be applied to unions"); + } + } +} + +fn impl_scalar_struct( + ast: &syn::DeriveInput, + data: &syn::DataStruct, + is_internal: bool, +) -> TokenStream { + let field = match data.fields { + syn::Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => { + fields.unnamed.first().unwrap().into_value() + } _ => { - panic!("#[derive(GraphQLScalarValue)] may only be applied to enums, not to structs"); + panic!("#[derive(GraphQLScalarValue)] may only be applied to enums or tuple structs with a single field"); } }; + let ident = &ast.ident; + let attrs = match TransparentAttributes::from_attrs(&ast.attrs) { + Ok(attrs) => attrs, + Err(e) => { + panic!("Invalid #[graphql] attribute: {}", e); + } + }; + if !(attrs.transparent) { + panic!("Deriving GraphQLScalarValue on a tuple struct requires a #[graphql(transparent) attribute"); + } + let inner_ty = &field.ty; + let name = attrs.name.unwrap_or(ident.to_string()); + + let crate_name = if is_internal { + quote!(crate) + } else { + quote!(juniper) + }; + + let description = match attrs.description { + Some(val) => quote!( .description( #val ) ), + None => quote!(), + }; + + quote!( + impl #crate_name::GraphQLType for #ident + where + S: #crate_name::ScalarValue, + for<'__b> &'__b S: #crate_name::ScalarRefValue<'__b>, + { + type Context = (); + type TypeInfo = (); + + fn name(_: &Self::TypeInfo) -> Option<&str> { + Some(#name) + } + + fn meta<'r>( + info: &Self::TypeInfo, + registry: &mut #crate_name::Registry<'r, S>, + ) -> #crate_name::meta::MetaType<'r, S> + where + for<'__b> &'__b S: #crate_name::ScalarRefValue<'__b>, + S: 'r, + { + registry.build_scalar_type::(info) + #description + .into_meta() + } + + fn resolve( + &self, + info: &(), + selection: Option<&[#crate_name::Selection]>, + executor: &#crate_name::Executor, + ) -> #crate_name::Value { + #crate_name::GraphQLType::resolve(&self.0, info, selection, executor) + } + } + + impl #crate_name::ToInputValue for #ident + where + S: #crate_name::ScalarValue, + for<'__b> &'__b S: #crate_name::ScalarRefValue<'__b>, + { + fn to_input_value(&self) -> #crate_name::InputValue { + #crate_name::ToInputValue::to_input_value(&self.0) + } + } + + impl #crate_name::FromInputValue for #ident + where + S: #crate_name::ScalarValue, + for<'__b> &'__b S: #crate_name::ScalarRefValue<'__b>, + { + fn from_input_value(v: &#crate_name::InputValue) -> Option<#ident> { + let inner: #inner_ty = #crate_name::FromInputValue::from_input_value(v)?; + Some(#ident(inner)) + } + } + + impl #crate_name::ParseScalarValue for #ident + where + S: #crate_name::ScalarValue, + for<'__b> &'__b S: #crate_name::ScalarRefValue<'__b>, + { + fn from_str<'a>( + value: #crate_name::parser::ScalarToken<'a>, + ) -> #crate_name::ParseScalarResult<'a, S> { + <#inner_ty as #crate_name::ParseScalarValue>::from_str(value) + } + } + ) +} - let froms = variants +fn impl_scalar_enum(ident: &syn::Ident, data: &syn::DataEnum, is_internal: bool) -> TokenStream { + let froms = data + .variants .iter() .map(|v| derive_from_variant(v, ident)) .collect::, String>>() .unwrap_or_else(|s| panic!("{}", s)); - let serialize = derive_serialize(variants.iter(), ident, is_internal); + let serialize = derive_serialize(data.variants.iter(), ident, is_internal); - let display = derive_display(variants.iter(), ident); + let display = derive_display(data.variants.iter(), ident); quote! { #(#froms)* diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 3c9ec907c..00b1028ef 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -55,7 +55,51 @@ pub fn derive_object(input: TokenStream) -> TokenStream { gen.into() } -#[proc_macro_derive(GraphQLScalarValue)] +/// This custom derive macro implements the #[derive(GraphQLScalarValue)] +/// derive. +/// +/// This can be used for two purposes. +/// +/// ## Transparent Newtype Wrapper +/// +/// Sometimes, you want to create a custerm scalar type by wrapping +/// an existing type. In Rust, this is often called the "newtype" pattern. +/// Thanks to this custom derive, this becomes really easy: +/// +/// ```rust +/// // Deriving GraphQLScalar is all that is required. +/// // Note the #[graphql(transparent)] attribute, which is mandatory. +/// #[derive(juniper::GraphQLScalarValue)] +/// #[graphql(transparent)] +/// struct UserId(String); +/// +/// #[derive(juniper::GraphQLObject)] +/// struct User { +/// id: UserId, +/// } +/// ``` +/// +/// The type can also be customized. +/// +/// ```rust +/// /// Doc comments are used for the GraphQL type description. +/// #[derive(juniper::GraphQLScalarValue)] +/// #[graphql( +/// transparent, +/// // Set a custom GraphQL name. +/// name= "MyUserId", +/// // A description can also specified in the attribute. +/// // This will the doc comment, if one exists. +/// description = "...", +/// )] +/// struct UserId(String); +/// ``` +/// +/// ### Base ScalarValue Enum +/// +/// TODO: write documentation. +/// +#[proc_macro_derive(GraphQLScalarValue, attributes(graphql))] pub fn derive_scalar_value(input: TokenStream) -> TokenStream { let ast = syn::parse::(input).unwrap(); let gen = derive_scalar_value::impl_scalar_value(&ast, false); From 93eebff21d99bb2c3d60e704ef957071599ca554 Mon Sep 17 00:00:00 2001 From: theduke Date: Tue, 18 Jun 2019 20:31:04 +0200 Subject: [PATCH 2/3] Allow scalars without #[repr(transparent)] --- juniper_codegen/src/derive_scalar_value.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index 61176347b..43a2d2747 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -101,9 +101,6 @@ fn impl_scalar_struct( panic!("Invalid #[graphql] attribute: {}", e); } }; - if !(attrs.transparent) { - panic!("Deriving GraphQLScalarValue on a tuple struct requires a #[graphql(transparent) attribute"); - } let inner_ty = &field.ty; let name = attrs.name.unwrap_or(ident.to_string()); From 0268d0347aaf97d61a2324506e5322f224f18aa5 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Tue, 25 Jun 2019 13:35:31 +0200 Subject: [PATCH 3/3] Clean up ScalarValue transparent derive argument handling and documentation. --- docs/book/content/types/scalars.md | 3 +-- juniper/CHANGELOG.md | 3 +-- juniper_codegen/src/derive_scalar_value.rs | 12 +++++------- juniper_codegen/src/lib.rs | 2 -- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/docs/book/content/types/scalars.md b/docs/book/content/types/scalars.md index 52d227e06..7ab9a1e67 100644 --- a/docs/book/content/types/scalars.md +++ b/docs/book/content/types/scalars.md @@ -41,11 +41,10 @@ crates. They are enabled via features that are on by default. Often, you might need a custom scalar that just wraps an existing type. This can be done with the newtype pattern and a custom derive, similar to how -serde supports this pattern with `#[transparent]`. +serde supports this pattern with `#[serde(transparent)]`. ```rust #[derive(juniper::GraphQLScalarValue)] -#[graphql(transparent)] pub struct UserId(i32); #[derive(juniper::GraphQLObject)] diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 526b94f8c..9424ea274 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -6,13 +6,12 @@ See [#345](https://github.com/graphql-rust/juniper/pull/345). The newtype pattern can now be used with the `GraphQLScalarValue` custom derive to easily implement custom scalar values that just wrap another scalar, -similar to serdes `#[transparent]` functionality. +similar to serdes `#[serde(transparent)]` functionality. Example: ```rust #[derive(juniper::GraphQLScalarValue)] -#[graphql(transparent)] struct UserId(i32); ``` diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index 43a2d2747..27bf14e81 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -5,9 +5,9 @@ use syn::{self, Data, Fields, Ident, Variant}; use crate::util; -#[derive(Debug)] +#[derive(Debug, Default)] struct TransparentAttributes { - transparent: bool, + transparent: Option, name: Option, description: Option, } @@ -15,7 +15,7 @@ struct TransparentAttributes { impl syn::parse::Parse for TransparentAttributes { fn parse(input: syn::parse::ParseStream) -> syn::parse::Result { let mut output = Self { - transparent: false, + transparent: None, name: None, description: None, }; @@ -37,7 +37,7 @@ impl syn::parse::Parse for TransparentAttributes { output.description = Some(val.value()); } "transparent" => { - output.transparent = true; + output.transparent = Some(true); } other => { return Err(content.error(format!("Unknown attribute: {}", other))); @@ -62,9 +62,7 @@ impl TransparentAttributes { } Ok(parsed) } - None => { - panic!("Missing required attribute: #[graphql(transparent)]"); - } + None => Ok(Default::default()), } } } diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 00b1028ef..f578ce311 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -68,9 +68,7 @@ pub fn derive_object(input: TokenStream) -> TokenStream { /// /// ```rust /// // Deriving GraphQLScalar is all that is required. -/// // Note the #[graphql(transparent)] attribute, which is mandatory. /// #[derive(juniper::GraphQLScalarValue)] -/// #[graphql(transparent)] /// struct UserId(String); /// /// #[derive(juniper::GraphQLObject)]