From 14e3b4fe69e6f4c1a67e2f0019713bb27f1abc6f Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Sun, 10 May 2020 20:03:13 -0700 Subject: [PATCH 1/4] Change interfaces to require enums --- async-graphql-derive/src/interface.rs | 116 ++++++++++++++++---------- 1 file changed, 70 insertions(+), 46 deletions(-) diff --git a/async-graphql-derive/src/interface.rs b/async-graphql-derive/src/interface.rs index 424dfd01..1c4cd195 100644 --- a/async-graphql-derive/src/interface.rs +++ b/async-graphql-derive/src/interface.rs @@ -6,26 +6,25 @@ use inflector::Inflector; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; use quote::quote; +use std::collections::HashSet; use syn::{Data, DeriveInput, Error, Fields, Result, Type}; pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result { let crate_name = get_crate_name(interface_args.internal); let ident = &input.ident; let generics = &input.generics; - let attrs = &input.attrs; - let vis = &input.vis; let s = match &input.data { - Data::Struct(s) => s, - _ => return Err(Error::new_spanned(input, "It should be a struct.")), - }; - let fields = match &s.fields { - Fields::Unnamed(fields) => Some(&fields.unnamed), - Fields::Unit => None, - _ => return Err(Error::new_spanned(input, "All fields must be unnamed.")), + Data::Enum(s) => s, + _ => { + return Err(Error::new_spanned( + input, + "Interfaces can only be applied to an enum.", + )) + } }; let extends = interface_args.extends; let mut enum_names = Vec::new(); - let mut enum_items = Vec::new(); + let mut enum_items = HashSet::new(); let mut type_into_impls = Vec::new(); let gql_typename = interface_args .name @@ -45,41 +44,67 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result let mut collect_inline_fields = Vec::new(); let mut get_introspection_typename = Vec::new(); - if let Some(fields) = fields { - for field in fields { - if let Type::Path(p) = &field.ty { - let enum_name = &p.path.segments.last().unwrap().ident; - enum_items.push(quote! { #enum_name(#p) }); - type_into_impls.push(quote! { - impl #generics From<#p> for #ident #generics { - fn from(obj: #p) -> Self { - #ident::#enum_name(obj) - } - } - }); - enum_names.push(enum_name); - - registry_types.push(quote! { - <#p as #crate_name::Type>::create_type_info(registry); - registry.add_implements(&<#p as #crate_name::Type>::type_name(), #gql_typename); - }); - - possible_types.push(quote! { - possible_types.insert(<#p as #crate_name::Type>::type_name().to_string()); - }); - - collect_inline_fields.push(quote! { - if let #ident::#enum_name(obj) = self { - return obj.collect_inline_fields(name, ctx, futures); - } - }); - - get_introspection_typename.push(quote! { - #ident::#enum_name(obj) => <#p as #crate_name::Type>::type_name() - }) - } else { - return Err(Error::new_spanned(field, "Invalid type")); + for variant in s.variants.iter() { + let enum_name = &variant.ident; + let field = match &variant.fields { + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => fields.unnamed.first().unwrap(), + Fields::Unnamed(_) => { + return Err(Error::new_spanned( + variant, + "Only single value variants are supported", + )) } + Fields::Unit => { + return Err(Error::new_spanned( + variant, + "Empty variants are not supported", + )) + } + Fields::Named(_) => { + return Err(Error::new_spanned( + variant, + "Variants with named fields are not supported", + )) + } + }; + if let Type::Path(p) = &field.ty { + // This validates that the field type wasn't already used + if enum_items.insert(p) == false { + return Err(Error::new_spanned( + field, + "This type already used in another variant", + )); + } + + type_into_impls.push(quote! { + impl #generics From<#p> for #ident #generics { + fn from(obj: #p) -> Self { + #ident::#enum_name(obj) + } + } + }); + enum_names.push(enum_name); + + registry_types.push(quote! { + <#p as #crate_name::Type>::create_type_info(registry); + registry.add_implements(&<#p as #crate_name::Type>::type_name(), #gql_typename); + }); + + possible_types.push(quote! { + possible_types.insert(<#p as #crate_name::Type>::type_name().to_string()); + }); + + collect_inline_fields.push(quote! { + if let #ident::#enum_name(obj) = self { + return obj.collect_inline_fields(name, ctx, futures); + } + }); + + get_introspection_typename.push(quote! { + #ident::#enum_name(obj) => <#p as #crate_name::Type>::type_name() + }) + } else { + return Err(Error::new_spanned(field, "Invalid type")); } } @@ -236,8 +261,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result }; let expanded = quote! { - #(#attrs)* - #vis enum #ident #generics { #(#enum_items),* } + #input #(#type_into_impls)* From 9483ff14be4cb2490003e34da5648405f9d47a7f Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Sun, 10 May 2020 20:25:49 -0700 Subject: [PATCH 2/4] Fix tests --- src/lib.rs | 11 +++++++++-- src/validation/test_harness.rs | 21 +++++++++++++++++---- tests/interface.rs | 29 ++++++++++++++++++++++------- tests/subscription.rs | 8 ++++++-- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9be5d353..07ca4b81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -447,7 +447,12 @@ pub use async_graphql_derive::InputObject; /// /// ```ignore /// #[Interface] -/// struct MyInterface(TypeA, TypeB, TypeC, ...); +/// enum MyInterface { +/// TypeA(TypeA), +/// TypeB(TypeB), +/// TypeC(TypeC), +/// ... +/// } /// ``` /// /// # Fields @@ -487,7 +492,9 @@ pub use async_graphql_derive::InputObject; /// arg(name = "a", type = "i32"), /// arg(name = "b", type = "i32")), /// )] -/// struct MyInterface(TypeA); +/// enum MyInterface { +/// TypeA(TypeA) +/// } /// /// struct QueryRoot; /// diff --git a/src/validation/test_harness.rs b/src/validation/test_harness.rs index ee0f0bb4..f04a4877 100644 --- a/src/validation/test_harness.rs +++ b/src/validation/test_harness.rs @@ -140,7 +140,12 @@ struct HumanOrAlien(Human, Alien); arg(name = "surname", type = "Option") ) )] -struct Being(Dog, Cat, Human, Alien); +enum Being { + Dog(Dog), + Cat(Cat), + Human(Human), + Alien(Alien), +} #[Interface( internal, @@ -150,7 +155,10 @@ struct Being(Dog, Cat, Human, Alien); arg(name = "surname", type = "Option") ) )] -struct Pet(Dog, Cat); +enum Pet { + Dog(Dog), + Cat(Cat), +} #[Interface( internal, @@ -160,10 +168,15 @@ struct Pet(Dog, Cat); arg(name = "surname", type = "Option") ) )] -struct Canine(Dog); +enum Canine { + Dog(Dog), +} #[Interface(internal, field(name = "iq", type = "Option"))] -struct Intelligent(Human, Alien); +enum Intelligent { + Human(Human), + Alien(Alien), +} #[InputObject(internal)] struct ComplexInput { diff --git a/tests/interface.rs b/tests/interface.rs index 66b0ef59..e9386074 100644 --- a/tests/interface.rs +++ b/tests/interface.rs @@ -9,7 +9,9 @@ pub async fn test_interface_simple_object() { } #[async_graphql::Interface(field(name = "id", type = "i32"))] - struct Node(MyObj); + enum Node { + MyObj(MyObj), + } struct Query; @@ -52,7 +54,9 @@ pub async fn test_interface_simple_object2() { } #[async_graphql::Interface(field(name = "id", type = "&i32"))] - struct Node(MyObj); + enum Node { + MyObj(MyObj), + } struct Query; @@ -105,10 +109,14 @@ pub async fn test_multiple_interfaces() { } #[async_graphql::Interface(field(name = "value_a", type = "i32"))] - struct InterfaceA(MyObj); + enum InterfaceA { + MyObj(MyObj), + } #[async_graphql::Interface(field(name = "value_b", type = "i32"))] - struct InterfaceB(MyObj); + enum InterfaceB { + MyObj(MyObj), + } struct Query; @@ -176,10 +184,15 @@ pub async fn test_multiple_objects_in_multiple_interfaces() { } #[async_graphql::Interface(field(name = "value_a", type = "i32"))] - struct InterfaceA(MyObjOne, MyObjTwo); + enum InterfaceA { + MyObjOne(MyObjOne), + MyObjTwo(MyObjTwo), + } #[async_graphql::Interface(field(name = "value_b", type = "i32"))] - struct InterfaceB(MyObjOne); + enum InterfaceB { + MyObjOne(MyObjOne), + } struct Query; @@ -232,7 +245,9 @@ pub async fn test_interface_field_result() { } #[async_graphql::Interface(field(name = "value", type = "FieldResult"))] - struct Node(MyObj); + enum Node { + MyObj(MyObj), + } struct Query; diff --git a/tests/subscription.rs b/tests/subscription.rs index 0c7df9c5..0b010701 100644 --- a/tests/subscription.rs +++ b/tests/subscription.rs @@ -325,7 +325,9 @@ pub async fn test_subscription_fragment() { } #[Interface(field(name = "a", type = "i32"))] - struct MyInterface(Event); + enum MyInterface { + Event(Event), + } #[Object] impl QueryRoot {} @@ -380,7 +382,9 @@ pub async fn test_subscription_fragment2() { } #[Interface(field(name = "a", type = "i32"))] - struct MyInterface(Event); + enum MyInterface { + Event(Event), + } #[Object] impl QueryRoot {} From 548447cdecd2bee0eb4ac821efb5c3a9a70b5ffb Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Sun, 10 May 2020 21:27:30 -0700 Subject: [PATCH 3/4] Change Unions to enums instead of structs --- async-graphql-derive/src/union.rs | 55 ++++-- src/lib.rs | 63 ++++++- src/validation/test_harness.rs | 15 +- tests/union.rs | 287 ++++++++++++++++++++++++++++++ 4 files changed, 402 insertions(+), 18 deletions(-) create mode 100644 tests/union.rs diff --git a/async-graphql-derive/src/union.rs b/async-graphql-derive/src/union.rs index a0168d06..fcb653c1 100644 --- a/async-graphql-derive/src/union.rs +++ b/async-graphql-derive/src/union.rs @@ -2,24 +2,24 @@ use crate::args; use crate::utils::{check_reserved_name, get_crate_name, get_rustdoc}; use proc_macro::TokenStream; use quote::quote; +use std::collections::HashSet; use syn::{Data, DeriveInput, Error, Fields, Result, Type}; pub fn generate(union_args: &args::Interface, input: &DeriveInput) -> Result { let crate_name = get_crate_name(union_args.internal); let ident = &input.ident; let generics = &input.generics; - let attrs = &input.attrs; - let vis = &input.vis; let s = match &input.data { - Data::Struct(s) => s, - _ => return Err(Error::new_spanned(input, "It should be a struct.")), - }; - let fields = match &s.fields { - Fields::Unnamed(fields) => fields, - _ => return Err(Error::new_spanned(input, "All fields must be unnamed.")), + Data::Enum(s) => s, + _ => { + return Err(Error::new_spanned( + input, + "Unions can only be applied to an enum.", + )) + } }; let mut enum_names = Vec::new(); - let mut enum_items = Vec::new(); + let mut enum_items = HashSet::new(); let mut type_into_impls = Vec::new(); let gql_typename = union_args.name.clone().unwrap_or_else(|| ident.to_string()); check_reserved_name(&gql_typename, union_args.internal)?; @@ -36,11 +36,39 @@ pub fn generate(union_args: &args::Interface, input: &DeriveInput) -> Result fields.unnamed.first().unwrap(), + Fields::Unnamed(_) => { + return Err(Error::new_spanned( + variant, + "Only single value variants are supported", + )) + } + Fields::Unit => { + return Err(Error::new_spanned( + variant, + "Empty variants are not supported", + )) + } + Fields::Named(_) => { + return Err(Error::new_spanned( + variant, + "Variants with named fields are not supported", + )) + } + }; if let Type::Path(p) = &field.ty { - let enum_name = &p.path.segments.last().unwrap().ident; + // This validates that the field type wasn't already used + if enum_items.insert(p) == false { + return Err(Error::new_spanned( + field, + "This type already used in another variant", + )); + } + enum_names.push(enum_name); - enum_items.push(quote! { #enum_name(#p) }); type_into_impls.push(quote! { impl #generics From<#p> for #ident #generics { fn from(obj: #p) -> Self { @@ -68,8 +96,7 @@ pub fn generate(union_args: &args::Interface, input: &DeriveInput) -> Result Vec { +/// vec![TypeA { value_a: 10 }.into(), TypeB { value_b: 20 }.into()] +/// } +/// } +/// +/// #[async_std::main] +/// async fn main() { +/// let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).data("hello".to_string()).finish(); +/// let res = schema.execute(r#" +/// { +/// allData { +/// ... on TypeA { +/// valueA +/// } +/// ... on TypeB { +/// valueB +/// } +/// } +/// }"#).await.unwrap().data; +/// assert_eq!(res, serde_json::json!({ +/// "allData": [ +/// { "valueA": 10 }, +/// { "valueB": 20 }, +/// ] +/// })); +/// } +/// ``` pub use async_graphql_derive::Union; /// Define a GraphQL subscription diff --git a/src/validation/test_harness.rs b/src/validation/test_harness.rs index f04a4877..a4d74f26 100644 --- a/src/validation/test_harness.rs +++ b/src/validation/test_harness.rs @@ -86,7 +86,10 @@ impl Cat { } #[Union(internal)] -struct CatOrDog(Cat, Dog); +enum CatOrDog { + Cat(Cat), + Dog(Dog), +} struct Human; @@ -127,10 +130,16 @@ impl Alien { } #[Union(internal)] -struct DogOrHuman(Dog, Human); +enum DogOrHuman { + Dog(Dog), + Human(Human), +} #[Union(internal)] -struct HumanOrAlien(Human, Alien); +enum HumanOrAlien { + Human(Human), + Alien(Alien), +} #[Interface( internal, diff --git a/tests/union.rs b/tests/union.rs new file mode 100644 index 00000000..9b2d90c9 --- /dev/null +++ b/tests/union.rs @@ -0,0 +1,287 @@ +use async_graphql::*; + +#[async_std::test] +pub async fn test_union_simple_object() { + #[async_graphql::SimpleObject] + struct MyObj { + id: i32, + title: String, + } + + #[async_graphql::Union] + enum Node { + MyObj(MyObj), + } + + struct Query; + + #[Object] + impl Query { + async fn node(&self) -> Node { + MyObj { + id: 33, + title: "haha".to_string(), + } + .into() + } + } + + let query = r#"{ + node { + ... on MyObj { + id + } + } + }"#; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(&query).await.unwrap().data, + serde_json::json!({ + "node": { + "id": 33, + } + }) + ); +} + +#[async_std::test] +pub async fn test_union_simple_object2() { + #[async_graphql::SimpleObject] + struct MyObj { + #[field(ref)] + id: i32, + title: String, + } + + #[async_graphql::Union] + enum Node { + MyObj(MyObj), + } + + struct Query; + + #[Object] + impl Query { + async fn node(&self) -> Node { + MyObj { + id: 33, + title: "haha".to_string(), + } + .into() + } + } + + let query = r#"{ + node { + ... on MyObj { + id + } + } + }"#; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(&query).await.unwrap().data, + serde_json::json!({ + "node": { + "id": 33, + } + }) + ); +} + +#[async_std::test] +pub async fn test_multiple_unions() { + struct MyObj; + + #[async_graphql::Object] + impl MyObj { + async fn value_a(&self) -> i32 { + 1 + } + + async fn value_b(&self) -> i32 { + 2 + } + + async fn value_c(&self) -> i32 { + 3 + } + } + + #[async_graphql::Union] + enum UnionA { + MyObj(MyObj), + } + + #[async_graphql::Union] + enum UnionB { + MyObj(MyObj), + } + + struct Query; + + #[Object] + impl Query { + async fn union_a(&self) -> UnionA { + MyObj.into() + } + async fn union_b(&self) -> UnionB { + MyObj.into() + } + } + + let schema = Schema::build(Query, EmptyMutation, EmptySubscription) + .register_type::() // `UnionA` is not directly referenced, so manual registration is required. + .finish(); + let query = r#"{ + unionA { + ... on MyObj { + valueA + valueB + valueC + } + } + unionB { + ... on MyObj { + valueA + valueB + valueC + } + } + }"#; + assert_eq!( + schema.execute(&query).await.unwrap().data, + serde_json::json!({ + "unionA": { + "valueA": 1, + "valueB": 2, + "valueC": 3, + }, + "unionB": { + "valueA": 1, + "valueB": 2, + "valueC": 3, + } + }) + ); +} + +#[async_std::test] +pub async fn test_multiple_objects_in_multiple_unions() { + struct MyObjOne; + + #[async_graphql::Object] + impl MyObjOne { + async fn value_a(&self) -> i32 { + 1 + } + + async fn value_b(&self) -> i32 { + 2 + } + + async fn value_c(&self) -> i32 { + 3 + } + } + + struct MyObjTwo; + + #[async_graphql::Object] + impl MyObjTwo { + async fn value_a(&self) -> i32 { + 1 + } + } + + #[async_graphql::Union] + enum UnionA { + MyObjOne(MyObjOne), + MyObjTwo(MyObjTwo), + } + + #[async_graphql::Union] + enum UnionB { + MyObjOne(MyObjOne), + } + + struct Query; + + #[Object] + impl Query { + async fn my_obj(&self) -> Vec { + vec![MyObjOne.into(), MyObjTwo.into()] + } + } + + let schema = Schema::build(Query, EmptyMutation, EmptySubscription) + .register_type::() // `UnionB` is not directly referenced, so manual registration is required. + .finish(); + let query = r#"{ + myObj { + ... on MyObjTwo { + valueA + } + ... on MyObjOne { + valueA + valueB + valueC + } + } + }"#; + assert_eq!( + schema.execute(&query).await.unwrap().data, + serde_json::json!({ + "myObj": [{ + "valueA": 1, + "valueB": 2, + "valueC": 3, + }, { + "valueA": 1 + }] + }) + ); +} + +#[async_std::test] +pub async fn test_union_field_result() { + struct MyObj; + + #[async_graphql::Object] + impl MyObj { + async fn value(&self) -> FieldResult { + Ok(10) + } + } + + #[async_graphql::Union] + enum Node { + MyObj(MyObj), + } + + struct Query; + + #[Object] + impl Query { + async fn node(&self) -> Node { + MyObj.into() + } + } + + let query = r#"{ + node { + ... on MyObj { + value + } + } + }"#; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(&query).await.unwrap().data, + serde_json::json!({ + "node": { + "value": 10, + } + }) + ); +} From d19ab4c559fdcd4066538652e67b8f4a1ca093ca Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Sun, 10 May 2020 21:39:43 -0700 Subject: [PATCH 4/4] Update the books --- docs/en/src/define_interface.md | 13 ++++++++----- docs/en/src/define_union.md | 9 ++++++--- docs/zh-CN/src/define_interface.md | 7 +++++-- docs/zh-CN/src/define_union.md | 7 +++++-- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/en/src/define_interface.md b/docs/en/src/define_interface.md index 95d428ce..9642de98 100644 --- a/docs/en/src/define_interface.md +++ b/docs/en/src/define_interface.md @@ -1,8 +1,8 @@ # Interface -`Interface` is used to abstract `Object`s with common fields. -`Async-graphql` implemented it as a wrapper. -The wrapper will forward Resolve to the `Object` that implemented this `Interface`. +`Interface` is used to abstract `Object`s with common fields. +`Async-graphql` implemented it as a wrapper. +The wrapper will forward Resolve to the `Object` that implemented this `Interface`. Therefore, the `Object`'s fields' type, arguments must match with the `Interface`'s. `Async-graphql` implemented auto conversion from `Object` to `Interface`, you only need to call `Into::into`. @@ -44,5 +44,8 @@ impl Square { field(name = "area", type = "f32"), field(name = "scale", type = "Shape", arg(name = "s", type = "f32")) )] -struct Shape(Circle, Square); -``` \ No newline at end of file +enum Shape { + Circle(Circle), + Square(Square), +} +``` diff --git a/docs/en/src/define_union.md b/docs/en/src/define_union.md index 136ff959..7b58423a 100644 --- a/docs/en/src/define_union.md +++ b/docs/en/src/define_union.md @@ -1,7 +1,7 @@ # Union The definition of `Union` is similar to `Interface`'s, **but no field allowed.**. -The implemention is quite similar for `Async-graphql`. +The implemention is quite similar for `Async-graphql`. From `Async-graphql`'s perspective, `Union` is a subset of `Interface`. The following example modified the definition of `Interface` a little bit and removed fields. @@ -40,5 +40,8 @@ impl Square { } #[Union] -struct Shape(Circle, Square); -``` \ No newline at end of file +enum Shape { + Circle(Circle), + Square(Square), +} +``` diff --git a/docs/zh-CN/src/define_interface.md b/docs/zh-CN/src/define_interface.md index e98c0383..2aa82852 100644 --- a/docs/zh-CN/src/define_interface.md +++ b/docs/zh-CN/src/define_interface.md @@ -41,5 +41,8 @@ impl Square { field(name = "area", type = "f32"), field(name = "scale", type = "Shape", arg(name = "s", type = "f32")) )] -struct Shape(Circle, Square); -``` \ No newline at end of file +enum Shape { + Circle(Circle), + Square(Square), +} +``` diff --git a/docs/zh-CN/src/define_union.md b/docs/zh-CN/src/define_union.md index 42b382f2..6fc35965 100644 --- a/docs/zh-CN/src/define_union.md +++ b/docs/zh-CN/src/define_union.md @@ -38,5 +38,8 @@ impl Square { } #[Union] -struct Shape(Circle, Square); -``` \ No newline at end of file +enum Shape { + Circle(Circle), + Square(Square), +} +```