Merge pull request #75 from phated/interface-enums

Change interfaces & unions to require enums
This commit is contained in:
Sunli 2020-05-11 13:44:19 +08:00 committed by GitHub
commit 693ffda56a
11 changed files with 550 additions and 91 deletions

View File

@ -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<TokenStream> {
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)*

View File

@ -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<TokenStream> {
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<Tok
let mut collect_inline_fields = Vec::new();
let mut get_introspection_typename = Vec::new();
for field in &fields.unnamed {
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 {
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<Tok
}
let expanded = quote! {
#(#attrs)*
#vis enum #ident #generics { #(#enum_items),* }
#input
#(#type_into_impls)*

View File

@ -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);
```
enum Shape {
Circle(Circle),
Square(Square),
}
```

View File

@ -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);
```
enum Shape {
Circle(Circle),
Square(Square),
}
```

View File

@ -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);
```
enum Shape {
Circle(Circle),
Square(Square),
}
```

View File

@ -38,5 +38,8 @@ impl Square {
}
#[Union]
struct Shape(Circle, Square);
```
enum Shape {
Circle(Circle),
Square(Square),
}
```

View File

@ -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;
///
@ -522,7 +529,68 @@ pub use async_graphql_derive::Interface;
/// Define a GraphQL union
///
/// It's similar to Interface, but it doesn't have fields.
///
/// # Macro parameters
///
/// | Attribute | description | Type | Optional |
/// |-------------|---------------------------|----------|----------|
/// | name | Object name | string | Y |
/// | desc | Object description | string | Y |
///
/// # Define a union
///
/// Define TypeA, TypeB, ... as MyUnion
///
/// ```rust
/// use async_graphql::*;
///
/// #[SimpleObject]
/// struct TypeA {
/// value_a: i32,
/// }
///
/// #[SimpleObject]
/// struct TypeB {
/// value_b: i32
/// }
///
/// #[Union]
/// enum MyUnion {
/// TypeA(TypeA),
/// TypeB(TypeB),
/// }
///
/// struct QueryRoot;
///
/// #[Object]
/// impl QueryRoot {
/// async fn all_data(&self) -> Vec<MyUnion> {
/// 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

View File

@ -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,
@ -140,7 +149,12 @@ struct HumanOrAlien(Human, Alien);
arg(name = "surname", type = "Option<bool>")
)
)]
struct Being(Dog, Cat, Human, Alien);
enum Being {
Dog(Dog),
Cat(Cat),
Human(Human),
Alien(Alien),
}
#[Interface(
internal,
@ -150,7 +164,10 @@ struct Being(Dog, Cat, Human, Alien);
arg(name = "surname", type = "Option<bool>")
)
)]
struct Pet(Dog, Cat);
enum Pet {
Dog(Dog),
Cat(Cat),
}
#[Interface(
internal,
@ -160,10 +177,15 @@ struct Pet(Dog, Cat);
arg(name = "surname", type = "Option<bool>")
)
)]
struct Canine(Dog);
enum Canine {
Dog(Dog),
}
#[Interface(internal, field(name = "iq", type = "Option<i32>"))]
struct Intelligent(Human, Alien);
enum Intelligent {
Human(Human),
Alien(Alien),
}
#[InputObject(internal)]
struct ComplexInput {

View File

@ -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<i32>"))]
struct Node(MyObj);
enum Node {
MyObj(MyObj),
}
struct Query;

View File

@ -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 {}

287
tests/union.rs Normal file
View File

@ -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>() // `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<UnionA> {
vec![MyObjOne.into(), MyObjTwo.into()]
}
}
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.register_type::<UnionB>() // `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<i32> {
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,
}
})
);
}