From df3312363e0c5f8c7127787544737e48739481f9 Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 17 Feb 2022 16:55:32 +0800 Subject: [PATCH] Add `OneOfObject` macro to support for oneof input object. #766 --- CHANGELOG.md | 4 + Cargo.toml | 1 + README.md | 1 + derive/Cargo.toml | 3 + derive/src/args.rs | 39 ++++++ derive/src/input_object.rs | 2 + derive/src/interface.rs | 11 +- derive/src/lib.rs | 17 +++ derive/src/oneof_object.rs | 257 ++++++++++++++++++++++++++++++++++++ derive/src/union.rs | 13 +- derive/src/utils.rs | 14 +- src/docs/input_object.md | 13 +- src/docs/object.md | 1 + src/docs/oneof_object.md | 56 ++++++++ src/lib.rs | 5 + src/model/type.rs | 9 ++ src/registry/export_sdl.rs | 6 + src/registry/mod.rs | 1 + tests/oneof_object.rs | 263 +++++++++++++++++++++++++++++++++++++ 19 files changed, 688 insertions(+), 28 deletions(-) create mode 100644 derive/src/oneof_object.rs create mode 100644 src/docs/oneof_object.md create mode 100644 tests/oneof_object.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 663939b7..9148aa49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Cargo.toml b/Cargo.toml index 18c4eb3e..3f899609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/README.md b/README.md index acbc7df0..d615fe1b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/derive/Cargo.toml b/derive/Cargo.toml index eca65b5c..812ba29c 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -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" diff --git a/derive/src/args.rs b/derive/src/args.rs index 95bb76a8..e519e1f7 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -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, + pub fields: Fields, + + #[darling(default)] + pub name: Option, + #[darling(default)] + pub validator: Option, + #[darling(default)] + pub visible: Option, + #[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, + pub data: Data, + + #[darling(default)] + pub internal: bool, + #[darling(default)] + pub name: Option, + #[darling(default)] + pub rename_fields: Option, + #[darling(default)] + pub visible: Option, + #[darling(default, multiple, rename = "concrete")] + pub concretes: Vec, +} + #[derive(FromMeta)] pub struct InterfaceFieldArgument { pub name: String, diff --git a/derive/src/input_object.rs b/derive/src/input_object.rs index c318dc56..70f5a934 100644 --- a/derive/src/input_object.rs +++ b/derive/src/input_object.rs @@ -223,6 +223,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult }, visible: #visible, rust_typename: ::std::any::type_name::(), + oneof: false, }) } @@ -269,6 +270,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult }, visible: #visible, rust_typename: ::std::any::type_name::(), + oneof: false, }) } diff --git a/derive/src/interface.rs b/derive/src/interface.rs index 72dcf6c9..5a224807 100644 --- a/derive/src/interface.rs +++ b/derive/src/interface.rs @@ -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 { @@ -76,14 +77,6 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult 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(), + } +} diff --git a/derive/src/oneof_object.rs b/derive/src/oneof_object.rs new file mode 100644 index 00000000..8017fd48 --- /dev/null +++ b/derive/src/oneof_object.rs @@ -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 { + 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::(|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::(), + oneof: true, + }) + } + + fn parse(value: ::std::option::Option<#crate_name::Value>) -> #crate_name::InputValueResult { + 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::(|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::(), + oneof: true, + }) + } + + fn __internal_parse(value: ::std::option::Option<#crate_name::Value>) -> #crate_name::InputValueResult 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::__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()) +} diff --git a/derive/src/union.rs b/derive/src/union.rs index 222e59f7..c35bc4bf 100644 --- a/derive/src/union.rs +++ b/derive/src/union.rs @@ -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 { let crate_name = get_crate_name(union_args.internal); @@ -71,14 +70,6 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult { 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); diff --git a/derive/src/utils.rs b/derive/src/utils.rs index fb05b8a6..1b578496 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -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); + } +} diff --git a/src/docs/input_object.md b/src/docs/input_object.md index 671f11aa..5b3cb82f 100644 --- a/src/docs/input_object.md +++ b/src/docs/input_object.md @@ -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 diff --git a/src/docs/object.md b/src/docs/object.md index d40af007..9faeb8d3 100644 --- a/src/docs/object.md +++ b/src/docs/object.md @@ -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 diff --git a/src/docs/oneof_object.md b/src/docs/oneof_object.md new file mode 100644 index 00000000..2624e7b0 --- /dev/null +++ b/src/docs/oneof_object.md @@ -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" })); +# }); +``` diff --git a/src/lib.rs b/src/lib.rs index 04cf05c8..50f1e35d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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")] diff --git a/src/model/type.rs b/src/model/type.rs index d30f9952..991547da 100644 --- a/src/model/type.rs +++ b/src/model/type.rs @@ -228,4 +228,13 @@ impl<'a> __Type<'a> { None } } + + #[cfg(feature = "unstable_oneof")] + async fn one_of(&self) -> Option { + if let TypeDetail::Named(registry::MetaType::InputObject { oneof, .. }) = &self.detail { + Some(*oneof) + } else { + None + } + } } diff --git a/src/registry/export_sdl.rs b/src/registry/export_sdl.rs index 2a1db35b..d70b6b8e 100644 --- a/src/registry/export_sdl.rs +++ b/src/registry/export_sdl.rs @@ -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 { diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 834d5d2d..fc8dac36 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -228,6 +228,7 @@ pub enum MetaType { input_fields: IndexMap, visible: Option, rust_typename: &'static str, + oneof: bool, }, } diff --git a/tests/oneof_object.rs b/tests/oneof_object.rs new file mode 100644 index 00000000..8a36db0e --- /dev/null +++ b/tests/oneof_object.rs @@ -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 { + A(i32), + B(T), + } + + assert_eq!(MyObj::::type_name(), "MyObjI32"); + assert_eq!(MyObj::::type_name(), "MyObjString"); + + assert_eq!( + MyObj::::parse(Some(value!({ + "a": 100, + }))) + .unwrap(), + MyObj::A(100) + ); + + assert_eq!( + MyObj::::A(100).to_value(), + value!({ + "a": 100, + }) + ); + + assert_eq!( + MyObj::::B("abc".to_string()).to_value(), + value!({ + "b": "abc", + }) + ); + + struct Query; + + #[Object] + impl Query { + async fn test(&self, obj: MyObj) -> 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!["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!["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")"# + ); +}