Add OneOfObject macro to support for oneof input object. #766

This commit is contained in:
Sunli 2022-02-17 16:55:32 +08:00
parent 83dbe4d058
commit df3312363e
19 changed files with 688 additions and 28 deletions

View File

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [3.0.31] 2022-02-17
- Add `OneOfObject` macro to support for oneof input object.
# [3.0.30] 2022-2-15
- Implement `ScalarType` for `time::Date`. [#822](https://github.com/async-graphql/async-graphql/pull/822)

View File

@ -24,6 +24,7 @@ decimal = ["rust_decimal"]
cbor = ["serde_cbor"]
chrono-duration = ["chrono", "iso8601-duration"]
password-strength-validator = ["zxcvbn"]
unstable_oneof = ["async-graphql-derive/unstable_oneof"]
[dependencies]
async-graphql-derive = { path = "derive", version = "3.0.30" }

View File

@ -81,6 +81,7 @@ This crate offers the following features, all of which are not activated by defa
- `smol_str`: Integrate with the [`smol_str` crate](https://crates.io/crates/smol_str).
- `hashbrown`: Integrate with the [`hashbrown` crate](https://github.com/rust-lang/hashbrown).
- `time`: Integrate with the [`time` crate](https://github.com/time-rs/time).
- `unstable_oneof`: Enable the `OneofObject` macro to define the oneof input object.
## Apollo Studio

View File

@ -14,6 +14,9 @@ categories = ["network-programming", "asynchronous"]
[lib]
proc-macro = true
[features]
unstable_oneof = []
[dependencies]
async-graphql-parser = { path = "../parser", version = "3.0.30" }
proc-macro2 = "1.0.24"

View File

@ -401,6 +401,45 @@ pub struct InputObject {
pub complex: bool,
}
#[cfg(feature = "unstable_oneof")]
#[derive(FromVariant)]
#[darling(attributes(graphql), forward_attrs(doc))]
pub struct OneofObjectField {
pub ident: Ident,
pub attrs: Vec<Attribute>,
pub fields: Fields<syn::Type>,
#[darling(default)]
pub name: Option<String>,
#[darling(default)]
pub validator: Option<Validators>,
#[darling(default)]
pub visible: Option<Visible>,
#[darling(default)]
pub secret: bool,
}
#[cfg(feature = "unstable_oneof")]
#[derive(FromDeriveInput)]
#[darling(attributes(graphql), forward_attrs(doc))]
pub struct OneofObject {
pub ident: Ident,
pub generics: Generics,
pub attrs: Vec<Attribute>,
pub data: Data<OneofObjectField, Ignored>,
#[darling(default)]
pub internal: bool,
#[darling(default)]
pub name: Option<String>,
#[darling(default)]
pub rename_fields: Option<RenameRule>,
#[darling(default)]
pub visible: Option<Visible>,
#[darling(default, multiple, rename = "concrete")]
pub concretes: Vec<ConcreteType>,
}
#[derive(FromMeta)]
pub struct InterfaceFieldArgument {
pub name: String,

View File

@ -223,6 +223,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
},
visible: #visible,
rust_typename: ::std::any::type_name::<Self>(),
oneof: false,
})
}
@ -269,6 +270,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
},
visible: #visible,
rust_typename: ::std::any::type_name::<Self>(),
oneof: false,
})
}

View File

@ -5,12 +5,13 @@ use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::visit_mut::VisitMut;
use syn::{visit_mut, Error, Lifetime, Type};
use syn::{Error, Type};
use crate::args::{self, InterfaceField, InterfaceFieldArgument, RenameRuleExt, RenameTarget};
use crate::output_type::OutputType;
use crate::utils::{
gen_deprecation, generate_default, get_crate_name, get_rustdoc, visible_fn, GeneratorResult,
RemoveLifetime,
};
pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream> {
@ -76,14 +77,6 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
);
}
struct RemoveLifetime;
impl VisitMut for RemoveLifetime {
fn visit_lifetime_mut(&mut self, i: &mut Lifetime) {
i.ident = Ident::new("_", Span::call_site());
visit_mut::visit_lifetime_mut(self, i);
}
}
let mut assert_ty = p.clone();
RemoveLifetime.visit_type_path_mut(&mut assert_ty);

View File

@ -15,6 +15,8 @@ mod merged_object;
mod merged_subscription;
mod newtype;
mod object;
#[cfg(feature = "unstable_oneof")]
mod oneof_object;
mod output_type;
mod scalar;
mod simple_object;
@ -217,3 +219,18 @@ pub fn Directive(args: TokenStream, input: TokenStream) -> TokenStream {
Err(err) => err.write_errors().into(),
}
}
#[cfg(feature = "unstable_oneof")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable_oneof")))]
#[proc_macro_derive(OneofObject, attributes(graphql))]
pub fn derive_oneof_object(input: TokenStream) -> TokenStream {
let object_args =
match args::OneofObject::from_derive_input(&parse_macro_input!(input as DeriveInput)) {
Ok(object_args) => object_args,
Err(err) => return TokenStream::from(err.write_errors()),
};
match oneof_object::generate(&object_args) {
Ok(expanded) => expanded,
Err(err) => err.write_errors().into(),
}
}

257
derive/src/oneof_object.rs Normal file
View File

@ -0,0 +1,257 @@
use darling::ast::{Data, Style};
use proc_macro::TokenStream;
use quote::quote;
use std::collections::HashSet;
use syn::{Error, Type};
use crate::args;
use crate::args::{RenameRuleExt, RenameTarget};
use crate::utils::{get_crate_name, get_rustdoc, visible_fn, GeneratorResult};
pub fn generate(object_args: &args::OneofObject) -> GeneratorResult<TokenStream> {
let crate_name = get_crate_name(object_args.internal);
let (impl_generics, ty_generics, where_clause) = object_args.generics.split_for_impl();
let ident = &object_args.ident;
let desc = get_rustdoc(&object_args.attrs)?
.map(|s| quote! { ::std::option::Option::Some(#s) })
.unwrap_or_else(|| quote! {::std::option::Option::None});
let gql_typename = object_args
.name
.clone()
.unwrap_or_else(|| RenameTarget::Type.rename(ident.to_string()));
let s = match &object_args.data {
Data::Enum(s) => s,
_ => {
return Err(
Error::new_spanned(ident, "InputObject can only be applied to an enum.").into(),
)
}
};
let mut enum_items = HashSet::new();
let mut enum_names = Vec::new();
let mut schema_fields = Vec::new();
let mut parse_item = Vec::new();
let mut put_fields = Vec::new();
for variant in s {
let enum_name = &variant.ident;
let field_name = variant.name.clone().unwrap_or_else(|| {
object_args
.rename_fields
.rename(enum_name.to_string(), RenameTarget::Field)
});
let desc = get_rustdoc(&object_args.attrs)?
.map(|s| quote! { ::std::option::Option::Some(#s) })
.unwrap_or_else(|| quote! {::std::option::Option::None});
let ty = match variant.fields.style {
Style::Tuple if variant.fields.fields.len() == 1 => &variant.fields.fields[0],
Style::Tuple => {
return Err(Error::new_spanned(
enum_name,
"Only single value variants are supported",
)
.into())
}
Style::Unit => {
return Err(
Error::new_spanned(enum_name, "Empty variants are not supported").into(),
)
}
Style::Struct => {
return Err(Error::new_spanned(
enum_name,
"Variants with named fields are not supported",
)
.into())
}
};
if let Type::Path(p) = ty {
// This validates that the field type wasn't already used
if !enum_items.insert(p) {
return Err(
Error::new_spanned(ty, "This type already used in another variant").into(),
);
}
enum_names.push(enum_name);
let secret = variant.secret;
let visible = visible_fn(&variant.visible);
schema_fields.push(quote! {
fields.insert(::std::borrow::ToOwned::to_owned(#field_name), #crate_name::registry::MetaInputValue {
name: #field_name,
description: #desc,
ty: <::std::option::Option<#ty> as #crate_name::InputType>::create_type_info(registry),
default_value: ::std::option::Option::None,
visible: #visible,
is_secret: #secret,
});
});
let validators = variant
.validator
.clone()
.unwrap_or_default()
.create_validators(
&crate_name,
quote!(&value),
quote!(#ty),
Some(quote!(.map_err(#crate_name::InputValueError::propagate))),
)?;
parse_item.push(quote! {
if obj.contains_key(#field_name) && obj.len() == 1 {
let value = #crate_name::InputType::parse(obj.remove(#field_name)).map_err(#crate_name::InputValueError::propagate)?;
#validators
return ::std::result::Result::Ok(Self::#enum_name(value));
}
});
put_fields.push(quote! {
Self::#enum_name(value) => {
map.insert(#crate_name::Name::new(#field_name), #crate_name::InputType::to_value(value));
}
});
} else {
return Err(Error::new_spanned(ty, "Invalid type").into());
}
}
let visible = visible_fn(&object_args.visible);
let expanded = if object_args.concretes.is_empty() {
quote! {
impl #crate_name::InputType for #ident {
type RawValueType = Self;
fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> {
::std::borrow::Cow::Borrowed(#gql_typename)
}
fn create_type_info(registry: &mut #crate_name::registry::Registry) -> ::std::string::String {
registry.create_input_type::<Self, _>(|registry| #crate_name::registry::MetaType::InputObject {
name: ::std::borrow::ToOwned::to_owned(#gql_typename),
description: #desc,
input_fields: {
let mut fields = #crate_name::indexmap::IndexMap::new();
#(#schema_fields)*
fields
},
visible: #visible,
rust_typename: ::std::any::type_name::<Self>(),
oneof: true,
})
}
fn parse(value: ::std::option::Option<#crate_name::Value>) -> #crate_name::InputValueResult<Self> {
if let ::std::option::Option::Some(#crate_name::Value::Object(mut obj)) = value {
#(#parse_item)*
::std::result::Result::Err(#crate_name::InputValueError::expected_type(async_graphql::Value::Object(obj)))
} else {
::std::result::Result::Err(#crate_name::InputValueError::expected_type(value.unwrap_or_default()))
}
}
fn to_value(&self) -> #crate_name::Value {
let mut map = #crate_name::indexmap::IndexMap::new();
match self {
#(#put_fields)*
}
#crate_name::Value::Object(map)
}
fn federation_fields() -> ::std::option::Option<::std::string::String> {
::std::option::Option::None
}
fn as_raw_value(&self) -> ::std::option::Option<&Self::RawValueType> {
::std::option::Option::Some(self)
}
}
}
} else {
let mut code = Vec::new();
code.push(quote! {
#[allow(clippy::all, clippy::pedantic)]
impl #impl_generics #ident #ty_generics #where_clause {
fn __internal_create_type_info(registry: &mut #crate_name::registry::Registry, name: &str) -> ::std::string::String where Self: #crate_name::InputType {
registry.create_input_type::<Self, _>(|registry| #crate_name::registry::MetaType::InputObject {
name: ::std::borrow::ToOwned::to_owned(name),
description: #desc,
input_fields: {
let mut fields = #crate_name::indexmap::IndexMap::new();
#(#schema_fields)*
fields
},
visible: #visible,
rust_typename: ::std::any::type_name::<Self>(),
oneof: true,
})
}
fn __internal_parse(value: ::std::option::Option<#crate_name::Value>) -> #crate_name::InputValueResult<Self> where Self: #crate_name::InputType {
if let ::std::option::Option::Some(#crate_name::Value::Object(mut obj)) = value {
#(#parse_item)*
::std::result::Result::Err(#crate_name::InputValueError::expected_type(async_graphql::Value::Object(obj)))
} else {
::std::result::Result::Err(#crate_name::InputValueError::expected_type(value.unwrap_or_default()))
}
}
fn __internal_to_value(&self) -> #crate_name::Value where Self: #crate_name::InputType {
let mut map = #crate_name::indexmap::IndexMap::new();
match self {
#(#put_fields)*
}
#crate_name::Value::Object(map)
}
}
});
for concrete in &object_args.concretes {
let gql_typename = &concrete.name;
let params = &concrete.params.0;
let concrete_type = quote! { #ident<#(#params),*> };
let expanded = quote! {
#[allow(clippy::all, clippy::pedantic)]
impl #crate_name::InputType for #concrete_type {
type RawValueType = Self;
fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> {
::std::borrow::Cow::Borrowed(#gql_typename)
}
fn create_type_info(registry: &mut #crate_name::registry::Registry) -> ::std::string::String {
Self::__internal_create_type_info(registry, #gql_typename)
}
fn parse(value: ::std::option::Option<#crate_name::Value>) -> #crate_name::InputValueResult<Self> {
Self::__internal_parse(value)
}
fn to_value(&self) -> #crate_name::Value {
self.__internal_to_value()
}
fn federation_fields() -> ::std::option::Option<::std::string::String> {
::std::option::Option::None
}
fn as_raw_value(&self) -> ::std::option::Option<&Self::RawValueType> {
::std::option::Option::Some(self)
}
}
impl #crate_name::InputObjectType for #concrete_type {}
};
code.push(expanded);
}
quote!(#(#code)*)
};
Ok(expanded.into())
}

View File

@ -1,13 +1,12 @@
use darling::ast::{Data, Style};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use std::collections::HashSet;
use syn::visit_mut::VisitMut;
use syn::{visit_mut, Error, Lifetime, Type};
use syn::{Error, Type};
use crate::args::{self, RenameTarget};
use crate::utils::{get_crate_name, get_rustdoc, visible_fn, GeneratorResult};
use crate::utils::{get_crate_name, get_rustdoc, visible_fn, GeneratorResult, RemoveLifetime};
pub fn generate(union_args: &args::Union) -> GeneratorResult<TokenStream> {
let crate_name = get_crate_name(union_args.internal);
@ -71,14 +70,6 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult<TokenStream> {
enum_names.push(enum_name);
struct RemoveLifetime;
impl VisitMut for RemoveLifetime {
fn visit_lifetime_mut(&mut self, i: &mut Lifetime) {
i.ident = Ident::new("_", Span::call_site());
visit_mut::visit_lifetime_mut(self, i);
}
}
let mut assert_ty = p.clone();
RemoveLifetime.visit_type_path_mut(&mut assert_ty);

View File

@ -6,9 +6,10 @@ use proc_macro2::{Span, TokenStream, TokenTree};
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use syn::visit::Visit;
use syn::visit_mut::VisitMut;
use syn::{
Attribute, Error, Expr, ExprPath, FnArg, Ident, ImplItemMethod, Lit, LitStr, Meta, Pat,
PatIdent, Type, TypeGroup, TypeParamBound, TypeReference,
visit_mut, Attribute, Error, Expr, ExprPath, FnArg, Ident, ImplItemMethod, Lifetime, Lit,
LitStr, Meta, Pat, PatIdent, Type, TypeGroup, TypeParamBound, TypeReference,
};
use thiserror::Error;
@ -307,3 +308,12 @@ pub fn extract_input_args(
Ok(args)
}
pub struct RemoveLifetime;
impl VisitMut for RemoveLifetime {
fn visit_lifetime_mut(&mut self, i: &mut Lifetime) {
i.ident = Ident::new("_", Span::call_site());
visit_mut::visit_lifetime_mut(self, i);
}
}

View File

@ -4,12 +4,13 @@ Define a GraphQL input object
# Macro attributes
| Attribute | description | Type | Optional |
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|----------|
| name | Object name | string | Y |
| rename_fields | Rename all the fields according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE". | string | Y |
| visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
| visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
| Attribute | description | Type | Optional |
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|----------|
| name | Object name | string | Y |
| rename_fields | Rename all the fields according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE". | string | Y |
| visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
| visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
| concretes | Specify how the concrete type of the generic SimpleObject should be implemented. | ConcreteType | Y |
# Field attributes

View File

@ -17,6 +17,7 @@ All methods are converted to camelCase.
| visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
| visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
| serial | Resolve each field sequentially. | bool | Y |
| concretes | Specify how the concrete type of the generic SimpleObject should be implemented. | ConcreteType | Y |
# Field attributes

56
src/docs/oneof_object.md Normal file
View File

@ -0,0 +1,56 @@
Define a GraphQL oneof input object
# Macro attributes
| Attribute | description | Type | Optional |
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|----------|
| name | Object name | string | Y |
| rename_fields | Rename all the fields according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE". | string | Y |
| visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
| visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
| concretes | Specify how the concrete type of the generic SimpleObject should be implemented. | ConcreteType | Y |
# Field attributes
| Attribute | description | Type | Optional |
|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------|-------------|----------|
| name | Field name | string | Y |
| validator | Input value validator *[See also the Book](https://async-graphql.github.io/async-graphql/en/input_value_validators.html)* | object | Y |
| visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
| visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
| secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
# Examples
```rust
use async_graphql::*;
#[derive(OneofObject)]
enum MyInputObject {
A(i32),
B(String),
}
struct Query;
#[Object]
impl Query {
/// value
async fn value(&self, input: MyInputObject) -> String {
match input {
MyInputObject::A(value) => format!("a:{}", value),
MyInputObject::B(value) => format!("b:{}", value),
}
}
}
# tokio::runtime::Runtime::new().unwrap().block_on(async move {
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let res = schema.execute(r#"
{
value1: value(input:{a:100})
value2: value(input:{b:"abc"})
}"#).await.into_result().unwrap().data;
assert_eq!(res, value!({ "value1": "a:100", "value2": "b:abc" }));
# });
```

View File

@ -76,6 +76,7 @@
//! - `smol_str`: Integrate with the [`smol_str` crate](https://crates.io/crates/smol_str).
//! - `hashbrown`: Integrate with the [`hashbrown` crate](https://github.com/rust-lang/hashbrown).
//! - `time`: Integrate with the [`time` crate](https://github.com/time-rs/time).
//! - `unstable_oneof`: Enable the `OneofObject` macro to define the oneof input object.
//!
//! ## Integrations
//!
@ -266,6 +267,10 @@ pub use async_graphql_derive::MergedSubscription;
pub use async_graphql_derive::NewType;
#[doc = include_str!("docs/object.md")]
pub use async_graphql_derive::Object;
#[cfg(feature = "unstable_oneof")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable_oneof")))]
#[doc = include_str!("docs/oneof_object.md")]
pub use async_graphql_derive::OneofObject;
#[doc = include_str!("docs/scalar.md")]
pub use async_graphql_derive::Scalar;
#[doc = include_str!("docs/simple_object.md")]

View File

@ -228,4 +228,13 @@ impl<'a> __Type<'a> {
None
}
}
#[cfg(feature = "unstable_oneof")]
async fn one_of(&self) -> Option<bool> {
if let TypeDetail::Named(registry::MetaType::InputObject { oneof, .. }) = &self.detail {
Some(*oneof)
} else {
None
}
}
}

View File

@ -202,12 +202,18 @@ impl Registry {
name,
input_fields,
description,
#[cfg(feature = "unstable_oneof")]
oneof,
..
} => {
if description.is_some() {
writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description.unwrap()).ok();
}
write!(sdl, "input {} ", name).ok();
#[cfg(feature = "unstable_oneof")]
if *oneof {
write!(sdl, "@oneof ").ok();
}
writeln!(sdl, "{{").ok();
for field in input_fields.values() {
if let Some(description) = field.description {

View File

@ -228,6 +228,7 @@ pub enum MetaType {
input_fields: IndexMap<String, MetaInputValue>,
visible: Option<MetaVisibleFn>,
rust_typename: &'static str,
oneof: bool,
},
}

263
tests/oneof_object.rs Normal file
View File

@ -0,0 +1,263 @@
use async_graphql::registry::{MetaType, Registry};
use async_graphql::*;
#[tokio::test]
async fn test_oneof_object() {
#[derive(Debug, InputObject, PartialEq)]
struct MyInput {
a: i32,
b: String,
}
#[derive(Debug, OneofObject, PartialEq)]
enum MyOneofObj {
A(i32),
B(MyInput),
}
assert_eq!(
MyOneofObj::parse(Some(value!({
"a": 100,
})))
.unwrap(),
MyOneofObj::A(100)
);
assert_eq!(
MyOneofObj::A(100).to_value(),
value!({
"a": 100,
})
);
assert_eq!(
MyOneofObj::parse(Some(value!({
"b": {
"a": 200,
"b": "abc",
},
})))
.unwrap(),
MyOneofObj::B(MyInput {
a: 200,
b: "abc".to_string()
})
);
assert_eq!(
MyOneofObj::B(MyInput {
a: 200,
b: "abc".to_string()
})
.to_value(),
value!({
"b": {
"a": 200,
"b": "abc",
},
})
);
struct Query;
#[Object]
impl Query {
async fn test(&self, obj: MyOneofObj) -> String {
match obj {
MyOneofObj::A(value) => format!("a:{}", value),
MyOneofObj::B(MyInput { a, b }) => format!("b:{}/{}", a, b),
}
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema
.execute("{ test(obj: {a: 100}) }")
.await
.into_result()
.unwrap()
.data,
value!({
"test": "a:100"
})
);
assert_eq!(
schema
.execute(r#"{ test(obj: {b: {a: 200, b: "abc"}}) }"#)
.await
.into_result()
.unwrap()
.data,
value!({
"test": "b:200/abc"
})
);
assert_eq!(
schema
.execute(r#"{ __type(name: "MyOneofObj") { name oneOf } }"#)
.await
.into_result()
.unwrap()
.data,
value!({
"__type": { "name": "MyOneofObj", "oneOf": true }
})
);
}
#[tokio::test]
async fn test_oneof_object_concrete() {
#[derive(Debug, OneofObject, PartialEq)]
#[graphql(
concrete(name = "MyObjI32", params(i32)),
concrete(name = "MyObjString", params(String))
)]
enum MyObj<T: InputType> {
A(i32),
B(T),
}
assert_eq!(MyObj::<i32>::type_name(), "MyObjI32");
assert_eq!(MyObj::<String>::type_name(), "MyObjString");
assert_eq!(
MyObj::<String>::parse(Some(value!({
"a": 100,
})))
.unwrap(),
MyObj::A(100)
);
assert_eq!(
MyObj::<i32>::A(100).to_value(),
value!({
"a": 100,
})
);
assert_eq!(
MyObj::<String>::B("abc".to_string()).to_value(),
value!({
"b": "abc",
})
);
struct Query;
#[Object]
impl Query {
async fn test(&self, obj: MyObj<String>) -> String {
match obj {
MyObj::A(value) => format!("a:{}", value),
MyObj::B(value) => format!("b:{}", value),
}
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema
.execute("{ test(obj: {a: 100}) }")
.await
.into_result()
.unwrap()
.data,
value!({
"test": "a:100"
})
);
assert_eq!(
schema
.execute(r#"{ test(obj: {b: "abc"}) }"#)
.await
.into_result()
.unwrap()
.data,
value!({
"test": "b:abc"
})
);
}
#[tokio::test]
async fn test_oneof_object_rename_fields() {
#[derive(OneofObject)]
#[graphql(rename_fields = "lowercase")]
enum MyInput {
Name(i32),
CreateAt(String),
}
let mut registry = Registry::default();
MyInput::create_type_info(&mut registry);
let ty: &MetaType = registry.types.get("MyInput").unwrap();
match ty {
MetaType::InputObject { input_fields, .. } => {
assert_eq!(
input_fields.keys().collect::<Vec<_>>(),
vec!["name", "createat"]
);
}
_ => unreachable!(),
}
}
#[tokio::test]
async fn test_oneof_object_rename_field() {
#[derive(OneofObject)]
enum MyInput {
Name(i32),
#[graphql(name = "create_At")]
CreateAt(String),
}
let mut registry = Registry::default();
MyInput::create_type_info(&mut registry);
let ty: &MetaType = registry.types.get("MyInput").unwrap();
match ty {
MetaType::InputObject { input_fields, .. } => {
assert_eq!(
input_fields.keys().collect::<Vec<_>>(),
vec!["name", "create_At"]
);
}
_ => unreachable!(),
}
}
#[tokio::test]
async fn test_oneof_object_validation() {
#[derive(Debug, OneofObject, PartialEq)]
enum MyOneofObj {
#[graphql(validator(maximum = 10))]
A(i32),
#[graphql(validator(max_length = 3))]
B(String),
}
assert_eq!(
MyOneofObj::parse(Some(value!({
"a": 5,
})))
.unwrap(),
MyOneofObj::A(5)
);
assert_eq!(
MyOneofObj::parse(Some(value!({
"a": 20,
})))
.unwrap_err()
.into_server_error(Default::default())
.message,
r#"Failed to parse "Int": the value is 20, must be less than or equal to 10 (occurred while parsing "MyOneofObj")"#
);
}