Add OneOfObject
macro to support for oneof input object. #766
This commit is contained in:
parent
83dbe4d058
commit
df3312363e
|
@ -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)
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
257
derive/src/oneof_object.rs
Normal 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())
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
56
src/docs/oneof_object.md
Normal 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" }));
|
||||
# });
|
||||
```
|
|
@ -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")]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
263
tests/oneof_object.rs
Normal 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")"#
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user