Merge branch 'master' into master

This commit is contained in:
gho1b 2022-03-05 01:58:09 +07:00 committed by GitHub
commit 2f74cd4641
68 changed files with 2297 additions and 458 deletions

View File

@ -4,6 +4,37 @@ 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.33] 2022-3-4
- Add support for oneof field on object. [#766](https://github.com/async-graphql/async-graphql/issues/766)
# [3.0.32] 2022-3-4
- Bump `Actix-web` from `4.0.0-rc.3` to `4.0.1`.
# [3.0.31] 2022-02-17
- Add `OneOfObject` macro to support for oneof input object.
- Bump actix-web from `4.0.0-rc.2` to `4.0.0-rc.3`.
# [3.0.30] 2022-2-15
- Implement `ScalarType` for `time::Date`. [#822](https://github.com/async-graphql/async-graphql/pull/822)
# [3.0.29] 2022-2-6
- Pass context to resolvers with flatten attribute. [#813](https://github.com/async-graphql/async-graphql/pull/813)
- Add support for using both `ComplexObject` and `InputObject`.
- Bump `Actix-web` from `4.0.0-beta.19` to `4.0.0-rc.2`.
# [3.0.28] 2022-1-30
- Implement `InputType` and `OutputType` for `Box<[T]>` and `Arc<[T]>`. [#805](https://github.com/async-graphql/async-graphql/issues/805)
# [3.0.27] 2022-1-28
- Fix possible stack overflow in validator, thanks @quapka.
# [3.0.26] 2022-1-26
- Add `skip_input` attribute to `InputObject` macro, `skip_output` attribute to `SimpleObject` macro.

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql"
version = "3.0.26"
version = "3.0.33"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "A GraphQL server library implemented in Rust"
@ -26,9 +26,9 @@ chrono-duration = ["chrono", "iso8601-duration"]
password-strength-validator = ["zxcvbn"]
[dependencies]
async-graphql-derive = { path = "derive", version = "3.0.26" }
async-graphql-value = { path = "value", version = "3.0.26" }
async-graphql-parser = { path = "parser", version = "3.0.26" }
async-graphql-derive = { path = "derive", version = "3.0.33" }
async-graphql-value = { path = "value", version = "3.0.33" }
async-graphql-parser = { path = "parser", version = "3.0.33" }
async-stream = "0.3.0"
async-trait = "0.1.48"
@ -52,15 +52,15 @@ fast_chemail = "0.9.6"
# Feature optional dependencies
bson = { version = "2.0.0", optional = true, features = ["chrono-0_4"] }
chrono = { version = "0.4.19", optional = true }
chrono = { version = "0.4.19", optional = true, default-features = false, features = ["clock", "std"] }
chrono-tz = { version = "0.5.3", optional = true }
hashbrown = { version = "0.12.0", optional = true }
iso8601-duration = { version = "0.1.0", optional = true }
log = { version = "0.4.14", optional = true }
secrecy = { version = "0.7.0", optional = true }
secrecy = { version = "0.8.0", optional = true }
tracinglib = { version = "0.1.25", optional = true, package = "tracing" }
tracing-futures = { version = "0.2.5", optional = true, features = ["std-future", "futures-03"] }
opentelemetry = { version = "0.16.0", optional = true, default-features = false, features = ["trace"] }
opentelemetry = { version = "0.17.0", optional = true, default-features = false, features = ["trace"] }
uuid = { version = "0.8.2", optional = true, features = ["v4", "serde"] }
rust_decimal = { version = "1.14.3", optional = true }
url = { version = "2.2.1", optional = true }
@ -70,7 +70,7 @@ time = { version = "0.3.5", optional = true, features = ["parsing", "formatting"
# Non-feature optional dependencies
blocking = { version = "1.0.2", optional = true }
lru = { version = "0.7.1", optional = true }
sha2 = { version = "0.9.3", optional = true }
sha2 = { version = "0.10.2", optional = true }
futures-timer = { version = "3.0.2", optional = true }
futures-channel = { version = "0.3.13", optional = true }
serde_cbor = { version = "0.11.1", optional = true }

View File

@ -2,9 +2,9 @@
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| >= 3.0.0 | :white_check_mark: |
| Version | Supported |
|----------|--------------------|
| >= 3.0.0 | :white_check_mark: |
## Reporting a Vulnerability

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-derive"
version = "3.0.26"
version = "3.0.33"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "Macros for async-graphql"
@ -15,7 +15,7 @@ categories = ["network-programming", "asynchronous"]
proc-macro = true
[dependencies]
async-graphql-parser = { path = "../parser", version = "3.0.26" }
async-graphql-parser = { path = "../parser", version = "3.0.33" }
proc-macro2 = "1.0.24"
syn = { version = "1.0.64", features = ["full", "extra-traits", "visit-mut", "visit"] }
quote = "1.0.9"

View File

@ -271,6 +271,7 @@ pub struct ObjectField {
#[darling(default, multiple)]
pub derived: Vec<DerivedField>,
pub flatten: bool,
pub oneof: bool,
}
#[derive(FromMeta, Default, Clone)]
@ -396,6 +397,46 @@ pub struct InputObject {
pub visible: Option<Visible>,
#[darling(default, multiple, rename = "concrete")]
pub concretes: Vec<ConcreteType>,
// for SimpleObject
#[darling(default)]
pub complex: bool,
}
#[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,
}
#[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)]
@ -417,7 +458,7 @@ pub struct InterfaceFieldArgument {
#[derive(FromMeta)]
pub struct InterfaceField {
pub name: String,
pub name: SpannedValue<String>,
#[darling(rename = "type")]
pub ty: LitStr,
#[darling(default)]
@ -436,6 +477,8 @@ pub struct InterfaceField {
pub requires: Option<String>,
#[darling(default)]
pub visible: Option<Visible>,
#[darling(default)]
pub oneof: bool,
}
#[derive(FromVariant)]
@ -511,6 +554,7 @@ pub struct SubscriptionField {
pub guard: Option<SpannedValue<String>>,
pub visible: Option<Visible>,
pub complexity: Option<ComplexityType>,
pub oneof: bool,
}
#[derive(FromField)]
@ -712,6 +756,7 @@ pub struct ComplexObjectField {
#[darling(multiple)]
pub derived: Vec<DerivedField>,
pub flatten: bool,
pub oneof: bool,
}
#[derive(FromMeta, Default)]

View File

@ -125,6 +125,9 @@ pub fn generate(
let cfg_attrs = get_cfg_attrs(&method.attrs);
if method_args.flatten {
// Only used to inject the context placeholder if required.
extract_input_args::<args::Argument>(&crate_name, method)?;
let ty = match &method.sig.output {
ReturnType::Type(_, ty) => OutputType::parse(ty)?,
ReturnType::Default => {
@ -149,7 +152,7 @@ pub fn generate(
resolvers.push(quote! {
#(#cfg_attrs)*
if let ::std::option::Option::Some(value) = #crate_name::ContainerType::resolve_field(&self.#ident().await, ctx).await? {
if let ::std::option::Option::Some(value) = #crate_name::ContainerType::resolve_field(&self.#ident(ctx).await, ctx).await? {
return ::std::result::Result::Ok(std::option::Option::Some(value));
}
});
@ -187,7 +190,120 @@ pub fn generate(
}
};
let args = extract_input_args(&crate_name, method)?;
let mut args = extract_input_args::<args::Argument>(&crate_name, method)?;
let mut schema_args = Vec::new();
let mut use_params = Vec::new();
let mut get_params = Vec::new();
let mut is_oneof_field = false;
if method_args.oneof {
is_oneof_field = true;
if args.len() != 1 {
return Err(Error::new_spanned(
&method,
"The `oneof` field requires exactly one argument.",
)
.into());
}
let (ident, ty, argument) = args.pop().unwrap();
schema_args.push(quote! {
#crate_name::static_assertions::assert_impl_one!(#ty: #crate_name::OneofObjectType);
if let #crate_name::registry::MetaType::InputObject { input_fields, .. } = registry.create_fake_input_type::<#ty>() {
args.extend(input_fields);
}
});
use_params.push(quote! { #ident });
let validators = argument
.validator
.clone()
.unwrap_or_default()
.create_validators(
&crate_name,
quote!(&#ident),
quote!(#ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
get_params.push(quote! {
#[allow(non_snake_case, unused_variables)]
let (__pos, #ident) = ctx.oneof_param_value::<#ty>()?;
#validators
});
} else {
for (
ident,
ty,
args::Argument {
name,
desc,
default,
default_with,
validator,
visible,
secret,
..
},
) in &args
{
let name = name.clone().unwrap_or_else(|| {
object_args
.rename_args
.rename(ident.ident.unraw().to_string(), RenameTarget::Argument)
});
let desc = desc
.as_ref()
.map(|s| quote! {::std::option::Option::Some(#s)})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let default = generate_default(default, default_with)?;
let schema_default = default
.as_ref()
.map(|value| {
quote! {
::std::option::Option::Some(::std::string::ToString::to_string(
&<#ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let visible = visible_fn(visible);
schema_args.push(quote! {
args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#ty as #crate_name::InputType>::create_type_info(registry),
default_value: #schema_default,
visible: #visible,
is_secret: #secret,
});
});
let param_ident = &ident.ident;
use_params.push(quote! { #param_ident });
let default = match default {
Some(default) => {
quote! { ::std::option::Option::Some(|| -> #ty { #default }) }
}
None => quote! { ::std::option::Option::None },
};
let validators = validator.clone().unwrap_or_default().create_validators(
&crate_name,
quote!(&#ident),
quote!(ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
get_params.push(quote! {
#[allow(non_snake_case)]
let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?;
#validators
});
}
}
let ty = match &method.sig.output {
ReturnType::Type(_, ty) => OutputType::parse(ty)?,
ReturnType::Default => {
@ -198,83 +314,6 @@ pub fn generate(
.into())
}
};
let mut schema_args = Vec::new();
let mut use_params = Vec::new();
let mut get_params = Vec::new();
for (
ident,
ty,
args::Argument {
name,
desc,
default,
default_with,
validator,
visible,
secret,
..
},
) in &args
{
let name = name.clone().unwrap_or_else(|| {
object_args
.rename_args
.rename(ident.ident.unraw().to_string(), RenameTarget::Argument)
});
let desc = desc
.as_ref()
.map(|s| quote! {::std::option::Option::Some(#s)})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let default = generate_default(default, default_with)?;
let schema_default = default
.as_ref()
.map(|value| {
quote! {
::std::option::Option::Some(::std::string::ToString::to_string(
&<#ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let visible = visible_fn(visible);
schema_args.push(quote! {
args.insert(#name, #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#ty as #crate_name::InputType>::create_type_info(registry),
default_value: #schema_default,
visible: #visible,
is_secret: #secret,
});
});
let param_ident = &ident.ident;
use_params.push(quote! { #param_ident });
let default = match default {
Some(default) => {
quote! { ::std::option::Option::Some(|| -> #ty { #default }) }
}
None => quote! { ::std::option::Option::None },
};
let validators = validator.clone().unwrap_or_default().create_validators(
&crate_name,
quote!(&#ident),
quote!(ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
get_params.push(quote! {
#[allow(non_snake_case)]
let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?;
#validators
});
}
let schema_ty = ty.value_type();
let visible = visible_fn(&method_args.visible);
@ -347,6 +386,7 @@ pub fn generate(
requires: #requires,
visible: #visible,
compute_complexity: #complexity,
oneof: #is_oneof_field,
}));
});

View File

@ -82,7 +82,7 @@ pub fn generate(
let visible = visible_fn(&visible);
schema_args.push(quote! {
args.insert(#name, #crate_name::registry::MetaInputValue {
args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#arg_ty as #crate_name::InputType>::create_type_info(registry),

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);
@ -143,8 +136,10 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
provides,
requires,
visible,
oneof,
} in &interface_args.fields
{
let name_span = name.span();
let (name, method_name) = if let Some(method) = method {
(name.to_string(), Ident::new(method, Span::call_site()))
} else {
@ -152,7 +147,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
(
interface_args
.rename_fields
.rename(name, RenameTarget::Field),
.rename(name.as_ref(), RenameTarget::Field),
method_name,
)
};
@ -177,61 +172,93 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
decl_params.push(quote! { ctx: &'ctx #crate_name::Context<'ctx> });
use_params.push(quote! { ctx });
for InterfaceFieldArgument {
name,
desc,
ty,
default,
default_with,
visible,
secret,
} in args
{
let mut is_oneof_field = false;
if *oneof {
is_oneof_field = true;
if args.len() != 1 {
return Err(
Error::new(name_span, "The `oneof` field requires one parameter.").into(),
);
}
let InterfaceFieldArgument { name, ty, .. } = &args[0];
let ident = Ident::new(name, Span::call_site());
let name = interface_args
.rename_args
.rename(name, RenameTarget::Argument);
let ty = match syn::parse_str::<syn::Type>(&ty.value()) {
Ok(ty) => ty,
Err(_) => return Err(Error::new_spanned(&ty, "Expect type").into()),
};
decl_params.push(quote! { #ident: #ty });
use_params.push(quote! { #ident });
let default = generate_default(default, default_with)?;
let get_default = match &default {
Some(default) => quote! { ::std::option::Option::Some(|| -> #ty { #default }) },
None => quote! { ::std::option::Option::None },
};
get_params.push(quote! {
let (_, #ident) = ctx.param_value::<#ty>(#name, #get_default)?;
#[allow(non_snake_case, unused_variables)]
let (_, #ident) = ctx.oneof_param_value::<#ty>()?;
});
let desc = desc
.as_ref()
.map(|s| quote! {::std::option::Option::Some(#s)})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let schema_default = default
.as_ref()
.map(|value| {
quote! {
::std::option::Option::Some(::std::string::ToString::to_string(
&<#ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let visible = visible_fn(visible);
schema_args.push(quote! {
args.insert(#name, #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#ty as #crate_name::InputType>::create_type_info(registry),
default_value: #schema_default,
visible: #visible,
is_secret: #secret,
});
#crate_name::static_assertions::assert_impl_one!(#ty: #crate_name::OneofObjectType);
if let #crate_name::registry::MetaType::InputObject { input_fields, .. } = registry.create_fake_input_type::<#ty>() {
args.extend(input_fields);
}
});
} else {
for InterfaceFieldArgument {
name,
desc,
ty,
default,
default_with,
visible,
secret,
} in args
{
let ident = Ident::new(name, Span::call_site());
let name = interface_args
.rename_args
.rename(name, RenameTarget::Argument);
let ty = match syn::parse_str::<syn::Type>(&ty.value()) {
Ok(ty) => ty,
Err(_) => return Err(Error::new_spanned(&ty, "Expect type").into()),
};
decl_params.push(quote! { #ident: #ty });
use_params.push(quote! { #ident });
let default = generate_default(default, default_with)?;
let get_default = match &default {
Some(default) => quote! { ::std::option::Option::Some(|| -> #ty { #default }) },
None => quote! { ::std::option::Option::None },
};
get_params.push(quote! {
let (_, #ident) = ctx.param_value::<#ty>(#name, #get_default)?;
});
let desc = desc
.as_ref()
.map(|s| quote! {::std::option::Option::Some(#s)})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let schema_default = default
.as_ref()
.map(|value| {
quote! {
::std::option::Option::Some(::std::string::ToString::to_string(
&<#ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let visible = visible_fn(visible);
schema_args.push(quote! {
args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#ty as #crate_name::InputType>::create_type_info(registry),
default_value: #schema_default,
visible: #visible,
is_secret: #secret,
});
});
}
}
for enum_name in &enum_names {
@ -282,6 +309,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
requires: #requires,
visible: #visible,
compute_complexity: ::std::option::Option::None,
oneof: #is_oneof_field,
});
});

View File

@ -15,6 +15,7 @@ mod merged_object;
mod merged_subscription;
mod newtype;
mod object;
mod oneof_object;
mod output_type;
mod scalar;
mod simple_object;
@ -217,3 +218,16 @@ pub fn Directive(args: TokenStream, input: TokenStream) -> TokenStream {
Err(err) => err.write_errors().into(),
}
}
#[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(),
}
}

View File

@ -141,7 +141,15 @@ pub fn generate(
return Err(Error::new_spanned(&method, "Must be asynchronous").into());
}
let args = extract_input_args(&crate_name, method)?;
if method_args.oneof {
return Err(Error::new_spanned(
&method,
"The `entity` and `oneof` attributes cannot be specified at the same time.",
)
.into());
}
let args = extract_input_args::<args::Argument>(&crate_name, method)?;
let ty = match &method.sig.output {
ReturnType::Type(_, ty) => OutputType::parse(ty)?,
@ -261,6 +269,9 @@ pub fn generate(
let cfg_attrs = get_cfg_attrs(&method.attrs);
if method_args.flatten {
// Only used to inject the context placeholder if required.
extract_input_args::<args::Argument>(&crate_name, method)?;
let ty = match &method.sig.output {
ReturnType::Type(_, ty) => OutputType::parse(ty)?,
ReturnType::Default => {
@ -285,7 +296,7 @@ pub fn generate(
resolvers.push(quote! {
#(#cfg_attrs)*
if let ::std::option::Option::Some(value) = #crate_name::ContainerType::resolve_field(&self.#ident().await, ctx).await? {
if let ::std::option::Option::Some(value) = #crate_name::ContainerType::resolve_field(&self.#ident(ctx).await, ctx).await? {
return ::std::result::Result::Ok(std::option::Option::Some(value));
}
});
@ -323,10 +334,121 @@ pub fn generate(
}
};
let args = extract_input_args(&crate_name, method)?;
let mut args = extract_input_args::<args::Argument>(&crate_name, method)?;
let mut schema_args = Vec::new();
let mut use_params = Vec::new();
let mut get_params = Vec::new();
let mut is_oneof_field = false;
if method_args.oneof {
is_oneof_field = true;
if args.len() != 1 {
return Err(Error::new_spanned(
&method,
"The `oneof` field requires exactly one argument.",
)
.into());
}
let (ident, ty, argument) = args.pop().unwrap();
schema_args.push(quote! {
#crate_name::static_assertions::assert_impl_one!(#ty: #crate_name::OneofObjectType);
if let #crate_name::registry::MetaType::InputObject { input_fields, .. } = registry.create_fake_input_type::<#ty>() {
args.extend(input_fields);
}
});
use_params.push(quote! { #ident });
let validators = argument
.validator
.clone()
.unwrap_or_default()
.create_validators(
&crate_name,
quote!(&#ident),
quote!(#ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
get_params.push(quote! {
#[allow(non_snake_case, unused_variables)]
let (__pos, #ident) = ctx.oneof_param_value::<#ty>()?;
#validators
});
} else {
for (
ident,
ty,
args::Argument {
name,
desc,
default,
default_with,
validator,
visible,
secret,
..
},
) in &args
{
let name = name.clone().unwrap_or_else(|| {
object_args
.rename_args
.rename(ident.ident.unraw().to_string(), RenameTarget::Argument)
});
let desc = desc
.as_ref()
.map(|s| quote! {::std::option::Option::Some(#s)})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let default = generate_default(default, default_with)?;
let schema_default = default
.as_ref()
.map(|value| {
quote! {
::std::option::Option::Some(::std::string::ToString::to_string(
&<#ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let visible = visible_fn(visible);
schema_args.push(quote! {
args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#ty as #crate_name::InputType>::create_type_info(registry),
default_value: #schema_default,
visible: #visible,
is_secret: #secret,
});
});
let param_ident = &ident.ident;
use_params.push(quote! { #param_ident });
let default = match default {
Some(default) => {
quote! { ::std::option::Option::Some(|| -> #ty { #default }) }
}
None => quote! { ::std::option::Option::None },
};
let validators = validator.clone().unwrap_or_default().create_validators(
&crate_name,
quote!(&#ident),
quote!(#ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
get_params.push(quote! {
#[allow(non_snake_case, unused_variables)]
let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?;
#validators
});
}
}
let ty = match &method.sig.output {
ReturnType::Type(_, ty) => OutputType::parse(ty)?,
ReturnType::Default => {
@ -337,79 +459,6 @@ pub fn generate(
.into())
}
};
for (
ident,
ty,
args::Argument {
name,
desc,
default,
default_with,
validator,
visible,
secret,
..
},
) in &args
{
let name = name.clone().unwrap_or_else(|| {
object_args
.rename_args
.rename(ident.ident.unraw().to_string(), RenameTarget::Argument)
});
let desc = desc
.as_ref()
.map(|s| quote! {::std::option::Option::Some(#s)})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let default = generate_default(default, default_with)?;
let schema_default = default
.as_ref()
.map(|value| {
quote! {
::std::option::Option::Some(::std::string::ToString::to_string(
&<#ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let visible = visible_fn(visible);
schema_args.push(quote! {
args.insert(#name, #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#ty as #crate_name::InputType>::create_type_info(registry),
default_value: #schema_default,
visible: #visible,
is_secret: #secret,
});
});
let param_ident = &ident.ident;
use_params.push(quote! { #param_ident });
let default = match default {
Some(default) => {
quote! { ::std::option::Option::Some(|| -> #ty { #default }) }
}
None => quote! { ::std::option::Option::None },
};
let validators = validator.clone().unwrap_or_default().create_validators(
&crate_name,
quote!(&#ident),
quote!(#ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
get_params.push(quote! {
#[allow(non_snake_case, unused_variables)]
let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?;
#validators
});
}
let schema_ty = ty.value_type();
let visible = visible_fn(&method_args.visible);
@ -482,6 +531,7 @@ pub fn generate(
requires: #requires,
visible: #visible,
compute_complexity: #complexity,
oneof: #is_oneof_field,
});
});

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

@ -0,0 +1,261 @@
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(#crate_name::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)
}
}
impl #crate_name::InputObjectType for #ident {}
impl #crate_name::OneofObjectType for #ident {}
}
} 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(#crate_name::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 {}
impl #crate_name::OneofObjectType for #concrete_type {}
};
code.push(expanded);
}
quote!(#(#code)*)
};
Ok(expanded.into())
}

View File

@ -167,6 +167,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
requires: #requires,
visible: #visible,
compute_complexity: ::std::option::Option::None,
oneof: false,
});
});
} else {

View File

@ -1,17 +1,14 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::ext::IdentExt;
use syn::{
Block, Error, FnArg, ImplItem, ItemImpl, Pat, ReturnType, Type, TypeImplTrait, TypeParamBound,
TypeReference,
};
use syn::{Block, Error, ImplItem, ItemImpl, ReturnType, Type, TypeImplTrait, TypeParamBound};
use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget, SubscriptionField};
use crate::output_type::OutputType;
use crate::utils::{
gen_deprecation, generate_default, generate_guards, get_cfg_attrs, get_crate_name, get_rustdoc,
get_type_path_and_name, parse_complexity_expr, parse_graphql_attrs, remove_graphql_attrs,
visible_fn, GeneratorResult,
extract_input_args, gen_deprecation, generate_default, generate_guards, get_cfg_attrs,
get_crate_name, get_rustdoc, get_type_path_and_name, parse_complexity_expr,
parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult,
};
pub fn generate(
@ -48,7 +45,7 @@ pub fn generate(
continue;
}
let ident = &method.sig.ident;
let ident = method.sig.ident.clone();
let field_name = field.name.clone().unwrap_or_else(|| {
subscription_args
.rename_fields
@ -68,6 +65,120 @@ pub fn generate(
.into());
}
let mut schema_args = Vec::new();
let mut use_params = Vec::new();
let mut get_params = Vec::new();
let mut is_oneof_field = false;
let mut args =
extract_input_args::<args::SubscriptionFieldArgument>(&crate_name, method)?;
if field.oneof {
is_oneof_field = true;
if args.len() != 1 {
return Err(Error::new_spanned(
&method,
"The `oneof` field requires exactly one argument.",
)
.into());
}
let (ident, ty, argument) = args.pop().unwrap();
schema_args.push(quote! {
#crate_name::static_assertions::assert_impl_one!(#ty: #crate_name::OneofObjectType);
if let #crate_name::registry::MetaType::InputObject { input_fields, .. } = registry.create_fake_input_type::<#ty>() {
args.extend(input_fields);
}
});
use_params.push(quote! { #ident });
let validators = argument
.validator
.clone()
.unwrap_or_default()
.create_validators(
&crate_name,
quote!(&#ident),
quote!(#ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
get_params.push(quote! {
#[allow(non_snake_case, unused_variables)]
let (__pos, #ident) = ctx.oneof_param_value::<#ty>()?;
#validators
});
} else {
for (
ident,
ty,
args::SubscriptionFieldArgument {
name,
desc,
default,
default_with,
validator,
visible: arg_visible,
secret,
},
) in &args
{
let name = name.clone().unwrap_or_else(|| {
subscription_args
.rename_args
.rename(ident.ident.unraw().to_string(), RenameTarget::Argument)
});
let desc = desc
.as_ref()
.map(|s| quote! {::std::option::Option::Some(#s)})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let default = generate_default(default, default_with)?;
let schema_default = default
.as_ref()
.map(|value| {
quote! {
::std::option::Option::Some(::std::string::ToString::to_string(
&<#ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let visible = visible_fn(arg_visible);
schema_args.push(quote! {
args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#ty as #crate_name::InputType>::create_type_info(registry),
default_value: #schema_default,
visible: #visible,
is_secret: #secret,
});
});
use_params.push(quote! { #ident });
let default = match default {
Some(default) => {
quote! { ::std::option::Option::Some(|| -> #ty { #default }) }
}
None => quote! { ::std::option::Option::None },
};
let validators = validator.clone().unwrap_or_default().create_validators(
&crate_name,
quote!(&#ident),
quote!(#ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
get_params.push(quote! {
#[allow(non_snake_case)]
let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?;
#validators
});
}
}
let ty = match &method.sig.output {
ReturnType::Type(_, ty) => OutputType::parse(ty)?,
ReturnType::Default => {
@ -78,138 +189,6 @@ pub fn generate(
.into())
}
};
let mut create_ctx = true;
let mut args = Vec::new();
for (idx, arg) in method.sig.inputs.iter_mut().enumerate() {
if let FnArg::Receiver(receiver) = arg {
if idx != 0 {
return Err(Error::new_spanned(
receiver,
"The self receiver must be the first parameter.",
)
.into());
}
} else if let FnArg::Typed(pat) = arg {
if idx == 0 {
return Err(Error::new_spanned(
pat,
"The self receiver must be the first parameter.",
)
.into());
}
match (&*pat.pat, &*pat.ty) {
(Pat::Ident(arg_ident), Type::Path(arg_ty)) => {
args.push((
arg_ident.clone(),
arg_ty.clone(),
parse_graphql_attrs::<args::SubscriptionFieldArgument>(&pat.attrs)?
.unwrap_or_default(),
));
remove_graphql_attrs(&mut pat.attrs);
}
(arg, Type::Reference(TypeReference { elem, .. })) => {
if let Type::Path(path) = elem.as_ref() {
if idx != 1 || path.path.segments.last().unwrap().ident != "Context"
{
return Err(Error::new_spanned(
arg,
"Only types that implement `InputType` can be used as input arguments.",
)
.into());
} else {
create_ctx = false;
}
}
}
_ => {
return Err(Error::new_spanned(arg, "Incorrect argument type").into());
}
}
} else {
return Err(Error::new_spanned(arg, "Incorrect argument type").into());
}
}
if create_ctx {
let arg = syn::parse2::<FnArg>(quote! { _: &#crate_name::Context<'_> }).unwrap();
method.sig.inputs.insert(1, arg);
}
let mut schema_args = Vec::new();
let mut use_params = Vec::new();
let mut get_params = Vec::new();
for (
ident,
ty,
args::SubscriptionFieldArgument {
name,
desc,
default,
default_with,
validator,
visible: arg_visible,
secret,
},
) in &args
{
let name = name.clone().unwrap_or_else(|| {
subscription_args
.rename_args
.rename(ident.ident.unraw().to_string(), RenameTarget::Argument)
});
let desc = desc
.as_ref()
.map(|s| quote! {::std::option::Option::Some(#s)})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let default = generate_default(default, default_with)?;
let schema_default = default
.as_ref()
.map(|value| {
quote! {
::std::option::Option::Some(::std::string::ToString::to_string(
&<#ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let visible = visible_fn(arg_visible);
schema_args.push(quote! {
args.insert(#name, #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#ty as #crate_name::InputType>::create_type_info(registry),
default_value: #schema_default,
visible: #visible,
is_secret: #secret,
});
});
use_params.push(quote! { #ident });
let default = match default {
Some(default) => quote! { ::std::option::Option::Some(|| -> #ty { #default }) },
None => quote! { ::std::option::Option::None },
};
let validators = validator.clone().unwrap_or_default().create_validators(
&crate_name,
quote!(&#ident),
quote!(#ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
get_params.push(quote! {
#[allow(non_snake_case)]
let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?;
#validators
});
}
let res_ty = ty.value_type();
let stream_ty = if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = &res_ty {
let mut r = None;
@ -307,6 +286,7 @@ pub fn generate(
provides: ::std::option::Option::None,
visible: #visible,
compute_complexity: #complexity,
oneof: #is_oneof_field,
});
});

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,13 +6,14 @@ 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;
use crate::args::{self, Argument, Deprecation, Visible};
use crate::args::{self, Deprecation, Visible};
#[derive(Error, Debug)]
pub enum GeneratorError {
@ -141,7 +142,9 @@ pub fn get_cfg_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
.collect()
}
pub fn parse_graphql_attrs<T: FromMeta>(attrs: &[Attribute]) -> GeneratorResult<Option<T>> {
pub fn parse_graphql_attrs<T: FromMeta + Default>(
attrs: &[Attribute],
) -> GeneratorResult<Option<T>> {
for attr in attrs {
if attr.path.is_ident("graphql") {
let meta = attr.parse_meta()?;
@ -237,10 +240,10 @@ pub fn gen_deprecation(deprecation: &Deprecation, crate_name: &TokenStream) -> T
}
}
pub fn extract_input_args(
pub fn extract_input_args<T: FromMeta + Default>(
crate_name: &proc_macro2::TokenStream,
method: &mut ImplItemMethod,
) -> GeneratorResult<Vec<(PatIdent, Type, Argument)>> {
) -> GeneratorResult<Vec<(PatIdent, Type, T)>> {
let mut args = Vec::new();
let mut create_ctx = true;
@ -277,8 +280,7 @@ pub fn extract_input_args(
args.push((
arg_ident.clone(),
pat.ty.as_ref().clone(),
parse_graphql_attrs::<args::Argument>(&pat.attrs)?
.unwrap_or_default(),
parse_graphql_attrs::<T>(&pat.attrs)?.unwrap_or_default(),
));
} else {
create_ctx = false;
@ -289,7 +291,7 @@ pub fn extract_input_args(
args.push((
arg_ident.clone(),
ty.clone(),
parse_graphql_attrs::<args::Argument>(&pat.attrs)?.unwrap_or_default(),
parse_graphql_attrs::<T>(&pat.attrs)?.unwrap_or_default(),
));
remove_graphql_attrs(&mut pat.attrs);
}
@ -307,3 +309,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

@ -34,7 +34,7 @@ impl Query {
let mut connection = Connection::new(start > 0, end < 10000);
connection.append(
(start..end).into_iter().map(|n|
Ok(Edge::new_with_additional_fields(n, n as i32, EmptyFields)),
Ok(Edge::with_additional_fields(n, n as i32, EmptyFields)),
))?;
Ok(connection)
})

View File

@ -4,7 +4,7 @@ In `Async-graphql` most common scalar types are built in, but you can also creat
Using `async-graphql::Scalar`, you can add support for a scalar when you implement it. You only need to implement parsing and output functions.
The following example defines a 64-bit integer scalar where its input and output are strings. (Note: `Async-graphql` already supports 64-bit integers and uses strings as input and output.)
The following example defines a 64-bit integer scalar where its input and output are strings.
```rust
use async_graphql::*;

View File

@ -4,7 +4,7 @@ Different from `SimpleObject`, `Object` must have a resolver defined for each fi
**A resolver function has to be asynchronous. The first argument has to be `&self`, the second is an optional `Context` and it is followed by field arguments.**
The resolvers is used to get the value of the field. For example, you can query a database and return the result. **The return type of the function is the type of the field.** You can also return a `async_graphql::Result` to return an error if it occurs. The error message will then be sent to query result.
The resolver is used to get the value of the field. For example, you can query a database and return the result. **The return type of the function is the type of the field.** You can also return a `async_graphql::Result` to return an error if it occurs. The error message will then be sent as query result.
You may need access to global data in your query, for example a database connection pool.
When creating your `Schema`, you can use `SchemaBuilder::data` to configure the global data, and `Context::data` to configure `Context` data.

@ -1 +1 @@
Subproject commit c8219078a4b7aa6d84d22e9b79f033088897be4b
Subproject commit 61384c8fa50e1395e50015b2be77b4bc6f5de4d2

View File

@ -4,29 +4,31 @@ Comparing Features of Other Rust GraphQL Implementations
**Please let me know if there is anything wrong.**
| | async-graphql | juniper(0.15.1) |
|----------------|---------------|-----------------|
| async/await | 👍 | 👍️ |
| Rustfmt friendly(No DSL) | 👍 | ⛔️ |
| Boilerplate | Less | Some |
| Type Safety | 👍 | 👍 |
| Query | 👍 | 👍 |
| Mutation | 👍 | 👍 |
| Interfaces | 👍 | 👍 |
| Union | 👍 | 👍 |
| Dataloading | 👍 | 👍 |
| Custom Scalar | 👍 | 👍 |
| Custom Error | 👍 | 👍 |
| Custom Directive | 👍 | ⛔ |
| Extensions | 👍 | ⛔️ |
| Cursor Connections | 👍 | ⛔️ |
| Query complexity/depth | 👍 | ⛔️ |
| Input validators | 👍 | ⛔️ |
| Field guard | 👍 | ⛔️ |
| Multipart request(upload file) | 👍 | ⛔️ |
| Subscription | 👍 | 👍️ |
| Opentracing | 👍 | ⛔️ |
| Apollo Federation | 👍 | ⛔️ |
| Apollo Tracing | 👍 | ⛔️ |
| Apollo Persisted Queries | 👍 | ⛔️ |
| Disabling introspection | 👍 | ⛔️ |
| | async-graphql | juniper(0.15.1) |
|------------------------------------------------------------------------|---------------|-----------------|
| async/await | 👍 | 👍️ |
| Rustfmt friendly(No DSL) | 👍 | ⛔️ |
| Boilerplate | Less | Some |
| Type Safety | 👍 | 👍 |
| Query | 👍 | 👍 |
| Mutation | 👍 | 👍 |
| Interfaces | 👍 | 👍 |
| Union | 👍 | 👍 |
| Dataloading | 👍 | 👍 |
| Custom Scalar | 👍 | 👍 |
| Custom Error | 👍 | 👍 |
| Custom Directive | 👍 | ⛔ |
| [Oneof Input Object](https://github.com/graphql/graphql-spec/pull/825) | 👍 | ⛔ |
| [Oneof Field](https://github.com/graphql/graphql-spec/pull/825) | 👍 | ⛔ |
| Extensions | 👍 | ⛔️ |
| Cursor Connections | 👍 | ⛔️ |
| Query complexity/depth | 👍 | ⛔️ |
| Input validators | 👍 | ⛔️ |
| Field guard | 👍 | ⛔️ |
| Multipart request(upload file) | 👍 | ⛔️ |
| Subscription | 👍 | 👍️ |
| Opentracing | 👍 | ⛔️ |
| Apollo Federation | 👍 | ⛔️ |
| Apollo Tracing | 👍 | ⛔️ |
| Apollo Persisted Queries | 👍 | ⛔️ |
| Disabling introspection | 👍 | ⛔️ |

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-actix-web"
version = "3.0.26"
version = "3.0.33"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "async-graphql for actix-web"
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "3.0.26" }
async-graphql = { path = "../..", version = "3.0.33" }
actix = "0.13.0"
actix-http = "3.0.1"
@ -31,6 +31,6 @@ default = []
cbor = ["serde_cbor"]
[dev-dependencies]
actix-rt = "2.2.0"
actix-rt = "2.6.0"
async-mutex = "1.4.0"
serde = { version = "1", features = ["derive"] }

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-axum"
version = "3.0.26"
version = "3.0.33"
authors = ["sunli <scott_s829@163.com>"]
edition = "2021"
description = "async-graphql for axum"
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "axum"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "3.0.26" }
async-graphql = { path = "../..", version = "3.0.33" }
async-trait = "0.1.51"
axum = { version = "0.4", features = ["ws", "headers"] }

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-poem"
version = "3.0.26"
version = "3.0.33"
authors = ["sunli <scott_s829@163.com>"]
edition = "2021"
description = "async-graphql for poem"
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "poem"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "3.0.26" }
async-graphql = { path = "../..", version = "3.0.33" }
poem = { version = "1.2.2", features = ["websocket"] }
futures-util = { version = "0.3.0", default-features = false }

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-rocket"
version = "3.0.26"
version = "3.0.33"
authors = ["Daniel Wiesenberg <daniel@simplificAR.io>"]
edition = "2021"
description = "async-graphql for Rocket.rs"
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "rocket"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "3.0.26" }
async-graphql = { path = "../..", version = "3.0.33" }
rocket = { version = "0.5.0-rc.1", default-features = false }
serde = "1.0.126"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-tide"
version = "3.0.26"
version = "3.0.33"
authors = ["vkill <vkill.net@gmail.com>", "sunli <scott_s829@163.com>"]
edition = "2021"
description = "async-graphql for tide"
@ -16,7 +16,7 @@ default = ["websocket"]
websocket = ["tide-websockets"]
[dependencies]
async-graphql = { path = "../..", version = "3.0.26" }
async-graphql = { path = "../..", version = "3.0.33" }
async-trait = "0.1.48"
futures-util = "0.3.0"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-warp"
version = "3.0.26"
version = "3.0.33"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "async-graphql for warp"
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "3.0.26" }
async-graphql = { path = "../..", version = "3.0.33" }
warp = { version = "0.3.0", default-features = false, features = ["websocket"] }
futures-util = { version = "0.3.0", default-features = false, features = ["sink"] }

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-parser"
version = "3.0.26"
version = "3.0.33"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "GraphQL query parser for async-graphql"
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql-value = { path = "../value", version = "3.0.26" }
async-graphql-value = { path = "../value", version = "3.0.33" }
pest = "2.1.3"
pest_derive = "2.1.0"
serde_json = "1.0.64"

View File

@ -148,6 +148,9 @@ pub trait UnionType: ContainerType {}
/// A GraphQL input object.
pub trait InputObjectType: InputType {}
/// A GraphQL oneof input object.
pub trait OneofObjectType: InputObjectType {}
#[async_trait::async_trait]
impl<T: OutputType + ?Sized> OutputType for Box<T> {
fn type_name() -> Cow<'static, str> {

View File

@ -19,8 +19,8 @@ use crate::parser::types::{
};
use crate::schema::SchemaEnv;
use crate::{
Error, InputType, Lookahead, Name, PathSegment, Pos, Positioned, Result, ServerError,
ServerResult, UploadValue, Value,
Error, InputType, Lookahead, Name, OneofObjectType, PathSegment, Pos, Positioned, Result,
ServerError, ServerResult, UploadValue, Value,
};
/// Data related functions of the context.
@ -604,6 +604,22 @@ impl<'a> ContextBase<'a, &'a Positioned<Field>> {
self.get_param_value(&self.item.node.arguments, name, default)
}
#[doc(hidden)]
pub fn oneof_param_value<T: OneofObjectType>(&self) -> ServerResult<(Pos, T)> {
use indexmap::IndexMap;
let mut map = IndexMap::new();
for (name, value) in &self.item.node.arguments {
let value = self.resolve_input_value(value.clone())?;
map.insert(name.node.clone(), value);
}
InputType::parse(Some(Value::Object(map)))
.map(|value| (self.item.pos, value))
.map_err(|e| e.into_server_error(self.item.pos))
}
/// Creates a uniform interface to inspect the forthcoming selections.
///
/// # Examples

View File

@ -36,6 +36,7 @@ some simple fields, and use the `ComplexObject` macro to define some other field
| complexity | Custom field complexity. | string | Y |
| derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
| flatten | Similar to serde (flatten) | boolean | Y |
| oneof | Oneof field | bool | Y |
# Field argument attributes

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

@ -30,6 +30,7 @@ Define a GraphQL interface
| requires | Annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. | 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 |
| oneof | Oneof field | bool | Y |
# Field argument 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
@ -38,6 +39,7 @@ All methods are converted to camelCase.
| complexity | Custom field complexity. | string | Y |
| derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
| flatten | Similar to serde (flatten) | boolean | Y |
| oneof | Oneof field | bool | Y |
# Field argument 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

@ -32,6 +32,7 @@ The filter function should be synchronous.
| complexity | Custom field complexity. *[See also the Book](https://async-graphql.github.io/async-graphql/en/depth_and_complexity.html).* | bool | Y |
| complexity | Custom field complexity. | string | Y |
| secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
| oneof | Oneof field | bool | Y |
# Field argument attributes

View File

@ -31,7 +31,8 @@ impl<T> OpenTelemetry<T> {
/// Use `tracer` to create an OpenTelemetry extension.
pub fn new(tracer: T) -> OpenTelemetry<T>
where
T: Tracer + Send + Sync,
T: Tracer + Send + Sync + 'static,
<T as Tracer>::Span: Sync + Send,
{
Self {
tracer: Arc::new(tracer),
@ -39,7 +40,11 @@ impl<T> OpenTelemetry<T> {
}
}
impl<T: Tracer + Send + Sync> ExtensionFactory for OpenTelemetry<T> {
impl<T> ExtensionFactory for OpenTelemetry<T>
where
T: Tracer + Send + Sync + 'static,
<T as Tracer>::Span: Sync + Send,
{
fn create(&self) -> Arc<dyn Extension> {
Arc::new(OpenTelemetryExtension {
tracer: self.tracer.clone(),
@ -52,7 +57,11 @@ struct OpenTelemetryExtension<T> {
}
#[async_trait::async_trait]
impl<T: Tracer + Send + Sync> Extension for OpenTelemetryExtension<T> {
impl<T> Extension for OpenTelemetryExtension<T>
where
T: Tracer + Send + Sync + 'static,
<T as Tracer>::Span: Sync + Send,
{
async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
next.run(ctx)
.with_context(OpenTelemetryContext::current_with_span(

View File

@ -213,8 +213,8 @@ pub use async_graphql_value::{
SerializerError, Variables,
};
pub use base::{
ComplexObject, Description, InputObjectType, InputType, InterfaceType, ObjectType, OutputType,
UnionType,
ComplexObject, Description, InputObjectType, InputType, InterfaceType, ObjectType,
OneofObjectType, OutputType, UnionType,
};
pub use custom_directive::{CustomDirective, CustomDirectiveFactory};
pub use error::{
@ -266,6 +266,8 @@ pub use async_graphql_derive::MergedSubscription;
pub use async_graphql_derive::NewType;
#[doc = include_str!("docs/object.md")]
pub use async_graphql_derive::Object;
#[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

@ -50,4 +50,8 @@ impl<'a> __Field<'a> {
async fn deprecation_reason(&self) -> Option<&str> {
self.field.deprecation.reason()
}
async fn one_of(&self) -> bool {
self.field.oneof
}
}

View File

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

View File

@ -69,6 +69,10 @@ impl Registry {
write!(sdl, "\t{}: {}", field.name, field.ty).ok();
}
if field.oneof {
write!(sdl, " @oneof").ok();
}
if federation {
if field.external {
write!(sdl, " @external").ok();
@ -202,12 +206,16 @@ impl Registry {
name,
input_fields,
description,
oneof,
..
} => {
if description.is_some() {
writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description.unwrap()).ok();
}
write!(sdl, "input {} ", name).ok();
if *oneof {
write!(sdl, "@oneof ").ok();
}
writeln!(sdl, "{{").ok();
for field in input_fields.values() {
if let Some(description) = field.description {

View File

@ -157,7 +157,7 @@ impl Deprecation {
pub struct MetaField {
pub name: String,
pub description: Option<&'static str>,
pub args: IndexMap<&'static str, MetaInputValue>,
pub args: IndexMap<String, MetaInputValue>,
pub ty: String,
pub deprecation: Deprecation,
pub cache_control: CacheControl,
@ -166,6 +166,7 @@ pub struct MetaField {
pub provides: Option<&'static str>,
pub visible: Option<MetaVisibleFn>,
pub compute_complexity: Option<ComplexityType>,
pub oneof: bool,
}
#[derive(Clone)]
@ -228,6 +229,7 @@ pub enum MetaType {
input_fields: IndexMap<String, MetaInputValue>,
visible: Option<MetaVisibleFn>,
rust_typename: &'static str,
oneof: bool,
},
}
@ -350,7 +352,7 @@ pub struct MetaDirective {
pub name: &'static str,
pub description: Option<&'static str>,
pub locations: Vec<model::__DirectiveLocation>,
pub args: IndexMap<&'static str, MetaInputValue>,
pub args: IndexMap<String, MetaInputValue>,
pub is_repeatable: bool,
pub visible: Option<MetaVisibleFn>,
}
@ -565,6 +567,7 @@ impl Registry {
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);
@ -576,7 +579,7 @@ impl Registry {
args: {
let mut args = IndexMap::new();
args.insert(
"representations",
"representations".to_string(),
MetaInputValue {
name: "representations",
description: None,
@ -596,6 +599,7 @@ impl Registry {
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);
}
@ -626,6 +630,7 @@ impl Registry {
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);
fields

View File

@ -318,7 +318,7 @@ where
],
args: {
let mut args = IndexMap::new();
args.insert("if", MetaInputValue {
args.insert("if".to_string(), MetaInputValue {
name: "if",
description: Some("Included when true."),
ty: "Boolean!".to_string(),
@ -342,7 +342,7 @@ where
],
args: {
let mut args = IndexMap::new();
args.insert("if", MetaInputValue {
args.insert("if".to_string(), MetaInputValue {
name: "if",
description: Some("Skipped when true."),
ty: "Boolean!".to_string(),

View File

@ -193,6 +193,7 @@ where
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);
@ -212,6 +213,7 @@ where
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);

View File

@ -101,6 +101,7 @@ where
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);
@ -118,6 +119,7 @@ where
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);

View File

@ -1,8 +1,12 @@
use std::borrow::Cow;
use std::sync::Arc;
use crate::parser::types::Field;
use crate::resolver_utils::resolve_list;
use crate::{registry, ContextSelectionSet, OutputType, Positioned, ServerResult, Value};
use crate::{
registry, ContextSelectionSet, InputType, InputValueError, InputValueResult, OutputType,
Positioned, ServerResult, Value,
};
#[async_trait::async_trait]
impl<'a, T: OutputType + 'a> OutputType for &'a [T] {
@ -27,3 +31,83 @@ impl<'a, T: OutputType + 'a> OutputType for &'a [T] {
resolve_list(ctx, field, self.iter(), Some(self.len())).await
}
}
macro_rules! impl_output_slice_for_smart_ptr {
($ty:ty) => {
#[async_trait::async_trait]
impl<T: OutputType> OutputType for $ty {
fn type_name() -> Cow<'static, str> {
Cow::Owned(format!("[{}]", T::qualified_type_name()))
}
fn qualified_type_name() -> String {
format!("[{}]!", T::qualified_type_name())
}
fn create_type_info(registry: &mut registry::Registry) -> String {
T::create_type_info(registry);
Self::qualified_type_name()
}
async fn resolve(
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<Value> {
resolve_list(ctx, field, self.iter(), Some(self.len())).await
}
}
};
}
impl_output_slice_for_smart_ptr!(Box<[T]>);
impl_output_slice_for_smart_ptr!(Arc<[T]>);
macro_rules! impl_input_slice_for_smart_ptr {
($ty:ty) => {
impl<T: InputType> InputType for $ty {
type RawValueType = Self;
fn type_name() -> Cow<'static, str> {
Cow::Owned(format!("[{}]", T::qualified_type_name()))
}
fn qualified_type_name() -> String {
format!("[{}]!", T::qualified_type_name())
}
fn create_type_info(registry: &mut registry::Registry) -> String {
T::create_type_info(registry);
Self::qualified_type_name()
}
fn parse(value: Option<Value>) -> InputValueResult<Self> {
match value.unwrap_or_default() {
Value::List(values) => values
.into_iter()
.map(|value| InputType::parse(Some(value)))
.collect::<Result<_, _>>()
.map_err(InputValueError::propagate),
value => {
Ok(
vec![InputType::parse(Some(value))
.map_err(InputValueError::propagate)?]
.into(),
)
}
}
}
fn to_value(&self) -> Value {
Value::List(self.iter().map(InputType::to_value).collect())
}
fn as_raw_value(&self) -> Option<&Self::RawValueType> {
Some(self)
}
}
};
}
impl_input_slice_for_smart_ptr!(Box<[T]>);
impl_input_slice_for_smart_ptr!(Arc<[T]>);

View File

@ -29,6 +29,8 @@ mod secrecy;
#[cfg(feature = "smol_str")]
mod smol_str;
#[cfg(feature = "time")]
mod time_date;
#[cfg(feature = "time")]
mod time_offset_date_time;
#[cfg(feature = "time")]
mod time_primitive_date_time;

67
src/types/external/time_date.rs vendored Normal file
View File

@ -0,0 +1,67 @@
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
use time::{format_description::FormatItem, macros::format_description, Date};
const DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]");
/// ISO 8601 calendar date without timezone.
/// Format: %Y-%m-%d
///
/// # Examples
///
/// * `1994-11-13`
/// * `2000-02-24`
#[Scalar(internal, name = "Date")]
impl ScalarType for Date {
fn parse(value: Value) -> InputValueResult<Self> {
match &value {
Value::String(s) => Ok(Self::parse(s, &DATE_FORMAT)?),
_ => Err(InputValueError::expected_type(value)),
}
}
fn to_value(&self) -> Value {
Value::String(
self.format(&DATE_FORMAT)
.unwrap_or_else(|e| panic!("Failed to format `Date`: {}", e)),
)
}
}
#[cfg(test)]
mod tests {
use crate::{ScalarType, Value};
use time::{macros::date, Date};
#[test]
fn test_date_to_value() {
let cases = [
(date!(1994 - 11 - 13), "1994-11-13"),
(date!(2000 - 01 - 24), "2000-01-24"),
];
for (value, expected) in cases {
let value = value.to_value();
if let Value::String(s) = value {
assert_eq!(s, expected);
} else {
panic!(
"Unexpected Value type when formatting PrimitiveDateTime: {:?}",
value
);
}
}
}
#[test]
fn test_date_parse() {
let cases = [
("1994-11-13", date!(1994 - 11 - 13)),
("2000-01-24", date!(2000 - 01 - 24)),
];
for (value, expected) in cases {
let value = Value::String(value.to_string());
let parsed = <Date as ScalarType>::parse(value).unwrap();
assert_eq!(parsed, expected);
}
}
}

View File

@ -112,6 +112,7 @@ impl<T: ObjectType> OutputType for QueryRoot<T> {
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);
@ -123,7 +124,7 @@ impl<T: ObjectType> OutputType for QueryRoot<T> {
args: {
let mut args = IndexMap::new();
args.insert(
"name",
"name".to_string(),
registry::MetaInputValue {
name: "name",
description: None,
@ -143,6 +144,7 @@ impl<T: ObjectType> OutputType for QueryRoot<T> {
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);
}

View File

@ -73,6 +73,7 @@ pub fn check_rules(
.with(rules::KnownDirectives::default())
.with(rules::DirectivesUnique::default())
.with(rules::OverlappingFieldsCanBeMerged)
.with(rules::OneofFieldsHaveExactlyOneArgument)
.with(rules::UploadFile)
.with(visitors::CacheControlCalculate {
cache_control: &mut cache_control,

View File

@ -11,7 +11,7 @@ use crate::{Name, Positioned, QueryPathSegment};
#[derive(Default)]
pub struct ArgumentsOfCorrectType<'a> {
current_args: Option<&'a IndexMap<&'static str, MetaInputValue>>,
current_args: Option<&'a IndexMap<String, MetaInputValue>>,
}
impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> {
@ -983,4 +983,82 @@ mod tests {
"#,
);
}
#[test]
fn oneof() {
expect_passes_rule!(
factory,
r#"
{
oneofArg(arg: {a: 10})
}
"#,
);
expect_passes_rule!(
factory,
r#"
{
oneofArg(arg: {b: "abc"})
}
"#,
);
expect_fails_rule!(
factory,
r#"
{
oneofArg(arg: {a: 10, b: "abc"})
}
"#,
);
}
#[test]
fn oneof_opt() {
expect_passes_rule!(
factory,
r#"
{
oneofOpt(arg: {a: 10})
}
"#,
);
expect_passes_rule!(
factory,
r#"
{
oneofOpt(arg: {b: "abc"})
}
"#,
);
expect_passes_rule!(
factory,
r#"
{
oneofOpt
}
"#,
);
expect_passes_rule!(
factory,
r#"
{
oneofOpt(arg: null)
}
"#,
);
expect_fails_rule!(
factory,
r#"
{
oneofOpt(arg: {a: 10, b: "abc"})
}
"#,
);
}
}

View File

@ -17,7 +17,7 @@ enum ArgsType<'a> {
#[derive(Default)]
pub struct KnownArgumentNames<'a> {
current_args: Option<(&'a IndexMap<&'static str, MetaInputValue>, ArgsType<'a>)>,
current_args: Option<(&'a IndexMap<String, MetaInputValue>, ArgsType<'a>)>,
}
impl<'a> KnownArgumentNames<'a> {
@ -26,7 +26,7 @@ impl<'a> KnownArgumentNames<'a> {
" Did you mean",
self.current_args
.iter()
.map(|(args, _)| args.iter().map(|arg| *arg.0))
.map(|(args, _)| args.iter().map(|arg| arg.0.as_str()))
.flatten(),
name,
)

View File

@ -11,6 +11,7 @@ mod no_fragment_cycles;
mod no_undefined_variables;
mod no_unused_fragments;
mod no_unused_variables;
mod oneof_fields_have_exactly_one_argument;
mod overlapping_fields_can_be_merged;
mod possible_fragment_spreads;
mod provided_non_null_arguments;
@ -34,6 +35,7 @@ pub use no_fragment_cycles::NoFragmentCycles;
pub use no_undefined_variables::NoUndefinedVariables;
pub use no_unused_fragments::NoUnusedFragments;
pub use no_unused_variables::NoUnusedVariables;
pub use oneof_fields_have_exactly_one_argument::OneofFieldsHaveExactlyOneArgument;
pub use overlapping_fields_can_be_merged::OverlappingFieldsCanBeMerged;
pub use possible_fragment_spreads::PossibleFragmentSpreads;
pub use provided_non_null_arguments::ProvidedNonNullArguments;

View File

@ -0,0 +1,66 @@
use crate::validation::visitor::{RuleError, Visitor};
use crate::VisitorContext;
use async_graphql_parser::types::Field;
use async_graphql_parser::Positioned;
pub struct OneofFieldsHaveExactlyOneArgument;
impl<'a> Visitor<'a> for OneofFieldsHaveExactlyOneArgument {
fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned<Field>) {
if let Some(parent_type) = ctx.parent_type() {
if let Some(field_def) = parent_type
.fields()
.and_then(|fields| fields.get(field.node.name.node.as_str()))
{
if field_def.oneof && field.node.arguments.len() != 1 {
ctx.errors.push(RuleError::new(
vec![field.pos],
"Oneof fields requires have exactly one argument".to_string(),
));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn factory() -> OneofFieldsHaveExactlyOneArgument {
OneofFieldsHaveExactlyOneArgument
}
#[test]
fn oneof_field() {
expect_passes_rule!(
factory,
r#"
query Foo {
oneofField(a: 10)
}
"#,
);
}
#[test]
fn oneof_not_exactly_one_argument() {
expect_fails_rule!(
factory,
r#"
query Foo {
oneofField(a: 10, b: "abc")
}
"#,
);
expect_fails_rule!(
factory,
r#"
query Foo {
oneofField
}
"#,
);
}
}

View File

@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use crate::parser::types::{Field, Selection, SelectionSet};
use crate::validation::visitor::{Visitor, VisitorContext};
@ -15,6 +15,7 @@ impl<'a> Visitor<'a> for OverlappingFieldsCanBeMerged {
) {
let mut find_conflicts = FindConflicts {
outputs: Default::default(),
visited: Default::default(),
ctx,
};
find_conflicts.find(selection_set);
@ -23,6 +24,7 @@ impl<'a> Visitor<'a> for OverlappingFieldsCanBeMerged {
struct FindConflicts<'a, 'ctx> {
outputs: HashMap<&'a str, &'a Positioned<Field>>,
visited: HashSet<&'a str>,
ctx: &'a mut VisitorContext<'ctx>,
}
@ -46,6 +48,13 @@ impl<'a, 'ctx> FindConflicts<'a, 'ctx> {
if let Some(fragment) =
self.ctx.fragment(&fragment_spread.node.fragment_name.node)
{
if !self
.visited
.insert(fragment_spread.node.fragment_name.node.as_str())
{
// To avoid recursing itself, this error is detected by the `NoFragmentCycles` validator.
continue;
}
self.find(&fragment.node.selection_set);
}
}

View File

@ -18,18 +18,20 @@ fn levenshtein_distance(s1: &str, s2: &str) -> usize {
column[s1.len()]
}
pub fn make_suggestion<'a, I>(prefix: &str, options: I, input: &str) -> Option<String>
pub fn make_suggestion<I, A>(prefix: &str, options: I, input: &str) -> Option<String>
where
I: Iterator<Item = &'a str>,
I: IntoIterator<Item = A>,
A: AsRef<str>,
{
let mut selected = Vec::new();
let mut distances = HashMap::new();
for opt in options {
let distance = levenshtein_distance(input, opt);
let opt = opt.as_ref().to_string();
let distance = levenshtein_distance(input, &opt);
let threshold = (input.len() / 2).max((opt.len() / 2).max(1));
if distance < threshold {
selected.push(opt);
selected.push(opt.clone());
distances.insert(opt, distance);
}
}

View File

@ -288,6 +288,13 @@ impl ComplicatedArgs {
}
}
#[derive(OneofObject)]
#[graphql(internal)]
enum OneofArg {
A(i32),
B(String),
}
pub struct Query;
#[Object(internal)]
@ -335,6 +342,19 @@ impl Query {
async fn complicated_args(&self) -> Option<ComplicatedArgs> {
unimplemented!()
}
async fn oneof_arg(&self, arg: OneofArg) -> String {
unimplemented!()
}
async fn oneof_opt(&self, arg: Option<OneofArg>) -> String {
unimplemented!()
}
#[graphql(oneof)]
async fn oneof_field(&self, arg: OneofArg) -> String {
unimplemented!()
}
}
pub struct Mutation;

View File

@ -120,9 +120,17 @@ pub fn is_valid_input_value(
registry::MetaType::InputObject {
input_fields,
name: object_name,
oneof,
..
} => match value {
ConstValue::Object(values) => {
if *oneof && values.len() != 1 {
return Some(valid_error(
&path_node,
"Oneof input objects requires have exactly one field".to_string(),
));
}
let mut input_names =
values.keys().map(AsRef::as_ref).collect::<HashSet<_>>();

View File

@ -365,6 +365,70 @@ async fn test_flatten() {
);
}
#[tokio::test]
async fn test_flatten_with_context() {
#[derive(SimpleObject)]
struct A {
a: i32,
b: i32,
}
#[derive(SimpleObject)]
#[graphql(complex)]
struct B {
#[graphql(skip)]
a: A,
c: i32,
}
#[ComplexObject]
impl B {
#[graphql(flatten)]
async fn a(&self, _ctx: &Context<'_>) -> &A {
&self.a
}
}
struct Query;
#[Object]
impl Query {
async fn obj(&self) -> B {
B {
a: A { a: 100, b: 200 },
c: 300,
}
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = "{ __type(name: \"B\") { fields { name } } }";
assert_eq!(
schema.execute(query).await.data,
value!({
"__type": {
"fields": [
{"name": "c"},
{"name": "a"},
{"name": "b"}
]
}
})
);
let query = "{ obj { a b c } }";
assert_eq!(
schema.execute(query).await.data,
value!({
"obj": {
"a": 100,
"b": 200,
"c": 300,
}
})
);
}
#[tokio::test]
async fn test_flatten_with_result() {
#[derive(SimpleObject)]
@ -428,3 +492,87 @@ async fn test_flatten_with_result() {
})
);
}
#[tokio::test]
async fn test_oneof_field() {
#[derive(OneofObject)]
enum TestArg {
A(i32),
B(String),
}
#[derive(SimpleObject)]
#[graphql(complex)]
struct Query {
a: i32,
}
#[ComplexObject]
impl Query {
#[graphql(oneof)]
async fn test(&self, arg: TestArg) -> String {
match arg {
TestArg::A(a) => format!("a:{}", a),
TestArg::B(b) => format!("b:{}", b),
}
}
}
let schema = Schema::new(Query { a: 10 }, EmptyMutation, EmptySubscription);
let query = "{ test(a: 10) }";
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"test": "a:10"
})
);
let query = r#"{ test(b: "abc") }"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"test": "b:abc"
})
);
let query = r#"{
__type(name: "Query") {
fields {
name
args {
name
type {
kind
name
}
}
}
}
}"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"__type": {
"fields": [{
"name": "a",
"args": []
}, {
"name": "test",
"args": [{
"name": "a",
"type": {
"kind": "SCALAR",
"name": "Int"
}
}, {
"name": "b",
"type": {
"kind": "SCALAR",
"name": "String"
}
}]
}]
}
})
);
}

View File

@ -479,3 +479,45 @@ pub async fn test_skip_output() {
})
);
}
#[tokio::test]
pub async fn test_complex_output() {
#[derive(SimpleObject, InputObject)]
#[graphql(input_name = "MyObjectInput")]
#[graphql(complex)]
#[allow(dead_code)]
struct MyObject {
a: i32,
}
#[ComplexObject]
impl MyObject {
async fn double(&self) -> i32 {
self.a * 2
}
}
struct Query;
#[Object]
impl Query {
async fn obj(&self, input: MyObject) -> MyObject {
input
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema
.execute("{ obj(input: { a: 1 }) { a, double } }")
.await
.into_result()
.unwrap()
.data,
value!({
"obj": {
"a": 1,
"double": 2,
}
})
);
}

View File

@ -55,3 +55,29 @@ pub async fn test_input_box_str() {
})
);
}
#[tokio::test]
pub async fn test_input_box_slice() {
struct Query;
#[Object]
impl Query {
async fn box_slice(&self, s: Box<[i32]>) -> Box<[i32]> {
s
}
async fn arc_slice(&self, s: Arc<[i32]>) -> Arc<[i32]> {
s
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = r#"{ boxSlice(s: [1, 2, 3]) arcSlice(s: [4, 5, 6]) }"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"boxSlice": [1, 2, 3],
"arcSlice": [4, 5, 6],
})
);
}

View File

@ -473,3 +473,118 @@ pub async fn test_issue_330() {
})
);
}
#[tokio::test]
pub async fn test_oneof() {
#[derive(OneofObject)]
enum TestArg {
A(i32),
B(String),
}
struct A;
#[Object]
impl A {
async fn test(&self, arg: TestArg) -> String {
match arg {
TestArg::A(a) => format!("A:a:{}", a),
TestArg::B(b) => format!("A:b:{}", b),
}
}
}
struct B;
#[Object]
impl B {
async fn test(&self, arg: TestArg) -> String {
match arg {
TestArg::A(a) => format!("B:a:{}", a),
TestArg::B(b) => format!("B:b:{}", b),
}
}
}
#[derive(Interface)]
#[graphql(field(
name = "test",
type = "String",
oneof,
arg(name = "arg", type = "TestArg")
))]
enum Obj {
A(A),
B(B),
}
struct Query;
#[Object]
impl Query {
async fn a(&self) -> Obj {
A.into()
}
async fn b(&self) -> Obj {
B.into()
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema
.execute("{ a { test(a: 10) } }")
.await
.into_result()
.unwrap()
.data,
value!({
"a": {
"test": "A:a:10"
}
})
);
assert_eq!(
schema
.execute("{ a { test(b: \"abc\") } }")
.await
.into_result()
.unwrap()
.data,
value!({
"a": {
"test": "A:b:abc"
}
})
);
assert_eq!(
schema
.execute("{ b { test(a: 10) } }")
.await
.into_result()
.unwrap()
.data,
value!({
"b": {
"test": "B:a:10"
}
})
);
assert_eq!(
schema
.execute("{ b { test(b: \"def\") } }")
.await
.into_result()
.unwrap()
.data,
value!({
"b": {
"test": "B:b:def"
}
})
);
}

View File

@ -58,3 +58,139 @@ async fn test_flatten() {
})
);
}
#[tokio::test]
async fn test_flatten_with_context() {
#[derive(SimpleObject)]
struct A {
a: i32,
b: i32,
}
struct B;
#[Object]
impl B {
#[graphql(flatten)]
async fn a(&self, _ctx: &Context<'_>) -> A {
A { a: 100, b: 200 }
}
async fn c(&self) -> i32 {
300
}
}
struct Query;
#[Object]
impl Query {
async fn obj(&self) -> B {
B
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = "{ __type(name: \"B\") { fields { name } } }";
assert_eq!(
schema.execute(query).await.data,
value!({
"__type": {
"fields": [
{"name": "a"},
{"name": "b"},
{"name": "c"}
]
}
})
);
let query = "{ obj { a b c } }";
assert_eq!(
schema.execute(query).await.data,
value!({
"obj": {
"a": 100,
"b": 200,
"c": 300,
}
})
);
}
#[tokio::test]
async fn test_oneof_field() {
#[derive(OneofObject)]
enum TestArg {
A(i32),
B(String),
}
struct Query;
#[Object]
impl Query {
#[graphql(oneof)]
async fn test(&self, arg: TestArg) -> String {
match arg {
TestArg::A(a) => format!("a:{}", a),
TestArg::B(b) => format!("b:{}", b),
}
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = "{ test(a: 10) }";
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"test": "a:10"
})
);
let query = r#"{ test(b: "abc") }"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"test": "b:abc"
})
);
let query = r#"{
__type(name: "Query") {
fields {
name
args {
name
type {
kind
name
}
}
}
}
}"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"__type": {
"fields": [{
"name": "test",
"args": [{
"name": "a",
"type": {
"kind": "SCALAR",
"name": "Int"
}
}, {
"name": "b",
"type": {
"kind": "SCALAR",
"name": "String"
}
}]
}]
}
})
);
}

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")"#
);
}

View File

@ -54,3 +54,51 @@ async fn test_flatten() {
})
);
}
#[tokio::test]
async fn recursive_fragment_definition() {
#[derive(SimpleObject)]
struct Hello {
world: String,
}
struct Query;
// this setup is actually completely irrelevant we just need to be able ot execute a query
#[Object]
impl Query {
async fn obj(&self) -> Hello {
Hello {
world: "Hello World".into(),
}
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = "fragment f on Query {...f} { __typename }";
assert!(schema.execute(query).await.into_result().is_err());
}
#[tokio::test]
async fn recursive_fragment_definition_nested() {
#[derive(SimpleObject)]
struct Hello {
world: String,
}
struct Query;
// this setup is actually completely irrelevant we just need to be able ot execute a query
#[Object]
impl Query {
async fn obj(&self) -> Hello {
Hello {
world: "Hello World".into(),
}
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = "fragment f on Query { a { ...f a { ...f } } } { __typename }";
assert!(schema.execute(query).await.into_result().is_err());
}

View File

@ -425,3 +425,46 @@ pub async fn test_subscription_fieldresult() {
assert!(stream.next().await.is_none());
}
#[tokio::test]
pub async fn test_oneof_field() {
#[derive(OneofObject)]
enum TestArg {
A(i32),
B(String),
}
struct Subscription;
#[Subscription]
impl Subscription {
#[graphql(oneof)]
async fn test(&self, arg: TestArg) -> impl Stream<Item = String> {
let value = match arg {
TestArg::A(a) => format!("a:{}", a),
TestArg::B(b) => format!("b:{}", b),
};
futures_util::stream::once(async move { value })
}
}
let schema = Schema::new(Query, EmptyMutation, Subscription);
let mut stream = schema.execute_stream(r#"subscription { test(a: 10) }"#);
assert_eq!(
Response::new(value!({
"test": "a:10"
})),
stream.next().await.unwrap()
);
assert!(stream.next().await.is_none());
let mut stream = schema.execute_stream(r#"subscription { test(b: "abc") }"#);
assert_eq!(
Response::new(value!({
"test": "b:abc"
})),
stream.next().await.unwrap()
);
assert!(stream.next().await.is_none());
}

View File

@ -789,3 +789,103 @@ pub async fn test_list_both_max_items_and_max_length() {
})
);
}
#[tokio::test]
pub async fn test_custom_validator_on_oneof_field() {
struct MyValidator;
impl CustomValidator<TestArg> for MyValidator {
fn check(&self, value: &TestArg) -> Result<(), String> {
match value {
TestArg::A(a) => {
if *a < 100 {
Ok(())
} else {
Err("invalid a".into())
}
}
TestArg::B(b) => {
if b.len() < 5 {
Ok(())
} else {
Err("invalid b".into())
}
}
}
}
}
#[derive(OneofObject)]
enum TestArg {
A(i32),
B(String),
}
struct Query;
#[Object]
impl Query {
#[graphql(oneof)]
async fn test(&self, #[graphql(validator(custom = "MyValidator"))] arg: TestArg) -> String {
match arg {
TestArg::A(a) => format!("a:{}", a),
TestArg::B(b) => format!("b:{}", b),
}
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema
.execute("{ test(a: 10) }")
.await
.into_result()
.unwrap()
.data,
value!({
"test": "a:10"
})
);
assert_eq!(
schema
.execute("{ test(a: 200) }")
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "TestArg": invalid a"#.to_string(),
source: None,
locations: vec![Pos { column: 3, line: 1 }],
path: vec![PathSegment::Field("test".to_string())],
extensions: None
}]
);
assert_eq!(
schema
.execute(r#"{ test(b: "abcd") }"#)
.await
.into_result()
.unwrap()
.data,
value!({
"test": "b:abcd"
})
);
assert_eq!(
schema
.execute(r#"{ test(b: "abcde") }"#)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "TestArg": invalid b"#.to_string(),
source: None,
locations: vec![Pos { column: 3, line: 1 }],
path: vec![PathSegment::Field("test".to_string())],
extensions: None
}]
);
}

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-value"
version = "3.0.26"
version = "3.0.33"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "GraphQL value for async-graphql"