Merge branch 'master' into feat-update-chrono-tz

This commit is contained in:
Sunli 2022-03-18 11:03:27 +08:00 committed by GitHub
commit bae68cdff8
63 changed files with 1525 additions and 462 deletions

View File

@ -4,6 +4,25 @@ 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.35] 2022-3-14
- Make `HashMap` more generics for `InputOutput` and `OutputType`.
- Add support `group` attribute to Object/SimpleObject/ComplexObject/Subscription macros. [#838](https://github.com/async-graphql/async-graphql/issues/838)
- Fixed recursive generic input objects failing to compile. [#859](https://github.com/async-graphql/async-graphql/issues/859)
- Add `ErrorExtensionValues::get` method. [#855](https://github.com/async-graphql/async-graphql/issues/855)
# [3.0.34] 2022-3-5
- Export `@oneOf` directive to SDL when Oneof type is defined. [#766](https://github.com/async-graphql/async-graphql/issues/766)
# [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.

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql"
version = "3.0.31"
version = "3.0.35"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "A GraphQL server library implemented in Rust"
@ -24,12 +24,11 @@ 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.31" }
async-graphql-value = { path = "value", version = "3.0.31" }
async-graphql-parser = { path = "parser", version = "3.0.31" }
async-graphql-derive = { path = "derive", version = "3.0.35" }
async-graphql-value = { path = "value", version = "3.0.35" }
async-graphql-parser = { path = "parser", version = "3.0.35" }
async-stream = "0.3.0"
async-trait = "0.1.48"
@ -53,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.6.1", 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 }
@ -71,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

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

View File

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

View File

@ -200,6 +200,8 @@ pub struct SimpleObject {
// for InputObject
#[darling(default)]
pub input_name: Option<String>,
#[darling(default)]
pub guard: Option<SpannedValue<String>>,
}
#[derive(FromMeta, Default)]
@ -229,6 +231,8 @@ pub struct Object {
pub serial: bool,
#[darling(multiple, rename = "concrete")]
pub concretes: Vec<ConcreteType>,
#[darling(default)]
pub guard: Option<SpannedValue<String>>,
}
pub enum ComplexityType {
@ -271,6 +275,7 @@ pub struct ObjectField {
#[darling(default, multiple)]
pub derived: Vec<DerivedField>,
pub flatten: bool,
pub oneof: bool,
}
#[derive(FromMeta, Default, Clone)]
@ -401,7 +406,6 @@ pub struct InputObject {
pub complex: bool,
}
#[cfg(feature = "unstable_oneof")]
#[derive(FromVariant)]
#[darling(attributes(graphql), forward_attrs(doc))]
pub struct OneofObjectField {
@ -419,7 +423,6 @@ pub struct OneofObjectField {
pub secret: bool,
}
#[cfg(feature = "unstable_oneof")]
#[derive(FromDeriveInput)]
#[darling(attributes(graphql), forward_attrs(doc))]
pub struct OneofObject {
@ -459,7 +462,7 @@ pub struct InterfaceFieldArgument {
#[derive(FromMeta)]
pub struct InterfaceField {
pub name: String,
pub name: SpannedValue<String>,
#[darling(rename = "type")]
pub ty: LitStr,
#[darling(default)]
@ -478,6 +481,8 @@ pub struct InterfaceField {
pub requires: Option<String>,
#[darling(default)]
pub visible: Option<Visible>,
#[darling(default)]
pub oneof: bool,
}
#[derive(FromVariant)]
@ -530,6 +535,8 @@ pub struct Subscription {
pub use_type_description: bool,
pub extends: bool,
pub visible: Option<Visible>,
#[darling(default)]
pub guard: Option<SpannedValue<String>>,
}
#[derive(FromMeta, Default)]
@ -553,11 +560,11 @@ pub struct SubscriptionField {
pub guard: Option<SpannedValue<String>>,
pub visible: Option<Visible>,
pub complexity: Option<ComplexityType>,
pub oneof: bool,
}
#[derive(FromField)]
pub struct MergedObjectField {
pub ident: Option<Ident>,
pub ty: Type,
}
@ -585,7 +592,6 @@ pub struct MergedObject {
#[derive(FromField)]
pub struct MergedSubscriptionField {
pub ident: Option<Ident>,
pub ty: Type,
}
@ -736,6 +742,7 @@ pub struct ComplexObject {
pub name: Option<String>,
pub rename_fields: Option<RenameRule>,
pub rename_args: Option<RenameRule>,
pub guard: Option<SpannedValue<String>>,
}
#[derive(FromMeta, Default)]
@ -754,6 +761,7 @@ pub struct ComplexObjectField {
#[darling(multiple)]
pub derived: Vec<DerivedField>,
pub flatten: bool,
pub oneof: bool,
}
#[derive(FromMeta, Default)]

View File

@ -126,7 +126,7 @@ pub fn generate(
if method_args.flatten {
// Only used to inject the context placeholder if required.
extract_input_args(&crate_name, method)?;
extract_input_args::<args::Argument>(&crate_name, method)?;
let ty = match &method.sig.output {
ReturnType::Type(_, ty) => OutputType::parse(ty)?,
@ -190,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 => {
@ -201,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);
@ -350,6 +386,7 @@ pub fn generate(
requires: #requires,
visible: #visible,
compute_complexity: #complexity,
oneof: #is_oneof_field,
}));
});
@ -380,7 +417,7 @@ pub fn generate(
let guard_map_err = quote! {
.map_err(|err| err.into_server_error(ctx.item.pos))
};
let guard = match &method_args.guard {
let guard = match method_args.guard.as_ref().or(object_args.guard.as_ref()) {
Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?),
None => None,
};

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

@ -289,7 +289,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
#crate_name::Value::Object(map)
}
fn __internal_federation_fields() -> ::std::option::Option<::std::string::String> {
fn __internal_federation_fields() -> ::std::option::Option<::std::string::String> where Self: #crate_name::InputType {
#get_federation_fields
}
}
@ -336,5 +336,6 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
}
quote!(#(#code)*)
};
Ok(expanded.into())
}

View File

@ -136,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 {
@ -145,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,
)
};
@ -170,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 {
@ -275,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,7 +15,6 @@ mod merged_object;
mod merged_subscription;
mod newtype;
mod object;
#[cfg(feature = "unstable_oneof")]
mod oneof_object;
mod output_type;
mod scalar;
@ -220,8 +219,6 @@ pub fn Directive(args: TokenStream, input: TokenStream) -> TokenStream {
}
}
#[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 =

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)?,
@ -262,7 +270,7 @@ pub fn generate(
if method_args.flatten {
// Only used to inject the context placeholder if required.
extract_input_args(&crate_name, method)?;
extract_input_args::<args::Argument>(&crate_name, method)?;
let ty = match &method.sig.output {
ReturnType::Type(_, ty) => OutputType::parse(ty)?,
@ -326,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 => {
@ -340,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);
@ -485,6 +531,7 @@ pub fn generate(
requires: #requires,
visible: #visible,
compute_complexity: #complexity,
oneof: #is_oneof_field,
});
});
@ -515,7 +562,7 @@ pub fn generate(
let guard_map_err = quote! {
.map_err(|err| err.into_server_error(ctx.item.pos))
};
let guard = match &method_args.guard {
let guard = match method_args.guard.as_ref().or(object_args.guard.as_ref()) {
Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?),
None => None,
};

View File

@ -148,7 +148,7 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult<TokenStream>
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)))
::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()))
}
@ -170,6 +170,9 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult<TokenStream>
::std::option::Option::Some(self)
}
}
impl #crate_name::InputObjectType for #ident {}
impl #crate_name::OneofObjectType for #ident {}
}
} else {
let mut code = Vec::new();
@ -195,7 +198,7 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult<TokenStream>
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)))
::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()))
}
@ -247,6 +250,7 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult<TokenStream>
}
impl #crate_name::InputObjectType for #concrete_type {}
impl #crate_name::OneofObjectType for #concrete_type {}
};
code.push(expanded);
}

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 {
@ -183,7 +184,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
let guard_map_err = quote! {
.map_err(|err| err.into_server_error(ctx.item.pos))
};
let guard = match &field.guard {
let guard = match field.guard.as_ref().or(object_args.guard.as_ref()) {
Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?),
None => None,
};

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,
});
});
@ -325,7 +305,7 @@ pub fn generate(
.with_path(::std::vec![#crate_name::PathSegment::Field(::std::borrow::ToOwned::to_owned(&*field_name))])
})
};
let guard = match &field.guard {
let guard = match field.guard.as_ref().or(subscription_args.guard.as_ref()) {
Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?),
None => None,
};

View File

@ -13,7 +13,7 @@ use syn::{
};
use thiserror::Error;
use crate::args::{self, Argument, Deprecation, Visible};
use crate::args::{self, Deprecation, Visible};
#[derive(Error, Debug)]
pub enum GeneratorError {
@ -142,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()?;
@ -238,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;
@ -278,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;
@ -290,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);
}

View File

@ -6,7 +6,7 @@ Defining a cursor connection in `async-graphql` is very simple, you just call th
```rust
use async_graphql::*;
use async_graphql::connection::*;
use async_graphql::types::connection::*;
struct Query;
@ -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

@ -8,7 +8,7 @@ Relay定义了一套游标连接规范以提供一致性的分页查询方式
```rust
use async_graphql::*;
use async_graphql::connection::*;
use async_graphql::types::connection::*;
struct Query;
@ -42,4 +42,4 @@ impl Query {
})
}
}
```
```

@ -1 +1 @@
Subproject commit 840fd0aa36f6fa4633047ef3000bb51b35e26b7b
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.31"
version = "3.0.35"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "async-graphql for actix-web"
@ -12,12 +12,12 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "3.0.31" }
async-graphql = { path = "../..", version = "3.0.35" }
actix = "0.12.0"
actix-http = "3.0.0-rc.2"
actix-web = { version = "4.0.0-rc.3", default-features = false }
actix-web-actors = "4.0.0-beta.12"
actix = "0.13.0"
actix-http = "3.0.1"
actix-web = { version = "4.0.1", default-features = false }
actix-web-actors = "4.1.0"
async-channel = "1.6.1"
futures-util = { version = "0.3.0", default-features = false }
serde_json = "1.0.64"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-axum"
version = "3.0.31"
version = "3.0.35"
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.31" }
async-graphql = { path = "../..", version = "3.0.35" }
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.31"
version = "3.0.35"
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.31" }
async-graphql = { path = "../..", version = "3.0.35" }
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.31"
version = "3.0.35"
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.31" }
async-graphql = { path = "../..", version = "3.0.35" }
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.31"
version = "3.0.35"
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.31" }
async-graphql = { path = "../..", version = "3.0.35" }
async-trait = "0.1.48"
futures-util = "0.3.0"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-warp"
version = "3.0.31"
version = "3.0.35"
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.31" }
async-graphql = { path = "../..", version = "3.0.35" }
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.31"
version = "3.0.35"
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.31" }
async-graphql-value = { path = "../value", version = "3.0.35" }
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

@ -15,6 +15,7 @@ some simple fields, and use the `ComplexObject` macro to define some other field
| 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 |
| rename_args | Rename all the arguments according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE". | string | Y |
| guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | Y |
# Field attributes
@ -36,6 +37,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

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

@ -18,6 +18,7 @@ All methods are converted to camelCase.
| 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 |
| guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | Y |
# Field attributes
@ -39,6 +40,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
@ -188,4 +190,39 @@ assert_eq!(res, value!({
]
}));
# });
```
```
# Oneof field
```rust
use async_graphql::*;
#[derive(OneofObject)]
enum MyInputObject {
A(i32),
B(String),
}
struct Query;
#[Object]
impl Query {
#[graphql(oneof)]
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(a:100)
value2: value(b:"abc")
}"#).await.into_result().unwrap().data;
assert_eq!(res, value!({ "value1": "a:100", "value2": "b:abc" }));
# });
```

View File

@ -35,7 +35,6 @@ struct Query;
#[Object]
impl Query {
/// value
async fn value(&self, input: MyInputObject) -> String {
match input {
MyInputObject::A(value) => format!("a:{}", value),

View File

@ -16,6 +16,7 @@ Similar to `Object`, but defined on a structure that automatically generates get
| 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. *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_simple_object.html#generic-simpleobjects) | ConcreteType | Y |
| serial | Resolve each field sequentially. | bool | Y |
| guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | Y |
# Field attributes

View File

@ -18,6 +18,7 @@ The filter function should be synchronous.
| 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 |
| use_type_description | Specifies that the description of the type is on the type declaration. [`Description`]()(derive.Description.html) | bool | Y |
| guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | Y |
# Field attributes
@ -32,6 +33,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

@ -24,6 +24,11 @@ impl ErrorExtensionValues {
pub fn unset(&mut self, name: impl AsRef<str>) {
self.0.remove(name.as_ref());
}
/// Get an extension value.
pub fn get(&self, name: impl AsRef<str>) -> Option<&Value> {
self.0.get(name.as_ref())
}
}
/// An error in a GraphQL server.

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

@ -76,7 +76,6 @@
//! - `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
//!
@ -214,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::{
@ -267,8 +266,6 @@ 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")]

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

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

View File

@ -6,6 +6,17 @@ impl Registry {
pub fn export_sdl(&self, federation: bool) -> String {
let mut sdl = String::new();
let has_oneof = self.types.values().any(|ty| match ty {
MetaType::InputObject { oneof: true, .. } => true,
MetaType::Object { fields, .. } => fields.values().any(|field| field.oneof),
_ => false,
});
if has_oneof {
sdl.write_str("directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION\n\n")
.ok();
}
for ty in self.types.values() {
if ty.name().starts_with("__") {
continue;
@ -69,6 +80,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,7 +217,6 @@ impl Registry {
name,
input_fields,
description,
#[cfg(feature = "unstable_oneof")]
oneof,
..
} => {
@ -210,7 +224,6 @@ impl Registry {
writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description.unwrap()).ok();
}
write!(sdl, "input {} ", name).ok();
#[cfg(feature = "unstable_oneof")]
if *oneof {
write!(sdl, "@oneof ").ok();
}

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)]
@ -351,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>,
}
@ -566,6 +567,7 @@ impl Registry {
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);
@ -577,7 +579,7 @@ impl Registry {
args: {
let mut args = IndexMap::new();
args.insert(
"representations",
"representations".to_string(),
MetaInputValue {
name: "representations",
description: None,
@ -597,6 +599,7 @@ impl Registry {
provides: None,
visible: None,
compute_complexity: None,
oneof: false,
},
);
}
@ -627,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

@ -26,7 +26,7 @@ pub struct EmptyFields;
///
/// ```rust
/// use async_graphql::*;
/// use async_graphql::connection::*;
/// use async_graphql::types::connection::*;
///
/// struct Query;
///
@ -116,7 +116,7 @@ where
///
/// ```rust
/// use async_graphql::*;
/// use async_graphql::connection::*;
/// use async_graphql::types::connection::*;
///
/// #[derive(SimpleObject)]
/// struct MyEdge {

View File

@ -1,7 +1,7 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Display;
use std::hash::Hash;
use std::hash::{BuildHasher, Hash};
use std::str::FromStr;
use async_graphql_parser::types::Field;
@ -17,11 +17,12 @@ use crate::{
ServerResult, Value,
};
impl<K, V> InputType for HashMap<K, V>
impl<K, V, S> InputType for HashMap<K, V, S>
where
K: ToString + FromStr + Eq + Hash + Send + Sync,
K::Err: Display,
V: Serialize + DeserializeOwned + Send + Sync,
S: Default + BuildHasher + Send + Sync,
{
type RawValueType = Self;
@ -75,10 +76,11 @@ where
}
#[async_trait::async_trait]
impl<K, V> OutputType for HashMap<K, V>
impl<K, V, S> OutputType for HashMap<K, V, S>
where
K: ToString + Eq + Hash + Send + Sync,
V: Serialize + Send + Sync,
S: Send + Sync,
{
fn type_name() -> Cow<'static, str> {
Cow::Borrowed("JSONObject")

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,100 @@
use async_graphql_parser::types::Field;
use async_graphql_parser::Positioned;
use crate::validation::visitor::{RuleError, Visitor};
use crate::{Value, VisitorContext};
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 {
if field.node.arguments.len() != 1 {
ctx.errors.push(RuleError::new(
vec![field.pos],
"Oneof fields requires have exactly one argument".to_string(),
));
return;
}
let value = field.node.arguments[0]
.1
.node
.clone()
.into_const_with(|var_name| {
ctx.variables
.and_then(|variables| variables.get(&var_name))
.map(Clone::clone)
.ok_or(())
})
.ok();
if let Some(Value::Null) = value {
ctx.errors.push(RuleError::new(
vec![field.pos],
"Oneof Fields require that exactly one argument must be supplied and that argument must not be null".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
}
"#,
);
}
#[test]
fn oneof_arguments_not_be_null() {
expect_fails_rule!(
factory,
r#"
query Foo {
oneofField(a: null)
}
"#,
);
}
}

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,28 @@ pub fn is_valid_input_value(
registry::MetaType::InputObject {
input_fields,
name: object_name,
oneof,
..
} => match value {
ConstValue::Object(values) => {
if *oneof {
if values.len() != 1 {
return Some(valid_error(
&path_node,
"Oneof input objects requires have exactly one field"
.to_string(),
));
}
if let ConstValue::Null = values[0] {
return Some(valid_error(
&path_node,
"Oneof Input Objects require that exactly one field must be supplied and that field must not be null"
.to_string(),
));
}
}
let mut input_names =
values.keys().map(AsRef::as_ref).collect::<HashSet<_>>();

View File

@ -492,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

@ -346,6 +346,158 @@ pub async fn test_guard_use_params() {
);
}
#[tokio::test]
pub async fn test_guard_on_simple_object() {
#[derive(SimpleObject)]
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
struct Query {
value: i32,
}
let schema = Schema::new(Query { value: 100 }, EmptyMutation, EmptySubscription);
let query = "{ value }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Admin))
.await
.data,
value!({"value": 100})
);
let query = "{ value }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Guest))
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
}]
);
}
#[tokio::test]
pub async fn test_guard_on_simple_object_field() {
#[derive(SimpleObject)]
#[graphql]
struct Query {
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
value: i32,
}
let schema = Schema::new(Query { value: 100 }, EmptyMutation, EmptySubscription);
let query = "{ value }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Admin))
.await
.data,
value!({"value": 100})
);
let query = "{ value }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Guest))
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
}]
);
}
#[tokio::test]
pub async fn test_guard_on_object() {
struct Query;
#[Object(guard = "RoleGuard::new(Role::Admin)")]
impl Query {
async fn value(&self) -> i32 {
100
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = "{ value }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Admin))
.await
.data,
value!({"value": 100})
);
let query = "{ value }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Guest))
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
}]
);
}
#[tokio::test]
pub async fn test_guard_on_object_field() {
struct Query;
#[Object]
impl Query {
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
async fn value(&self) -> i32 {
100
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let query = "{ value }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Admin))
.await
.data,
value!({"value": 100})
);
let query = "{ value }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Guest))
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
}]
);
}
#[tokio::test]
pub async fn test_guard_on_complex_object() {
#[derive(SimpleObject)]
@ -354,6 +506,49 @@ pub async fn test_guard_on_complex_object() {
value1: i32,
}
#[ComplexObject(guard = "RoleGuard::new(Role::Admin)")]
impl Query {
async fn value2(&self) -> i32 {
100
}
}
let schema = Schema::new(Query { value1: 10 }, EmptyMutation, EmptySubscription);
let query = "{ value2 }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Admin))
.await
.data,
value!({"value2": 100})
);
let query = "{ value2 }";
assert_eq!(
schema
.execute(Request::new(query).data(Role::Guest))
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value2".to_owned())],
extensions: None,
}]
);
}
#[tokio::test]
pub async fn test_guard_on_complex_object_field() {
#[derive(SimpleObject)]
#[graphql(complex)]
struct Query {
value1: i32,
}
#[ComplexObject]
impl Query {
#[graphql(guard = "RoleGuard::new(Role::Admin)")]

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

@ -117,3 +117,80 @@ async fn test_flatten_with_context() {
})
);
}
#[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"
}
}]
}]
}
})
);
}

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.31"
version = "3.0.35"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "GraphQL value for async-graphql"