diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3f1532f..1277ce0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,8 @@ jobs: fail-fast: false matrix: include: - - { rust: stable, os: ubuntu-latest } - - { rust: 1.54.0, os: ubuntu-latest } +# - { rust: stable, os: ubuntu-latest } + - { rust: 1.56.1, os: ubuntu-latest } steps: - name: Checkout uses: actions/checkout@v2 diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index f1a3b0e0..889879f1 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -10,6 +10,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: 1.56.1 + override: true - name: Run cargo-tarpaulin uses: actions-rs/tarpaulin@v0.1 with: diff --git a/.rustfmt.toml b/.rustfmt.toml index 50ff879a..9d25d3fc 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,2 +1,2 @@ -edition = "2018" +edition = "2021" newline_style = "unix" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da8f201..1d6e5614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ 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). +## Unreleased + +- Add `chrono::Duration` custom scalar. [#689](https://github.com/async-graphql/async-graphql/pull/689) +- Implement `From>>` for `MaybeUndefined`. +- Add `MaybeUndefined::as_opt_ref`, `MaybeUndefined::as_opt_deref`, `MaybeUndefined::map`, `MaybeUndefined::map_value`, `MaybeUndefined::contains`, `MaybeUndefined::contains_value`, and `MaybeUndefined::transpose` methods. +- Made `MaybeUndefined::is_undefined`, `MaybeUndefined::is_null`, `MaybeUndefined::is_value`, `MaybeUndefined::value` and `MaybeUndefined::as_opt_ref` const. +- Add `ResolverError` type. [#671](https://github.com/async-graphql/async-graphql/issues/671) +- [async-graphql-axum] Bump axum from `0.2.5` to `0.3`. +- [async-graphql-poem] Export the HTTP headers in the `Context`. + +## [2.11.0] 2021-11-03 + +- Use Rust `2021` edition. +- Subscription typename - [GraphQL - October 2021] [#681](https://github.com/async-graphql/async-graphql/issues/681) +- Allow directive on variable definition - [GraphQL - October 2021] [#678](https://github.com/async-graphql/async-graphql/issues/678) +- Specified By - [GraphQL - October 2021] [#677](https://github.com/async-graphql/async-graphql/issues/677) +- Add `specified_by_url` for `Tz`, `DateTime`, `Url`, `Uuid` and `Upload` scalars. +- Number value literal lookahead restrictions - [GraphQL - October 2021] [#685](https://github.com/async-graphql/async-graphql/issues/685) + +## [2.10.8] 2021-10-26 + +- [async-graphql-poem] Bump poem to `1.0.13`. + +## [2.10.6] 2021-10-26 + +- Add derived for object & simple object & complex object. [#667](https://github.com/async-graphql/async-graphql/pull/667) [#670](https://github.com/async-graphql/async-graphql/pull/670) +- Respect query object field order. [#612](https://github.com/async-graphql/async-graphql/issues/612) + +## [2.10.5] 2021-10-22 + +- Bump poem from `0.6.6` to `1.0.7`. + ## [2.10.4] 2021-10-22 - Implement `Default` for ID #659. diff --git a/Cargo.toml b/Cargo.toml index 5766996c..6c9f93ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql" -version = "2.10.4" +version = "2.11.0" authors = ["sunli ", "Koxiaet"] -edition = "2018" +edition = "2021" description = "A GraphQL server library implemented in Rust" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql/" @@ -22,11 +22,12 @@ dataloader = ["futures-timer", "futures-channel", "lru"] tracing = ["tracinglib", "tracing-futures"] decimal = ["rust_decimal"] cbor = ["serde_cbor"] +chrono-duration = ["chrono", "iso8601-duration"] [dependencies] -async-graphql-derive = { path = "derive", version = "=2.10.4" } -async-graphql-value = { path = "value", version = "=2.10.4" } -async-graphql-parser = { path = "parser", version = "=2.10.4" } +async-graphql-derive = { path = "derive", version = "=2.11.0" } +async-graphql-value = { path = "value", version = "=2.11.0" } +async-graphql-parser = { path = "parser", version = "=2.11.0" } async-stream = "0.3.0" async-trait = "0.1.48" @@ -50,6 +51,7 @@ mime = "0.3.15" bson = { version = "2.0.0", optional = true, features = ["chrono-0_4"] } chrono = { version = "0.4.19", optional = true } chrono-tz = { version = "0.5.3", optional = true } +iso8601-duration = { version = "0.1.0", optional = true } log = { version = "0.4.14", optional = true } secrecy = { version = "0.7.0", optional = true } tracinglib = { version = "0.1.25", optional = true, package = "tracing" } diff --git a/README.md b/README.md index 6954e70f..09a94ec8 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ * [Docs](https://docs.rs/async-graphql) * [GitHub repository](https://github.com/async-graphql/async-graphql) * [Cargo package](https://crates.io/crates/async-graphql) -* Minimum supported Rust version: 1.54.0 or later +* Minimum supported Rust version: 1.56.1 or later ## Safety diff --git a/derive/Cargo.toml b/derive/Cargo.toml index ee246810..905c8497 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql-derive" -version = "2.10.4" +version = "2.11.0" authors = ["sunli ", "Koxiaet"] -edition = "2018" +edition = "2021" description = "Macros for async-graphql" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql/" @@ -15,7 +15,7 @@ categories = ["network-programming", "asynchronous"] proc-macro = true [dependencies] -async-graphql-parser = { path = "../parser", version = "=2.10.4" } +async-graphql-parser = { path = "../parser", version = "=2.11.0" } proc-macro2 = "1.0.24" syn = { version = "1.0.64", features = ["full", "extra-traits", "visit-mut", "visit"] } quote = "1.0.9" diff --git a/derive/src/args.rs b/derive/src/args.rs index 0cdcf5b2..4985953a 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -6,7 +6,7 @@ use syn::{ Attribute, Generics, Ident, Lit, LitBool, LitStr, Meta, NestedMeta, Path, Type, Visibility, }; -#[derive(FromMeta)] +#[derive(FromMeta, Clone)] #[darling(default)] pub struct CacheControl { public: bool, @@ -46,7 +46,7 @@ impl FromMeta for DefaultValue { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Visible { None, HiddenAlways, @@ -86,7 +86,7 @@ pub struct ConcreteType { pub params: PathList, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Deprecation { NoDeprecated, Deprecated { reason: Option }, @@ -115,7 +115,7 @@ impl FromMeta for Deprecation { } } -#[derive(FromField)] +#[derive(FromField, Clone)] #[darling(attributes(graphql), forward_attrs(doc))] pub struct SimpleObjectField { pub ident: Option, @@ -143,6 +143,8 @@ pub struct SimpleObjectField { pub guard: Option, #[darling(default)] pub visible: Option, + #[darling(default, multiple)] + pub derived: Vec, } #[derive(FromDeriveInput)] @@ -243,6 +245,19 @@ pub struct ObjectField { pub guard: Option, pub visible: Option, pub complexity: Option, + #[darling(default, multiple)] + pub derived: Vec, +} + +#[derive(FromMeta, Default, Clone)] +#[darling(default)] +/// Derivied fields arguments: are used to generate derivied fields. +pub struct DerivedField { + pub name: Option, + pub into: Option, + pub with: Option, + #[darling(default)] + pub owned: Option, } #[derive(FromDeriveInput)] @@ -431,6 +446,7 @@ pub struct Scalar { pub name: Option, pub use_type_description: bool, pub visible: Option, + pub specified_by_url: Option, } #[derive(FromMeta, Default)] @@ -637,6 +653,8 @@ pub struct NewType { pub name: NewTypeName, #[darling(default)] pub visible: Option, + #[darling(default)] + pub specified_by_url: Option, } #[derive(FromMeta, Default)] diff --git a/derive/src/complex_object.rs b/derive/src/complex_object.rs index d88d8922..2926518a 100644 --- a/derive/src/complex_object.rs +++ b/derive/src/complex_object.rs @@ -1,7 +1,13 @@ use proc_macro::TokenStream; +use proc_macro2::Ident; use quote::quote; +use std::iter::FromIterator; +use std::str::FromStr; use syn::ext::IdentExt; -use syn::{Block, Error, ImplItem, ItemImpl, ReturnType}; +use syn::{ + punctuated::Punctuated, Block, Error, FnArg, ImplItem, ItemImpl, Pat, ReturnType, Token, Type, + TypeReference, +}; use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget}; use crate::output_type::OutputType; @@ -23,6 +29,91 @@ pub fn generate( let mut resolvers = Vec::new(); let mut schema_fields = Vec::new(); + // Computation of the derivated fields + let mut derived_impls = vec![]; + for item in &mut item_impl.items { + if let ImplItem::Method(method) = item { + let method_args: args::ObjectField = + parse_graphql_attrs(&method.attrs)?.unwrap_or_default(); + + for derived in method_args.derived { + if derived.name.is_some() && derived.into.is_some() { + let base_function_name = &method.sig.ident; + let name = derived.name.unwrap(); + let with = derived.with; + let into = Type::Verbatim( + proc_macro2::TokenStream::from_str(&derived.into.unwrap()).unwrap(), + ); + + let mut new_impl = method.clone(); + new_impl.sig.ident = name; + new_impl.sig.output = + syn::parse2::(quote! { -> #crate_name::Result<#into> }) + .expect("invalid result type"); + + let should_create_context = new_impl + .sig + .inputs + .iter() + .nth(1) + .map(|x| { + if let FnArg::Typed(pat) = x { + if let Type::Reference(TypeReference { elem, .. }) = &*pat.ty { + if let Type::Path(path) = elem.as_ref() { + return path.path.segments.last().unwrap().ident + != "Context"; + } + } + }; + true + }) + .unwrap_or(true); + + if should_create_context { + let arg_ctx = syn::parse2::(quote! { ctx: &Context<'_> }) + .expect("invalid arg type"); + new_impl.sig.inputs.insert(1, arg_ctx); + } + + let other_atts: Punctuated = Punctuated::from_iter( + new_impl + .sig + .inputs + .iter() + .filter_map(|x| match x { + FnArg::Typed(pat) => match &*pat.pat { + Pat::Ident(ident) => Some(Ok(ident.ident.clone())), + _ => Some(Err(Error::new_spanned( + &pat, + "Must be a simple argument", + ))), + }, + FnArg::Receiver(_) => None, + }) + .collect::, Error>>()? + .into_iter(), + ); + + let new_block = match with { + Some(with) => quote!({ + ::std::result::Result::Ok(#with(#self_ty::#base_function_name(&self, #other_atts).await?)) + }), + None => quote!({ + { + ::std::result::Result::Ok(#self_ty::#base_function_name(&self, #other_atts).await?.into()) + } + }), + }; + + new_impl.block = syn::parse2::(new_block).expect("invalid block"); + + derived_impls.push(ImplItem::Method(new_impl)); + } + } + } + } + item_impl.items.append(&mut derived_impls); + for item in &mut item_impl.items { if let ImplItem::Method(method) = item { let method_args: args::ObjectField = diff --git a/derive/src/enum.rs b/derive/src/enum.rs index 781d8815..f5510988 100644 --- a/derive/src/enum.rs +++ b/derive/src/enum.rs @@ -71,13 +71,9 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult { } let remote_conversion = if let Some(remote) = &enum_args.remote { - let remote_ty = if let Ok(ty) = syn::parse_str::(remote) { - ty - } else { - return Err( - Error::new_spanned(remote, format!("Invalid remote type: '{}'", remote)).into(), - ); - }; + let remote_ty = syn::parse_str::(remote).map_err(|_| { + Error::new_spanned(remote, format!("Invalid remote type: '{}'", remote)) + })?; let local_to_remote_items = enum_items.iter().map(|item| { quote! { diff --git a/derive/src/input_object.rs b/derive/src/input_object.rs index cd7680d7..228938e3 100644 --- a/derive/src/input_object.rs +++ b/derive/src/input_object.rs @@ -231,7 +231,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult } fn to_value(&self) -> #crate_name::Value { - let mut map = ::std::collections::BTreeMap::new(); + let mut map = #crate_name::indexmap::IndexMap::new(); #(#put_fields)* #crate_name::Value::Object(map) } @@ -272,7 +272,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult } fn __internal_to_value(&self) -> #crate_name::Value where Self: #crate_name::InputType { - let mut map = ::std::collections::BTreeMap::new(); + let mut map = #crate_name::indexmap::IndexMap::new(); #(#put_fields)* #crate_name::Value::Object(map) } diff --git a/derive/src/merged_object.rs b/derive/src/merged_object.rs index f6ccc3fb..3ea5b313 100644 --- a/derive/src/merged_object.rs +++ b/derive/src/merged_object.rs @@ -90,6 +90,7 @@ pub fn generate(object_args: &args::MergedObject) -> GeneratorResult GeneratorResult GeneratorResult { None => quote! { <#inner_ty as #crate_name::Type>::type_name() }, }; let create_type_info = if let Some(name) = &gql_typename { + let specified_by_url = match &newtype_args.specified_by_url { + Some(specified_by_url) => quote! { ::std::option::Option::Some(#specified_by_url) }, + None => quote! { ::std::option::Option::None }, + }; + quote! { registry.create_type::<#ident, _>(|_| #crate_name::registry::MetaType::Scalar { name: ::std::borrow::ToOwned::to_owned(#name), description: #desc, is_valid: |value| <#ident as #crate_name::ScalarType>::is_valid(value), visible: #visible, + specified_by_url: #specified_by_url, }) } } else { diff --git a/derive/src/object.rs b/derive/src/object.rs index 2f44632a..94fa4e89 100644 --- a/derive/src/object.rs +++ b/derive/src/object.rs @@ -1,7 +1,13 @@ use proc_macro::TokenStream; +use proc_macro2::Ident; use quote::quote; +use std::iter::FromIterator; +use std::str::FromStr; use syn::ext::IdentExt; -use syn::{Block, Error, ImplItem, ItemImpl, ReturnType}; +use syn::{ + punctuated::Punctuated, Block, Error, FnArg, ImplItem, ItemImpl, Pat, ReturnType, Token, Type, + TypeReference, +}; use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget}; use crate::output_type::OutputType; @@ -38,6 +44,91 @@ pub fn generate( let mut add_keys = Vec::new(); let mut create_entity_types = Vec::new(); + // Computation of the derivated fields + let mut derived_impls = vec![]; + for item in &mut item_impl.items { + if let ImplItem::Method(method) = item { + let method_args: args::ObjectField = + parse_graphql_attrs(&method.attrs)?.unwrap_or_default(); + + for derived in method_args.derived { + if derived.name.is_some() && derived.into.is_some() { + let base_function_name = &method.sig.ident; + let name = derived.name.unwrap(); + let with = derived.with; + let into = Type::Verbatim( + proc_macro2::TokenStream::from_str(&derived.into.unwrap()).unwrap(), + ); + + let mut new_impl = method.clone(); + new_impl.sig.ident = name; + new_impl.sig.output = + syn::parse2::(quote! { -> #crate_name::Result<#into> }) + .expect("invalid result type"); + + let should_create_context = new_impl + .sig + .inputs + .iter() + .nth(1) + .map(|x| { + if let FnArg::Typed(pat) = x { + if let Type::Reference(TypeReference { elem, .. }) = &*pat.ty { + if let Type::Path(path) = elem.as_ref() { + return path.path.segments.last().unwrap().ident + != "Context"; + } + } + }; + true + }) + .unwrap_or(true); + + if should_create_context { + let arg_ctx = syn::parse2::(quote! { ctx: &Context<'_> }) + .expect("invalid arg type"); + new_impl.sig.inputs.insert(1, arg_ctx); + } + + let other_atts: Punctuated = Punctuated::from_iter( + new_impl + .sig + .inputs + .iter() + .filter_map(|x| match x { + FnArg::Typed(pat) => match &*pat.pat { + Pat::Ident(ident) => Some(Ok(ident.ident.clone())), + _ => Some(Err(Error::new_spanned( + &pat, + "Must be a simple argument", + ))), + }, + FnArg::Receiver(_) => None, + }) + .collect::, Error>>()? + .into_iter(), + ); + + let new_block = match with { + Some(with) => quote!({ + ::std::result::Result::Ok(#with(#self_ty::#base_function_name(&self, #other_atts).await?)) + }), + None => quote!({ + { + ::std::result::Result::Ok(#self_ty::#base_function_name(&self, #other_atts).await?.into()) + } + }), + }; + + new_impl.block = syn::parse2::(new_block).expect("invalid block"); + + derived_impls.push(ImplItem::Method(new_impl)); + } + } + } + } + item_impl.items.append(&mut derived_impls); + for item in &mut item_impl.items { if let ImplItem::Method(method) = item { let method_args: args::ObjectField = @@ -472,6 +563,7 @@ pub fn generate( extends: #extends, keys: ::std::option::Option::None, visible: #visible, + is_subscription: false, }); #(#create_entity_types)* #(#add_keys)* @@ -539,6 +631,7 @@ pub fn generate( extends: #extends, keys: ::std::option::Option::None, visible: #visible, + is_subscription: false, }); #(#create_entity_types)* #(#add_keys)* diff --git a/derive/src/scalar.rs b/derive/src/scalar.rs index 561ee384..3aa55b0a 100644 --- a/derive/src/scalar.rs +++ b/derive/src/scalar.rs @@ -30,6 +30,11 @@ pub fn generate( let generic = &item_impl.generics; let where_clause = &item_impl.generics.where_clause; let visible = visible_fn(&scalar_args.visible); + let specified_by_url = match &scalar_args.specified_by_url { + Some(specified_by_url) => quote! { ::std::option::Option::Some(#specified_by_url) }, + None => quote! { ::std::option::Option::None }, + }; + let expanded = quote! { #item_impl @@ -45,6 +50,7 @@ pub fn generate( description: #desc, is_valid: |value| <#self_ty as #crate_name::ScalarType>::is_valid(value), visible: #visible, + specified_by_url: #specified_by_url, }) } } diff --git a/derive/src/simple_object.rs b/derive/src/simple_object.rs index d0665abc..a603e8cb 100644 --- a/derive/src/simple_object.rs +++ b/derive/src/simple_object.rs @@ -1,14 +1,28 @@ use darling::ast::Data; use proc_macro::TokenStream; use quote::quote; +use std::str::FromStr; use syn::ext::IdentExt; -use syn::Error; +use syn::{Error, Ident, Path, Type}; -use crate::args::{self, RenameRuleExt, RenameTarget}; +use crate::args::{self, RenameRuleExt, RenameTarget, SimpleObjectField}; use crate::utils::{ gen_deprecation, generate_guards, get_crate_name, get_rustdoc, visible_fn, GeneratorResult, }; +#[derive(Debug)] +struct DerivedFieldMetadata { + ident: Ident, + into: Type, + owned: Option, + with: Option, +} + +struct SimpleObjectFieldGenerator<'a> { + field: &'a SimpleObjectField, + derived: Option, +} + pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult { let crate_name = get_crate_name(object_args.internal); let ident = &object_args.ident; @@ -37,15 +51,62 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult = vec![]; + + // Before processing the fields, we generate the derivated fields for field in &s.fields { + processed_fields.push(SimpleObjectFieldGenerator { + field, + derived: None, + }); + + for derived in &field.derived { + if derived.name.is_some() && derived.into.is_some() { + let name = derived.name.clone().unwrap(); + let into = match syn::parse2::( + proc_macro2::TokenStream::from_str(&derived.into.clone().unwrap()).unwrap(), + ) { + Ok(e) => e, + _ => { + return Err(Error::new_spanned( + &name, + "derived into must be a valid type.", + ) + .into()); + } + }; + + let derived = DerivedFieldMetadata { + ident: name, + into, + owned: derived.owned, + with: derived.with.clone(), + }; + + processed_fields.push(SimpleObjectFieldGenerator { + field, + derived: Some(derived), + }) + } + } + } + + for SimpleObjectFieldGenerator { field, derived } in &processed_fields { if field.skip { continue; } - let ident = match &field.ident { + + let base_ident = match &field.ident { Some(ident) => ident, None => return Err(Error::new_spanned(&ident, "All fields must be named.").into()), }; + let ident = if let Some(derived) = derived { + &derived.ident + } else { + base_ident + }; + let field_name = field.name.clone().unwrap_or_else(|| { object_args .rename_fields @@ -65,7 +126,18 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult quote! { ::std::option::Option::None }, }; let vis = &field.vis; - let ty = &field.ty; + + let ty = if let Some(derived) = derived { + &derived.into + } else { + &field.ty + }; + + let owned = if let Some(derived) = derived { + derived.owned.unwrap_or(field.owned) + } else { + field.owned + }; let cache_control = { let public = field.cache_control.is_public(); @@ -104,23 +176,41 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult quote! { + &self.#base_ident + }, + false => quote! { + ::std::clone::Clone::clone(&self.#base_ident) + }, + }; + + block = match (derived, with_function) { + (Some(_), Some(with)) => quote! { + #with(#block) + }, + (Some(_), None) => quote! { + ::std::convert::Into::into(#block) + }, + (_, _) => block, + }; + + let ty = match !owned { + true => quote! { &#ty }, + false => quote! { #ty }, + }; + + getters.push( quote! { #[inline] #[allow(missing_docs)] - #vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::Result<&#ty> { - ::std::result::Result::Ok(&self.#ident) + #vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::Result<#ty> { + ::std::result::Result::Ok(#block) } } - } else { - quote! { - #[inline] - #[allow(missing_docs)] - #vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::Result<#ty> { - ::std::result::Result::Ok(::std::clone::Clone::clone(&self.#ident)) - } - } - }); + ); resolvers.push(quote! { if ctx.item.node.name.node == #field_name { @@ -203,6 +293,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult GeneratorResult InputValueResult { ... } + + fn to_value(&self) -> Value { + Value::String(self.0.to_rfc3339()) + } +} + +#[Scalar] +impl ScalarType for DateRFC2822 { + fn parse(value: Value) -> InputValueResult { ... } + + fn to_value(&self) -> Value { + Value::String(self.0.to_rfc2822()) + } +} + +impl From for DateRFC3339 { + fn from(value: DateRFC2822) -> Self { + DateRFC3339(value.0) + } +} + +struct Query; + +#[Object] +impl Query { + #[graphql(derived(name = "date_rfc3339", into = "DateRFC3339"))] + async fn duration_rfc2822(&self, arg: String) -> DateRFC2822 { + todo!() + } +} +``` + +It will render a GraphQL like: + +```graphql +type Query { + duration_rfc2822(arg: String): DateRFC2822! + duration_rfc3339(arg: String): DateRFC3339! +} +``` + +## Wrapper types + +A derived field won't be able to manage everythings easily: without the specialization from the Rust language, you won't be able to implement specialized trait like: +``` +impl From> for Vec { + ... +} +``` + +So you wouldn't be able to generate derived fields for existing wrapper type structures like `Vec` or `Option`. But when you implement a `From for T` you should be able to derived a `From> for Vec` and a `From> for Option`. +We included a `with` parameter to help you define a function to call instead of using the `Into` trait implementation between wrapper structures. + + +### Example + +```rust +#[derive(Serialize, Deserialize, Clone)] +struct ValueDerived(String); + +#[derive(Serialize, Deserialize, Clone)] +struct ValueDerived2(String); + +scalar!(ValueDerived); +scalar!(ValueDerived2); + +impl From for ValueDerived2 { + fn from(value: ValueDerived) -> Self { + ValueDerived2(value.0) + } +} + +fn option_to_option>(value: Option) -> Option { + value.map(|x| x.into()) +} + +#[derive(SimpleObject)] +struct TestObj { + #[graphql(derived(owned, name = "value2", into = "Option", with = "option_to_option"))] + pub value1: Option, +} +``` diff --git a/docs/en/src/extensions.md b/docs/en/src/extensions.md new file mode 100644 index 00000000..6bb21dfd --- /dev/null +++ b/docs/en/src/extensions.md @@ -0,0 +1,3 @@ +# Extensions + +`async-graphql` has the capability to be extended with extensions without having to modify the original source code. A lot of features can be added this way, and a lot of extensions already exists. diff --git a/docs/en/src/extensions_available.md b/docs/en/src/extensions_available.md new file mode 100644 index 00000000..c29e69a4 --- /dev/null +++ b/docs/en/src/extensions_available.md @@ -0,0 +1,54 @@ +# Extensions available + +There are a lot of available extensions in the `async-graphql` to empower your GraphQL Server, some of these documentations are documented here. + +## Analyzer +*Available in the repository* + +The `analyzer` extension will output a field containing `complexity` and `depth` in the response extension field of each query. + + +## Apollo Persisted Queries +*Available in the repository* + +To improve network performance for large queries, you can enable this Persisted Queries extension. With this extension enabled, each unique query is associated to a unique identifier, so clients can send this identifier instead of the corresponding query string to reduce requests sizes. + +This extension doesn't force you to use some cache strategy, you can choose the caching strategy you want, you'll just have to implement the `CacheStorage` trait: +```rust +#[async_trait::async_trait] +pub trait CacheStorage: Send + Sync + Clone + 'static { + /// Load the query by `key`. + async fn get(&self, key: String) -> Option; + /// Save the query by `key`. + async fn set(&self, key: String, query: String); +} +``` + +### References + +[Apollo doc - Persisted Queries](https://www.apollographql.com/docs/react/api/link/persisted-queries/) + +## Apollo Tracing +*Available in the repository* + +Apollo Tracing is an extension which includes analytics data for your queries. This extension works to follow the old and now deprecated [Apollo Tracing Spec](https://github.com/apollographql/apollo-tracing). If you want to check the newer Apollo Reporting Protocol, it's implemented by [async-graphql Apollo studio extension](https://github.com/async-graphql/async_graphql_apollo_studio_extension) for Apollo Studio. + +## Apollo Studio +*Available at [async-graphql/async_graphql_apollo_studio_extension](https://github.com/async-graphql/async_graphql_apollo_studio_extension)* + +Apollo Studio is a cloud platform that helps you build, validate, and secure your organization's graph (description from the official documentation). It's a service allowing you to monitor & work with your team around your GraphQL Schema. `async-graphql` provides an extension implementing the official [Apollo Specification](https://www.apollographql.com/docs/studio/setup-analytics/#third-party-support) available at [async-graphql-extension-apollo-tracing](https://github.com/async-graphql/async_graphql_apollo_studio_extension) and [Crates.io](https://crates.io/crates/async-graphql-extension-apollo-tracing). + +## Logger +*Available in the repository* + +Logger is a simple extension allowing you to add some logging feature to `async-graphql`. It's also a good example to learn how to create your own extension. + +## OpenTelemetry +*Available in the repository* + +OpenTelemetry is an extension providing an integration with the [opentelemetry crate](https://crates.io/crates/opentelemetry) to allow your application to capture distributed traces and metrics from `async-grraphql`. + +## Tracing +*Available in the repository* + +Tracing is a simple extension allowing you to add some tracing feature to `async-graphql`. A little like the `Logger` extension. diff --git a/docs/en/src/extensions_inner_working.md b/docs/en/src/extensions_inner_working.md new file mode 100644 index 00000000..14c63788 --- /dev/null +++ b/docs/en/src/extensions_inner_working.md @@ -0,0 +1,162 @@ +# How extensions are defined + +An `async-graphql` extension is defined by implementing the trait `Extension` associated. The `Extension` trait allow you to insert custom code to some several steps used to respond to GraphQL's queries through `async-graphql`. With `Extensions`, your application can hook into the GraphQL's requests lifecycle to add behaviors about incoming requests or outgoing response. + +`Extensions` are a lot like middleware from other frameworks, be careful when using those: when you use an extension **it'll be run for every GraphQL requests**. + +Across every step, you'll have the `ExtensionContext` supplied with data about your current request execution. Feel free to check how it's constructed in the code, documentation about it will soon come. + +## A word about middleware + +For those who don't know, let's dig deeper into what is a middleware: + +```rust +async fn middleware(&self, ctx: &ExtensionContext<'_>, next: NextMiddleware<'_>) -> MiddlewareResult { + // Logic to your middleware. + + /* + * Final step to your middleware, we call the next function which will trigger + * the execution of the next middleware. It's like a `callback` in JavaScript. + */ + next.run(ctx).await +} +``` + +As you have seen, a `Middleware` is only a function calling the next function at the end, but we could also do a middleware with the `next` function at the start. This is where it's becoming tricky: depending on where you put your logics and where is the `next` call, your logic won't have the same execution order. + + +Depending on your logic code, you'll want to process it before or after the `next` call. If you need more information about middlewares, there are a lot of things in the web. + +## Processing of a query + +There are several steps to go to process a query to completion, you'll be able to create extension based on these hooks. + +### request + +First, when we receive a request, if it's not a subscription, the first function to be called will be `request`, it's the first step, it's the function called at the incoming request, and it's also the function which will output the response to the user. + +Default implementation for `request`: + +```rust +async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response { + next.run(ctx).await +} +``` + +Depending on where you put your logic code, it'll be executed at the beginning or at the ending of the query being processed. + + +```rust +async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response { + // The code here will be run before the prepare_request is executed. + let result = next.run(ctx).await; + // The code after the completion of this futue will be after the processing, just before sending the result to the user. + result +} +``` + +### prepare_request + +Just after the `request`, we will have the `prepare_request` lifecycle, which will be hooked. + +```rust +async fn prepare_request( + &self, + ctx: &ExtensionContext<'_>, + request: Request, + next: NextPrepareRequest<'_>, +) -> ServerResult { + // The code here will be un before the prepare_request is executed, just after the request lifecycle hook. + let result = next.run(ctx, request).await; + // The code here will be run just after the prepare_request + result +} +``` + +### parse_query + +The `parse_query` will create a GraphQL `ExecutableDocument` on your query, it'll check if the query is valid for the GraphQL Spec. Usually the implemented spec in `async-graphql` tends to be the last stable one (October2021). + +```rust +/// Called at parse query. +async fn parse_query( + &self, + ctx: &ExtensionContext<'_>, + // The raw query + query: &str, + // The variables + variables: &Variables, + next: NextParseQuery<'_>, +) -> ServerResult { + next.run(ctx, query, variables).await +} +``` + +### validation + +The `validation` step will check (depending on your `validation_mode`) rules the query should abide to and give the client data about why the query is not valid. + +```rust +/// Called at validation query. +async fn validation( + &self, + ctx: &ExtensionContext<'_>, + next: NextValidation<'_>, +) -> Result> { + next.run(ctx).await +} +``` + +### execute + +The `execution` step is a huge one, it'll start the execution of the query by calling each resolver concurrently for a `Query` and serially for a `Mutation`. + +```rust +/// Called at execute query. +async fn execute( + &self, + ctx: &ExtensionContext<'_>, + operation_name: Option<&str>, + next: NextExecute<'_>, +) -> Response { + // Befoe starting resolving the whole query + let result = next.run(ctx, operation_name).await; + // After resolving the whole query + result +} +```` + +### resolve + +The `resolve` step is launched for each field. + +```rust +/// Called at resolve field. +async fn resolve( + &self, + ctx: &ExtensionContext<'_>, + info: ResolveInfo<'_>, + next: NextResolve<'_>, +) -> ServerResult> { + // Logic before resolving the field + let result = next.run(ctx, info).await; + // Logic after resolving the field + result +} +``` + +### subscribe + +The `subscribe` lifecycle has the same behavior as the `request` but for a `Subscritpion`. + +```rust +/// Called at subscribe request. +fn subscribe<'s>( + &self, + ctx: &ExtensionContext<'_>, + stream: BoxStream<'s, Response>, + next: NextSubscribe<'_>, +) -> BoxStream<'s, Response> { + next.run(ctx, stream) +} +``` diff --git a/examples b/examples index df8cfdff..8641a72c 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit df8cfdff35d25d1cef3c9fda4d98e639a55fffc2 +Subproject commit 8641a72c8ccb78e924e2550cd1fbfa2e62319174 diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index 85f0a13e..6f4acc2e 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql-actix-web" -version = "2.10.4" +version = "2.11.0" authors = ["sunli ", "Koxiaet"] -edition = "2018" +edition = "2021" description = "async-graphql for actix-web" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql-actix-web/" @@ -12,8 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "=2.10.4" } - +async-graphql = { path = "../..", version = "=2.11.0" } actix = "0.12.0" actix-http = "3.0.0-beta.11" actix-web = { version = "4.0.0-beta.10", default-features = false } @@ -22,6 +21,7 @@ async-channel = "1.6.1" futures-util = { version = "0.3.17", default-features = false } serde_json = "1.0.64" serde_urlencoded = "0.7.0" +futures-channel = "0.3.13" [dev-dependencies] actix-rt = "2.2.0" diff --git a/integrations/actix-web/src/lib.rs b/integrations/actix-web/src/lib.rs index 06cf949f..a0c4e227 100644 --- a/integrations/actix-web/src/lib.rs +++ b/integrations/actix-web/src/lib.rs @@ -174,4 +174,4 @@ impl Responder for Response { } res.body(serde_json::to_string(&self.0).unwrap()) } -} +} \ No newline at end of file diff --git a/integrations/actix-web/src/subscription.rs b/integrations/actix-web/src/subscription.rs index 0fbd1ab2..4a4f6aea 100644 --- a/integrations/actix-web/src/subscription.rs +++ b/integrations/actix-web/src/subscription.rs @@ -212,4 +212,4 @@ where .spawn(ctx) } } -} +} \ No newline at end of file diff --git a/integrations/actix-web/tests/graphql.rs b/integrations/actix-web/tests/graphql.rs index 4868cdbf..bd328142 100644 --- a/integrations/actix-web/tests/graphql.rs +++ b/integrations/actix-web/tests/graphql.rs @@ -241,4 +241,4 @@ async fn test_count() { .into() ) ); -} +} \ No newline at end of file diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index d747e1f2..9f1210b7 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql-axum" -version = "2.10.4" +version = "2.11.0" authors = ["sunli "] -edition = "2018" +edition = "2021" description = "async-graphql for axum" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql-axum/" @@ -12,10 +12,10 @@ keywords = ["futures", "async", "graphql", "axum"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "=2.10.4" } +async-graphql = { path = "../..", version = "=2.11.0" } async-trait = "0.1.51" -axum = { version = "0.2.5", features = ["ws", "headers"] } +axum = { version = "0.3", features = ["ws", "headers"] } bytes = "1.0.1" headers = "0.3.4" http = "0.2.4" diff --git a/integrations/axum/src/response.rs b/integrations/axum/src/response.rs index d124c358..ed3d7b51 100644 --- a/integrations/axum/src/response.rs +++ b/integrations/axum/src/response.rs @@ -1,5 +1,3 @@ -use std::convert::TryFrom; - use axum::body::Body; use axum::response::IntoResponse; use headers::HeaderName; diff --git a/integrations/poem/Cargo.toml b/integrations/poem/Cargo.toml index 4cbd349e..9c5a6fbd 100644 --- a/integrations/poem/Cargo.toml +++ b/integrations/poem/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql-poem" -version = "2.10.4" +version = "2.11.0" authors = ["sunli "] -edition = "2018" +edition = "2021" description = "async-graphql for poem" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql-poem/" @@ -11,11 +11,10 @@ repository = "https://github.com/async-graphql/async-graphql" keywords = ["futures", "async", "graphql", "poem"] categories = ["network-programming", "asynchronous"] - [dependencies] -async-graphql = { path = "../..", version = "=2.10.4" } +async-graphql = { path = "../..", version = "=2.11.0" } -poem = { version = "0.6.6", features = ["websocket"] } +poem = { version = "1.0.19", features = ["websocket"] } futures-util = { version = "0.3.13", default-features = false } serde_json = "1.0.66" tokio-util = { version = "0.6.7", features = ["compat"] } diff --git a/integrations/poem/src/extractor.rs b/integrations/poem/src/extractor.rs index ed9e36c3..4e09a66e 100644 --- a/integrations/poem/src/extractor.rs +++ b/integrations/poem/src/extractor.rs @@ -1,4 +1,5 @@ use async_graphql::http::MultipartOptions; +use poem::error::BadRequest; use poem::http::{header, Method}; use poem::web::Query; use poem::{async_trait, Error, FromRequest, Request, RequestBody, Result}; @@ -12,7 +13,7 @@ use tokio_util::compat::TokioAsyncReadCompatExt; /// # Example /// /// ``` -/// use poem::{handler, RouteMethod, route, EndpointExt}; +/// use poem::{handler, Route, post, EndpointExt}; /// use poem::web::{Json, Data}; /// use poem::middleware::AddData; /// use async_graphql_poem::GraphQLRequest; @@ -35,7 +36,7 @@ use tokio_util::compat::TokioAsyncReadCompatExt; /// } /// /// let schema = Schema::new(Query, EmptyMutation, EmptySubscription); -/// let app = route().at("/", RouteMethod::new().post(index.with(AddData::new(schema)))); +/// let app = Route::new().at("/", post(index.with(AddData::new(schema)))); /// ``` pub struct GraphQLRequest(pub async_graphql::Request); @@ -49,7 +50,7 @@ impl<'a> FromRequest<'a> for GraphQLRequest { .await? .0 .into_single() - .map_err(Error::bad_request)?, + .map_err(BadRequest)?, )) } } @@ -63,10 +64,7 @@ impl<'a> FromRequest<'a> for GraphQLBatchRequest { async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result { if req.method() == Method::GET { - let req = Query::from_request(req, body) - .await - .map_err(Error::bad_request)? - .0; + let req = Query::from_request(req, body).await?.0; Ok(Self(async_graphql::BatchRequest::Single(req))) } else { let content_type = req @@ -80,8 +78,7 @@ impl<'a> FromRequest<'a> for GraphQLBatchRequest { body.take()?.into_async_read().compat(), MultipartOptions::default(), ) - .await - .map_err(Error::bad_request)?, + .await?, )) } } diff --git a/integrations/poem/src/lib.rs b/integrations/poem/src/lib.rs index 0a140beb..7fcc5ebd 100644 --- a/integrations/poem/src/lib.rs +++ b/integrations/poem/src/lib.rs @@ -4,8 +4,10 @@ mod extractor; mod query; +mod response; mod subscription; pub use extractor::{GraphQLBatchRequest, GraphQLRequest}; pub use query::GraphQL; +pub use response::{GraphQLBatchResponse, GraphQLResponse}; pub use subscription::GraphQLSubscription; diff --git a/integrations/poem/src/query.rs b/integrations/poem/src/query.rs index 1b5d1d5e..73d20fbd 100644 --- a/integrations/poem/src/query.rs +++ b/integrations/poem/src/query.rs @@ -1,15 +1,14 @@ -use async_graphql::{BatchResponse as GraphQLBatchResponse, ObjectType, Schema, SubscriptionType}; -use poem::web::Json; +use async_graphql::{ObjectType, Schema, SubscriptionType}; use poem::{async_trait, Endpoint, FromRequest, Request, Result}; -use crate::GraphQLBatchRequest; +use crate::{GraphQLBatchRequest, GraphQLBatchResponse}; /// A GraphQL query endpoint. /// /// # Example /// /// ``` -/// use poem::{route, RouteMethod}; +/// use poem::{Route, post}; /// use async_graphql_poem::GraphQL; /// use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema}; /// @@ -25,7 +24,7 @@ use crate::GraphQLBatchRequest; /// type MySchema = Schema; /// /// let schema = Schema::new(Query, EmptyMutation, EmptySubscription); -/// let app = route().at("/", RouteMethod::new().post(GraphQL::new(schema))); +/// let app = Route::new().at("/", post(GraphQL::new(schema))); /// ``` pub struct GraphQL { schema: Schema, @@ -45,11 +44,11 @@ where Mutation: ObjectType + 'static, Subscription: SubscriptionType + 'static, { - type Output = Result>; + type Output = Result; async fn call(&self, req: Request) -> Self::Output { let (req, mut body) = req.split(); let req = GraphQLBatchRequest::from_request(&req, &mut body).await?; - Ok(Json(self.schema.execute_batch(req.0).await)) + Ok(GraphQLBatchResponse(self.schema.execute_batch(req.0).await)) } } diff --git a/integrations/poem/src/response.rs b/integrations/poem/src/response.rs new file mode 100644 index 00000000..7e0dfd68 --- /dev/null +++ b/integrations/poem/src/response.rs @@ -0,0 +1,50 @@ +use poem::http::header::HeaderName; +use poem::web::Json; +use poem::{IntoResponse, Response}; + +/// Response for `async_graphql::Request`. +pub struct GraphQLResponse(pub async_graphql::Response); + +impl From for GraphQLResponse { + fn from(resp: async_graphql::Response) -> Self { + Self(resp) + } +} + +impl IntoResponse for GraphQLResponse { + fn into_response(self) -> Response { + GraphQLBatchResponse(self.0.into()).into_response() + } +} + +/// Response for `async_graphql::BatchRequest`. +pub struct GraphQLBatchResponse(pub async_graphql::BatchResponse); + +impl From for GraphQLBatchResponse { + fn from(resp: async_graphql::BatchResponse) -> Self { + Self(resp) + } +} + +impl IntoResponse for GraphQLBatchResponse { + fn into_response(self) -> Response { + let mut resp = Json(&self.0).into_response(); + + if self.0.is_ok() { + if let Some(cache_control) = self.0.cache_control().value() { + if let Ok(value) = cache_control.try_into() { + resp.headers_mut().insert("cache-control", value); + } + } + } + + for (name, value) in self.0.http_headers() { + if let (Ok(name), Ok(value)) = (TryInto::::try_into(name), value.try_into()) + { + resp.headers_mut().append(name, value); + } + } + + resp + } +} diff --git a/integrations/poem/src/subscription.rs b/integrations/poem/src/subscription.rs index ed7a73ec..92cc025f 100644 --- a/integrations/poem/src/subscription.rs +++ b/integrations/poem/src/subscription.rs @@ -12,7 +12,7 @@ use poem::{http, Endpoint, FromRequest, IntoResponse, Request, Response, Result} /// # Example /// /// ``` -/// use poem::{route, RouteMethod}; +/// use poem::{Route, get}; /// use async_graphql_poem::GraphQLSubscription; /// use async_graphql::{EmptyMutation, Object, Schema, Subscription}; /// use futures_util::{Stream, stream}; @@ -38,7 +38,7 @@ use poem::{http, Endpoint, FromRequest, IntoResponse, Request, Response, Result} /// type MySchema = Schema; /// /// let schema = Schema::new(Query, EmptyMutation, Subscription); -/// let app = route().at("/ws", RouteMethod::new().get(GraphQLSubscription::new(schema))); +/// let app = Route::new().at("/ws", get(GraphQLSubscription::new(schema))); /// ``` pub struct GraphQLSubscription { schema: Schema, diff --git a/integrations/rocket/Cargo.toml b/integrations/rocket/Cargo.toml index 1cc81c93..cca3a9db 100644 --- a/integrations/rocket/Cargo.toml +++ b/integrations/rocket/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql-rocket" -version = "2.10.4" +version = "2.11.0" authors = ["Daniel Wiesenberg "] -edition = "2018" +edition = "2021" description = "async-graphql for Rocket.rs" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql/" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "rocket"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "=2.10.4" } +async-graphql = { path = "../..", version = "=2.11.0" } rocket = { version = "0.5.0-rc.1", default-features = false } serde = "1.0.126" diff --git a/integrations/tide/Cargo.toml b/integrations/tide/Cargo.toml index f0f91a0c..757fdf9f 100644 --- a/integrations/tide/Cargo.toml +++ b/integrations/tide/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql-tide" -version = "2.10.4" +version = "2.11.0" authors = ["vkill "] -edition = "2018" +edition = "2021" description = "async-graphql for tide" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql-tide/" @@ -16,7 +16,7 @@ default = ["websocket"] websocket = ["tide-websockets"] [dependencies] -async-graphql = { path = "../..", version = "=2.10.4" } +async-graphql = { path = "../..", version = "=2.11.0" } async-trait = "0.1.48" futures-util = "0.3.13" serde_json = "1.0.64" diff --git a/integrations/warp/Cargo.toml b/integrations/warp/Cargo.toml index f200ee01..c2034d1b 100644 --- a/integrations/warp/Cargo.toml +++ b/integrations/warp/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql-warp" -version = "2.10.4" +version = "2.11.0" authors = ["sunli ", "Koxiaet"] -edition = "2018" +edition = "2021" description = "async-graphql for warp" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql-warp/" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "=2.10.4" } +async-graphql = { path = "../..", version = "=2.11.0" } warp = { version = "0.3.0", default-features = false, features = ["websocket"] } futures-util = { version = "0.3.13", default-features = false, features = ["sink"] } diff --git a/integrations/warp/src/batch_request.rs b/integrations/warp/src/batch_request.rs index b9152ee8..e0ff25cf 100644 --- a/integrations/warp/src/batch_request.rs +++ b/integrations/warp/src/batch_request.rs @@ -1,4 +1,3 @@ -use std::convert::TryInto; use std::io; use std::io::ErrorKind; diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 4de70439..0dcf2008 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql-parser" -version = "2.10.4" +version = "2.11.0" authors = ["sunli ", "Koxiaet"] -edition = "2018" +edition = "2021" description = "GraphQL query parser for async-graphql" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql/" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql-value = { path = "../value", version = "=2.10.4" } +async-graphql-value = { path = "../value", version = "=2.11.0" } pest = "2.1.3" pest_derive = "2.1.0" serde_json = "1.0.64" diff --git a/parser/src/graphql.pest b/parser/src/graphql.pest index 1a7fcd5b..12cff935 100644 --- a/parser/src/graphql.pest +++ b/parser/src/graphql.pest @@ -10,7 +10,7 @@ executable_definition = { operation_definition | fragment_definition } operation_definition = { named_operation_definition | selection_set } named_operation_definition = { operation_type ~ name? ~ variable_definitions? ~ directives? ~ selection_set } variable_definitions = { "(" ~ variable_definition* ~ ")" } -variable_definition = { variable ~ ":" ~ type_ ~ default_value? } +variable_definition = { variable ~ ":" ~ type_ ~ directives? ~ default_value? } selection_set = { "{" ~ selection+ ~ "}" } selection = { field | inline_fragment | fragment_spread } @@ -86,6 +86,7 @@ directive_location = { | "FRAGMENT_DEFINITION" | "FRAGMENT_SPREAD" | "INLINE_FRAGMENT" + | "VARIABLE_DEFINITION" | "SCHEMA" | "SCALAR" | "OBJECT" @@ -115,7 +116,7 @@ value = { variable | number | string | boolean | null | enum_value | variable = { "$" ~ name } -number = @{ float | int } +number = @{ (float | int) ~ !name_start } float = { int ~ ((fractional ~ exponent) | fractional | exponent) } fractional = { "." ~ ASCII_DIGIT+ } exponent = { ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ } @@ -164,4 +165,5 @@ arguments = { "(" ~ argument+ ~ ")" } const_argument = { name ~ ":" ~ const_value } argument = { name ~ ":" ~ value } -name = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHA | ASCII_DIGIT | "_")* } +name_start = @{ (ASCII_ALPHA | "_") } +name = @{ name_start ~ (ASCII_ALPHA | ASCII_DIGIT | "_")* } diff --git a/parser/src/parse/executable.rs b/parser/src/parse/executable.rs index 2fc2a1e6..6ec0207d 100644 --- a/parser/src/parse/executable.rs +++ b/parser/src/parse/executable.rs @@ -208,6 +208,8 @@ fn parse_variable_definition( let variable = parse_variable(pairs.next().unwrap(), pc)?; let var_type = parse_type(pairs.next().unwrap(), pc)?; + + let directives = parse_opt_directives(&mut pairs, pc)?; let default_value = parse_if_rule(&mut pairs, Rule::default_value, |pair| { parse_default_value(pair, pc) })?; @@ -218,6 +220,7 @@ fn parse_variable_definition( VariableDefinition { name: variable, var_type, + directives, default_value, }, pos, diff --git a/parser/src/parse/mod.rs b/parser/src/parse/mod.rs index 54c10963..3a3e4259 100644 --- a/parser/src/parse/mod.rs +++ b/parser/src/parse/mod.rs @@ -312,3 +312,21 @@ fn parse_name(pair: Pair, pc: &mut PositionCalculator) -> Result DirectiveLocation::FragmentDefinition, "FRAGMENT_SPREAD" => DirectiveLocation::FragmentSpread, "INLINE_FRAGMENT" => DirectiveLocation::InlineFragment, + "VARIABLE_DEFINITION" => DirectiveLocation::VariableDefinition, "SCHEMA" => DirectiveLocation::Schema, "SCALAR" => DirectiveLocation::Scalar, "OBJECT" => DirectiveLocation::Object, diff --git a/parser/src/pos.rs b/parser/src/pos.rs index 3f37e3e7..a52b062f 100644 --- a/parser/src/pos.rs +++ b/parser/src/pos.rs @@ -10,7 +10,7 @@ use std::str::Chars; /// Original position of an element in source code. /// /// You can serialize and deserialize it to the GraphQL `locations` format -/// ([reference](https://spec.graphql.org/June2018/#sec-Errors)). +/// ([reference](https://spec.graphql.org/October2021/#sec-Errors)). #[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Default, Hash, Serialize, Deserialize)] pub struct Pos { /// One-based line number. diff --git a/parser/src/types/executable.rs b/parser/src/types/executable.rs index 88371ac1..0e70b13a 100644 --- a/parser/src/types/executable.rs +++ b/parser/src/types/executable.rs @@ -5,7 +5,7 @@ use async_graphql_value::{ConstValue, Name, Value}; /// An executable GraphQL file or request string. /// -/// [Reference](https://spec.graphql.org/June2018/#ExecutableDocument). +/// [Reference](https://spec.graphql.org/October2021/#ExecutableDocument). #[derive(Debug, Clone)] pub struct ExecutableDocument { /// The operations of the document. @@ -93,7 +93,7 @@ enum OperationsIterInner<'a> { /// A GraphQL operation, such as `mutation($content:String!) { makePost(content: $content) { id } }`. /// -/// [Reference](https://spec.graphql.org/June2018/#OperationDefinition). +/// [Reference](https://spec.graphql.org/October2021/#OperationDefinition). #[derive(Debug, Clone)] pub struct OperationDefinition { /// The type of operation. @@ -108,13 +108,15 @@ pub struct OperationDefinition { /// A variable definition inside a list of variable definitions, for example `$name:String!`. /// -/// [Reference](https://spec.graphql.org/June2018/#VariableDefinition). +/// [Reference](https://spec.graphql.org/October2021/#VariableDefinition). #[derive(Debug, Clone)] pub struct VariableDefinition { /// The name of the variable, without the preceding `$`. pub name: Positioned, /// The type of the variable. pub var_type: Positioned, + /// The variable's directives. + pub directives: Vec>, /// The optional default value of the variable. pub default_value: Option>, } @@ -139,7 +141,7 @@ impl VariableDefinition { /// A set of fields to be selected, for example `{ name age }`. /// -/// [Reference](https://spec.graphql.org/June2018/#SelectionSet). +/// [Reference](https://spec.graphql.org/October2021/#SelectionSet). #[derive(Debug, Default, Clone)] pub struct SelectionSet { /// The fields to be selected. @@ -148,7 +150,7 @@ pub struct SelectionSet { /// A part of an object to be selected; a single field, a fragment spread or an inline fragment. /// -/// [Reference](https://spec.graphql.org/June2018/#Selection). +/// [Reference](https://spec.graphql.org/October2021/#Selection). #[derive(Debug, Clone)] pub enum Selection { /// Select a single field, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`. @@ -182,7 +184,7 @@ impl Selection { /// A field being selected on an object, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`. /// -/// [Reference](https://spec.graphql.org/June2018/#Field). +/// [Reference](https://spec.graphql.org/October2021/#Field). #[derive(Debug, Clone)] pub struct Field { /// The optional field alias. @@ -217,7 +219,7 @@ impl Field { /// A fragment selector, such as `... userFields`. /// -/// [Reference](https://spec.graphql.org/June2018/#FragmentSpread). +/// [Reference](https://spec.graphql.org/October2021/#FragmentSpread). #[derive(Debug, Clone)] pub struct FragmentSpread { /// The name of the fragment being selected. @@ -228,7 +230,7 @@ pub struct FragmentSpread { /// An inline fragment selector, such as `... on User { name }`. /// -/// [Reference](https://spec.graphql.org/June2018/#InlineFragment). +/// [Reference](https://spec.graphql.org/October2021/#InlineFragment). #[derive(Debug, Clone)] pub struct InlineFragment { /// The type condition. @@ -241,7 +243,7 @@ pub struct InlineFragment { /// The definition of a fragment, such as `fragment userFields on User { name age }`. /// -/// [Reference](https://spec.graphql.org/June2018/#FragmentDefinition). +/// [Reference](https://spec.graphql.org/October2021/#FragmentDefinition). #[derive(Debug, Clone)] pub struct FragmentDefinition { /// The type this fragment operates on. @@ -254,7 +256,7 @@ pub struct FragmentDefinition { /// A type a fragment can apply to (`on` followed by the type). /// -/// [Reference](https://spec.graphql.org/June2018/#TypeCondition). +/// [Reference](https://spec.graphql.org/October2021/#TypeCondition). #[derive(Debug, Clone)] pub struct TypeCondition { /// The type this fragment applies to. diff --git a/parser/src/types/mod.rs b/parser/src/types/mod.rs index b52e35d2..151fa94f 100644 --- a/parser/src/types/mod.rs +++ b/parser/src/types/mod.rs @@ -4,7 +4,7 @@ //! [`ServiceDocument`](struct.ServiceDocument.html), representing an executable GraphQL query and a //! GraphQL service respectively. //! -//! This follows the [June 2018 edition of the GraphQL spec](https://spec.graphql.org/June2018/). +//! This follows the [June 2018 edition of the GraphQL spec](https://spec.graphql.org/October2021/). mod executable; mod service; @@ -19,7 +19,7 @@ pub use service::*; /// The type of an operation; `query`, `mutation` or `subscription`. /// -/// [Reference](https://spec.graphql.org/June2018/#OperationType). +/// [Reference](https://spec.graphql.org/October2021/#OperationType). #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum OperationType { /// A query. @@ -42,7 +42,7 @@ impl Display for OperationType { /// A GraphQL type, for example `String` or `[String!]!`. /// -/// [Reference](https://spec.graphql.org/June2018/#Type). +/// [Reference](https://spec.graphql.org/October2021/#Type). #[derive(Debug, PartialEq, Eq, Clone)] pub struct Type { /// The base type. @@ -105,7 +105,7 @@ impl Display for BaseType { /// from [`Directive`](struct.Directive.html) in that it uses [`ConstValue`](enum.ConstValue.html) /// instead of [`Value`](enum.Value.html). /// -/// [Reference](https://spec.graphql.org/June2018/#Directive). +/// [Reference](https://spec.graphql.org/October2021/#Directive). #[derive(Debug, Clone)] pub struct ConstDirective { /// The name of the directive. @@ -140,7 +140,7 @@ impl ConstDirective { /// A GraphQL directive, such as `@deprecated(reason: "Use the other field")`. /// -/// [Reference](https://spec.graphql.org/June2018/#Directive). +/// [Reference](https://spec.graphql.org/October2021/#Directive). #[derive(Debug, Clone)] pub struct Directive { /// The name of the directive. diff --git a/parser/src/types/service.rs b/parser/src/types/service.rs index 2a3e81f5..f5e2bcdb 100644 --- a/parser/src/types/service.rs +++ b/parser/src/types/service.rs @@ -5,7 +5,7 @@ use async_graphql_value::Name; /// A GraphQL file or request string defining a GraphQL service. /// -/// [Reference](https://spec.graphql.org/June2018/#Document). +/// [Reference](https://spec.graphql.org/October2021/#Document). #[derive(Debug, Clone)] pub struct ServiceDocument { /// The definitions of this document. @@ -14,8 +14,8 @@ pub struct ServiceDocument { /// A definition concerning the type system of a GraphQL service. /// -/// [Reference](https://spec.graphql.org/June2018/#TypeSystemDefinition). This enum also covers -/// [extensions](https://spec.graphql.org/June2018/#TypeSystemExtension). +/// [Reference](https://spec.graphql.org/October2021/#TypeSystemDefinition). This enum also covers +/// [extensions](https://spec.graphql.org/October2021/#TypeSystemExtension). #[derive(Debug, Clone)] pub enum TypeSystemDefinition { /// The definition of the schema of the service. @@ -28,8 +28,8 @@ pub enum TypeSystemDefinition { /// The definition of the schema in a GraphQL service. /// -/// [Reference](https://spec.graphql.org/June2018/#SchemaDefinition). This also covers -/// [extensions](https://spec.graphql.org/June2018/#SchemaExtension). +/// [Reference](https://spec.graphql.org/October2021/#SchemaDefinition). This also covers +/// [extensions](https://spec.graphql.org/October2021/#SchemaExtension). #[derive(Debug, Clone)] pub struct SchemaDefinition { /// Whether the schema is an extension of another schema. @@ -46,8 +46,8 @@ pub struct SchemaDefinition { /// The definition of a type in a GraphQL service. /// -/// [Reference](https://spec.graphql.org/June2018/#TypeDefinition). This also covers -/// [extensions](https://spec.graphql.org/June2018/#TypeExtension). +/// [Reference](https://spec.graphql.org/October2021/#TypeDefinition). This also covers +/// [extensions](https://spec.graphql.org/October2021/#TypeExtension). #[derive(Debug, Clone)] pub struct TypeDefinition { /// Whether the type is an extension of another type. @@ -81,7 +81,7 @@ pub enum TypeKind { /// The definition of an object type. /// -/// [Reference](https://spec.graphql.org/June2018/#ObjectType). +/// [Reference](https://spec.graphql.org/October2021/#ObjectType). #[derive(Debug, Clone)] pub struct ObjectType { /// The interfaces implemented by the object. @@ -92,7 +92,7 @@ pub struct ObjectType { /// The definition of a field inside an object or interface. /// -/// [Reference](https://spec.graphql.org/June2018/#FieldDefinition). +/// [Reference](https://spec.graphql.org/October2021/#FieldDefinition). #[derive(Debug, Clone)] pub struct FieldDefinition { /// The description of the field. @@ -109,7 +109,7 @@ pub struct FieldDefinition { /// The definition of an interface type. /// -/// [Reference](https://spec.graphql.org/June2018/#InterfaceType). +/// [Reference](https://spec.graphql.org/October2021/#InterfaceType). #[derive(Debug, Clone)] pub struct InterfaceType { /// The interfaces implemented by the interface. @@ -120,7 +120,7 @@ pub struct InterfaceType { /// The definition of a union type. /// -/// [Reference](https://spec.graphql.org/June2018/#UnionType). +/// [Reference](https://spec.graphql.org/October2021/#UnionType). #[derive(Debug, Clone)] pub struct UnionType { /// The member types of the union. @@ -129,7 +129,7 @@ pub struct UnionType { /// The definition of an enum. /// -/// [Reference](https://spec.graphql.org/June2018/#EnumType). +/// [Reference](https://spec.graphql.org/October2021/#EnumType). #[derive(Debug, Clone)] pub struct EnumType { /// The possible values of the enum. @@ -138,7 +138,7 @@ pub struct EnumType { /// The definition of a value inside an enum. /// -/// [Reference](https://spec.graphql.org/June2018/#EnumValueDefinition). +/// [Reference](https://spec.graphql.org/October2021/#EnumValueDefinition). #[derive(Debug, Clone)] pub struct EnumValueDefinition { /// The description of the argument. @@ -151,7 +151,7 @@ pub struct EnumValueDefinition { /// The definition of an input object. /// -/// [Reference](https://spec.graphql.org/June2018/#InputObjectType). +/// [Reference](https://spec.graphql.org/October2021/#InputObjectType). #[derive(Debug, Clone)] pub struct InputObjectType { /// The fields of the input object. @@ -160,7 +160,7 @@ pub struct InputObjectType { /// The definition of an input value inside the arguments of a field. /// -/// [Reference](https://spec.graphql.org/June2018/#InputValueDefinition). +/// [Reference](https://spec.graphql.org/October2021/#InputValueDefinition). #[derive(Debug, Clone)] pub struct InputValueDefinition { /// The description of the argument. @@ -177,7 +177,7 @@ pub struct InputValueDefinition { /// The definition of a directive in a service. /// -/// [Reference](https://spec.graphql.org/June2018/#DirectiveDefinition). +/// [Reference](https://spec.graphql.org/October2021/#DirectiveDefinition). #[derive(Debug, Clone)] pub struct DirectiveDefinition { /// The description of the directive. @@ -192,7 +192,7 @@ pub struct DirectiveDefinition { /// Where a directive can apply to. /// -/// [Reference](https://spec.graphql.org/June2018/#DirectiveLocation). +/// [Reference](https://spec.graphql.org/October2021/#DirectiveLocation). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DirectiveLocation { /// A [query](enum.OperationType.html#variant.Query) [operation](struct.OperationDefinition.html). @@ -233,4 +233,6 @@ pub enum DirectiveLocation { /// An [input value definition](struct.InputValueDefinition.html) on an input object but not a /// field. InputFieldDefinition, + /// An [variable definition](struct.VariableDefinition.html). + VariableDefinition, } diff --git a/parser/tests/executables/variable_directive.graphql b/parser/tests/executables/variable_directive.graphql new file mode 100644 index 00000000..b6a9d07b --- /dev/null +++ b/parser/tests/executables/variable_directive.graphql @@ -0,0 +1,3 @@ +query Foo($a: Int @directive = 10, $b: Int @directive) { + value +} diff --git a/parser/tests/services/directive.graphql b/parser/tests/services/directive.graphql index 401b6f43..456904bd 100644 --- a/parser/tests/services/directive.graphql +++ b/parser/tests/services/directive.graphql @@ -4,3 +4,5 @@ directive @test1(service: String!) on FIELD_DEFINITION directive @test2(service: String!) on FIELD directive @test3(service: String!) on ENUM_VALUE directive @test4(service: String!) on ENUM + +directive @test5(service: String!) on VARIABLE_DEFINITION diff --git a/src/error.rs b/src/error.rs index e0c0485e..871c7c61 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,9 @@ +use std::any::Any; use std::collections::BTreeMap; +use std::error::Error as StdError; use std::fmt::{self, Debug, Display, Formatter}; use std::marker::PhantomData; +use std::sync::Arc; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -20,10 +23,13 @@ impl ErrorExtensionValues { } /// An error in a GraphQL server. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct ServerError { /// An explanatory message of the error. pub message: String, + /// The source of the error, comes from [`ResolverError`]. + #[serde(skip)] + pub source: Option>, /// Where the error occurred. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub locations: Vec, @@ -39,17 +45,55 @@ fn error_extensions_is_empty(values: &Option) -> bool { values.as_ref().map_or(true, |values| values.0.is_empty()) } +impl Debug for ServerError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("ServerError") + .field("message", &self.message) + .field("locations", &self.locations) + .field("path", &self.path) + .field("extensions", &self.extensions) + .finish() + } +} + +impl PartialEq for ServerError { + fn eq(&self, other: &Self) -> bool { + self.message.eq(&other.message) + && self.locations.eq(&other.locations) + && self.path.eq(&other.path) + && self.extensions.eq(&other.extensions) + } +} + impl ServerError { /// Create a new server error with the message. pub fn new(message: impl Into, pos: Option) -> Self { Self { message: message.into(), + source: None, locations: pos.map(|pos| vec![pos]).unwrap_or_default(), path: Vec::new(), extensions: None, } } + /// Get the source of the error. + /// + /// # Examples + /// + /// ```rust + /// use std::string::FromUtf8Error; + /// use async_graphql::{ResolverError, Error, ServerError, Pos}; + /// + /// let bytes = vec![0, 159]; + /// let err: Error = String::from_utf8(bytes).map_err(ResolverError::new).unwrap_err().into(); + /// let server_err: ServerError = err.into_server_error(Pos { line: 1, column: 1 }); + /// assert!(server_err.source::().is_some()); + /// ``` + pub fn source(&self) -> Option<&T> { + self.source.as_ref().map(|err| err.downcast_ref()).flatten() + } + #[doc(hidden)] pub fn with_path(self, path: Vec) -> Self { Self { path, ..self } @@ -72,6 +116,7 @@ impl From for ServerError { fn from(e: parser::Error) -> Self { Self { message: e.to_string(), + source: None, locations: e.positions().collect(), path: Vec::new(), extensions: None, @@ -160,20 +205,39 @@ impl From for InputValueError { pub type InputValueResult = Result>; /// An error with a message and optional extensions. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Clone, Serialize)] pub struct Error { /// The error message. pub message: String, + /// The source of the error, comes from [`ResolverError`]. + #[serde(skip)] + pub source: Option>, /// Extensions to the error. #[serde(skip_serializing_if = "error_extensions_is_empty")] pub extensions: Option, } +impl Debug for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Error") + .field("message", &self.message) + .field("extensions", &self.extensions) + .finish() + } +} + +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + self.message.eq(&other.message) && self.extensions.eq(&other.extensions) + } +} + impl Error { /// Create an error from the given error message. pub fn new(message: impl Into) -> Self { Self { message: message.into(), + source: None, extensions: None, } } @@ -183,6 +247,7 @@ impl Error { pub fn into_server_error(self, pos: Pos) -> ServerError { ServerError { message: self.message, + source: self.source, locations: vec![pos], path: Vec::new(), extensions: self.extensions, @@ -190,10 +255,21 @@ impl Error { } } -impl From for Error { +impl From for Error { fn from(e: T) -> Self { Self { message: e.to_string(), + source: None, + extensions: None, + } + } +} + +impl From for Error { + fn from(e: ResolverError) -> Self { + Self { + message: e.message, + source: Some(e.error), extensions: None, } } @@ -267,40 +343,58 @@ impl From for ParseRequestError { /// An error which can be extended into a `Error`. pub trait ErrorExtensions: Sized { /// Convert the error to a `Error`. - fn extend(&self) -> Error; + fn extend(self) -> Error; /// Add extensions to the error, using a callback to make the extensions. fn extend_with(self, cb: C) -> Error where C: FnOnce(&Self, &mut ErrorExtensionValues), { - let message = self.extend().message; - let mut extensions = self.extend().extensions.unwrap_or_default(); - cb(&self, &mut extensions); + let mut new_extensions = Default::default(); + cb(&self, &mut new_extensions); + + let Error { + message, + source, + extensions, + } = self.extend(); + + let mut extensions = extensions.unwrap_or_default(); + extensions.0.extend(new_extensions.0); + Error { message, + source, extensions: Some(extensions), } } } impl ErrorExtensions for Error { - fn extend(&self) -> Error { - self.clone() + fn extend(self) -> Error { + self } } // implementing for &E instead of E gives the user the possibility to implement for E which does // not conflict with this implementation acting as a fallback. -impl ErrorExtensions for &E { - fn extend(&self) -> Error { +impl ErrorExtensions for &E { + fn extend(self) -> Error { Error { message: self.to_string(), + source: None, extensions: None, } } } +impl ErrorExtensions for ResolverError { + #[inline] + fn extend(self) -> Error { + Error::from(self) + } +} + /// Extend a `Result`'s error value with [`ErrorExtensions`](trait.ErrorExtensions.html). pub trait ResultExt: Sized { /// Extend the error value of the result with the callback. @@ -335,3 +429,27 @@ where } } } + +/// A wrapper around a dynamic error type for resolver. +#[derive(Debug)] +pub struct ResolverError { + message: String, + error: Arc, +} + +impl From for ResolverError { + fn from(err: T) -> Self { + Self { + message: err.to_string(), + error: Arc::new(err), + } + } +} + +impl ResolverError { + /// Create a new failure. + #[inline] + pub fn new(err: T) -> Self { + From::from(err) + } +} diff --git a/src/http/playground_source.rs b/src/http/playground_source.rs index 3a6d2278..952330fd 100644 --- a/src/http/playground_source.rs +++ b/src/http/playground_source.rs @@ -624,7 +624,7 @@ impl<'a> GraphQLPlaygroundConfig<'a> { #[cfg(test)] mod tests { use super::*; - use std::collections::BTreeMap; + use indexmap::IndexMap; #[test] fn test_with_setting_can_use_any_json_value() { @@ -634,7 +634,7 @@ mod tests { .with_setting("number", 10) .with_setting("null", Value::Null) .with_setting("array", Vec::from([1, 2, 3])) - .with_setting("object", BTreeMap::new()); + .with_setting("object", IndexMap::new()); let json = serde_json::to_value(settings).unwrap(); let settings = json["settings"].as_object().unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 37fa27b5..37da9c4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ //! * [Docs](https://docs.rs/async-graphql) //! * [GitHub repository](https://github.com/async-graphql/async-graphql) //! * [Cargo package](https://crates.io/crates/async-graphql) -//! * Minimum supported Rust version: 1.51 or later +//! * Minimum supported Rust version: 1.56.1 or later //! //! ## Features //! @@ -210,7 +210,7 @@ pub use base::{ }; pub use error::{ Error, ErrorExtensionValues, ErrorExtensions, InputValueError, InputValueResult, - ParseRequestError, PathSegment, Result, ResultExt, ServerError, ServerResult, + ParseRequestError, PathSegment, ResolverError, Result, ResultExt, ServerError, ServerResult, }; pub use look_ahead::Lookahead; pub use registry::CacheControl; @@ -279,6 +279,7 @@ pub type FieldResult = Result; /// | default | Use `Default::default` for default value | none | Y | /// | default | Argument default value | literal | Y | /// | default_with | Expression to generate default value | code string | Y | +/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y | /// | validator | Input value validator | [`InputValueValidator`](validators/trait.InputValueValidator.html) | Y | /// | 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 | @@ -288,6 +289,14 @@ pub type FieldResult = Result; /// | serial | Resolve each field sequentially. | bool | Y | /// | key | Is entity key(for Federation) | bool | Y | /// +/// # Derived argument parameters +/// +/// | Attribute | description | Type | Optional | +/// |--------------|------------------------------------------|------------ |----------| +/// | name | Generated derived field name | string | N | +/// | into | Type to derived an into | string | Y | +/// | with | Function to apply to manage advanced use cases | string| Y | +/// /// # Valid field return types /// /// - Scalar values, such as `i32` and `bool`. `usize`, `isize`, `u128` and `i128` are not @@ -443,6 +452,7 @@ pub use async_graphql_derive::Object; /// | name | Field name | string | Y | /// | deprecation | Field deprecated | bool | Y | /// | deprecation | Field deprecation reason | string | Y | +/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y | /// | owned | Field resolver return a ownedship value | bool | Y | /// | cache_control | Field cache control | [`CacheControl`](struct.CacheControl.html) | Y | /// | external | Mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. | bool | Y | @@ -452,6 +462,16 @@ pub use async_graphql_derive::Object; /// | 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 | /// +/// # Derived argument parameters +/// +/// | Attribute | description | Type | Optional | +/// |--------------|------------------------------------------|------------ |----------| +/// | name | Generated derived field name | string | N | +/// | into | Type to derived an into | string | Y | +/// | owned | Field resolver return a ownedship value | bool | Y | +/// | with | Function to apply to manage advanced use cases | string| Y | +/// +/// /// # Examples /// /// ```rust @@ -498,6 +518,7 @@ pub use async_graphql_derive::SimpleObject; /// | name | Field name | string | Y | /// | deprecation | Field deprecated | bool | Y | /// | deprecation | Field deprecation reason | string | Y | +/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y | /// | cache_control | Field cache control | [`CacheControl`](struct.CacheControl.html) | Y | /// | external | Mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. | bool | Y | /// | provides | Annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | string | Y | @@ -507,6 +528,14 @@ pub use async_graphql_derive::SimpleObject; /// | 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 | /// +/// # Derived argument parameters +/// +/// | Attribute | description | Type | Optional | +/// |--------------|------------------------------------------|------------ |----------| +/// | name | Generated derived field name | string | N | +/// | into | Type to derived an into | string | Y | +/// | with | Function to apply to manage advanced use cases | string| Y | +/// /// # Examples /// /// ```rust @@ -958,6 +987,7 @@ pub use async_graphql_derive::Subscription; /// | Attribute | description | Type | Optional | /// |-------------|---------------------------|----------|----------| /// | name | Scalar name | string | Y | +/// | specified_by_url | Provide a specification URL for this scalar type, it must link to a human-readable specification of the data format, serialization and coercion rules for this scalar. | string | Y | /// pub use async_graphql_derive::Scalar; @@ -971,8 +1001,9 @@ pub use async_graphql_derive::Scalar; /// |-------------|---------------------------|----------|----------| /// | name | If this attribute is provided then define a new scalar, otherwise it is just a transparent proxy for the internal scalar. | string | Y | /// | name | If this attribute is provided then define a new scalar, otherwise it is just a transparent proxy for the internal scalar. | bool | Y | -/// | visible(Only valid for new scalars.) | 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(Only valid for new scalars.) | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | +/// | visible(Only valid for new scalars) | 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(Only valid for new scalars) | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | +/// | specified_by_url(Only valid for new scalars) | Provide a specification URL for this scalar type, it must link to a human-readable specification of the data format, serialization and coercion rules for this scalar. | string | Y | /// /// # Examples /// diff --git a/src/look_ahead.rs b/src/look_ahead.rs index ccd874a8..a142221c 100644 --- a/src/look_ahead.rs +++ b/src/look_ahead.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::convert::TryFrom; use crate::parser::types::{Field, FragmentDefinition, Selection, SelectionSet}; use crate::Context; diff --git a/src/model/type.rs b/src/model/type.rs index 4f20506c..b8ddfdb9 100644 --- a/src/model/type.rs +++ b/src/model/type.rs @@ -218,4 +218,16 @@ impl<'a> __Type<'a> { None } } + + #[graphql(name = "specifiedByURL")] + async fn specified_by_url(&self) -> Option<&'a str> { + if let TypeDetail::Named(registry::MetaType::Scalar { + specified_by_url, .. + }) = &self.detail + { + *specified_by_url + } else { + None + } + } } diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 254cae2c..efbd4c9f 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -189,6 +189,7 @@ pub enum MetaType { description: Option<&'static str>, is_valid: fn(value: &Value) -> bool, visible: Option, + specified_by_url: Option<&'static str>, }, Object { name: String, @@ -198,6 +199,7 @@ pub enum MetaType { extends: bool, keys: Option>, visible: Option, + is_subscription: bool, }, Interface { name: String, @@ -374,6 +376,7 @@ impl Registry { extends: false, keys: None, visible: None, + is_subscription: false, }, ); let ty = f(self); @@ -507,6 +510,7 @@ impl Registry { extends: false, keys: None, visible: None, + is_subscription: false, }, ); diff --git a/src/request.rs b/src/request.rs index 53c83978..183ec798 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,6 +1,5 @@ use std::any::Any; use std::collections::HashMap; -use std::convert::TryFrom; use std::fmt::{self, Debug, Formatter}; use serde::{Deserialize, Deserializer, Serialize}; diff --git a/src/resolver_utils/container.rs b/src/resolver_utils/container.rs index dfe30795..d0dc3048 100644 --- a/src/resolver_utils/container.rs +++ b/src/resolver_utils/container.rs @@ -1,7 +1,8 @@ -use std::collections::BTreeMap; use std::future::Future; use std::pin::Pin; +use indexmap::IndexMap; + use crate::extensions::ResolveInfo; use crate::parser::types::Selection; use crate::registry::MetaType; @@ -74,7 +75,7 @@ pub async fn resolve_container_serial<'a, T: ContainerType + ?Sized>( resolve_container_inner(ctx, root, false).await } -fn insert_value(target: &mut BTreeMap, name: Name, value: Value) { +fn insert_value(target: &mut IndexMap, name: Name, value: Value) { if let Some(prev_value) = target.get_mut(&name) { if let Value::Object(target_map) = prev_value { if let Value::Object(obj) = value { @@ -118,7 +119,7 @@ async fn resolve_container_inner<'a, T: ContainerType + ?Sized>( results }; - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); for (name, value) in res { insert_value(&mut map, name, value); } diff --git a/src/resolver_utils/scalar.rs b/src/resolver_utils/scalar.rs index 2a788345..062547b0 100644 --- a/src/resolver_utils/scalar.rs +++ b/src/resolver_utils/scalar.rs @@ -68,6 +68,9 @@ pub trait ScalarType: Sized + Send { /// // Rename to `MV` and add description. /// // scalar!(MyValue, "MV", "This is my value"); /// +/// // Rename to `MV`, add description and specifiedByURL. +/// // scalar!(MyValue, "MV", "This is my value", "https://tools.ietf.org/html/rfc4122"); +/// /// struct Query; /// /// #[Object] @@ -92,23 +95,47 @@ pub trait ScalarType: Sized + Send { /// ``` #[macro_export] macro_rules! scalar { + ($ty:ty, $name:literal, $desc:literal, $specified_by_url:literal) => { + $crate::scalar_internal!( + $ty, + $name, + ::std::option::Option::Some($desc), + ::std::option::Option::Some($specified_by_url) + ); + }; + ($ty:ty, $name:literal, $desc:literal) => { - $crate::scalar_internal!($ty, $name, ::std::option::Option::Some($desc)); + $crate::scalar_internal!( + $ty, + $name, + ::std::option::Option::Some($desc), + ::std::option::Option::None + ); }; ($ty:ty, $name:literal) => { - $crate::scalar_internal!($ty, $name, ::std::option::Option::None); + $crate::scalar_internal!( + $ty, + $name, + ::std::option::Option::None, + ::std::option::Option::None + ); }; ($ty:ty) => { - $crate::scalar_internal!($ty, ::std::stringify!($ty), ::std::option::Option::None); + $crate::scalar_internal!( + $ty, + ::std::stringify!($ty), + ::std::option::Option::None, + ::std::option::Option::None + ); }; } #[macro_export] #[doc(hidden)] macro_rules! scalar_internal { - ($ty:ty, $name:expr, $desc:expr) => { + ($ty:ty, $name:expr, $desc:expr, $specified_by_url:expr) => { impl $crate::Type for $ty { fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> { ::std::borrow::Cow::Borrowed($name) @@ -122,6 +149,7 @@ macro_rules! scalar_internal { description: $desc, is_valid: |value| <$ty as $crate::ScalarType>::is_valid(value), visible: ::std::option::Option::None, + specified_by_url: $specified_by_url, }) } } diff --git a/src/response.rs b/src/response.rs index 1a3fea0a..d851cca9 100644 --- a/src/response.rs +++ b/src/response.rs @@ -98,6 +98,7 @@ impl Response { } /// Response for batchable queries +#[allow(clippy::large_enum_variant)] #[derive(Debug, Serialize)] #[serde(untagged)] pub enum BatchResponse { diff --git a/src/types/connection/connection_type.rs b/src/types/connection/connection_type.rs index 9080004a..ff0193c6 100644 --- a/src/types/connection/connection_type.rs +++ b/src/types/connection/connection_type.rs @@ -190,6 +190,7 @@ where extends: false, keys: None, visible: None, + is_subscription: false, } }) } diff --git a/src/types/connection/edge.rs b/src/types/connection/edge.rs index f6c893bb..afa69142 100644 --- a/src/types/connection/edge.rs +++ b/src/types/connection/edge.rs @@ -107,6 +107,7 @@ where extends: false, keys: None, visible: None, + is_subscription: false, } }) } diff --git a/src/types/empty_mutation.rs b/src/types/empty_mutation.rs index 481a3cb5..da90a82c 100644 --- a/src/types/empty_mutation.rs +++ b/src/types/empty_mutation.rs @@ -45,6 +45,7 @@ impl Type for EmptyMutation { extends: false, keys: None, visible: None, + is_subscription: false, }) } } diff --git a/src/types/empty_subscription.rs b/src/types/empty_subscription.rs index 0c98e473..10d28950 100644 --- a/src/types/empty_subscription.rs +++ b/src/types/empty_subscription.rs @@ -25,6 +25,7 @@ impl Type for EmptySubscription { extends: false, keys: None, visible: None, + is_subscription: true, }) } } diff --git a/src/types/external/chrono_tz.rs b/src/types/external/chrono_tz.rs index 86d16372..fafefbef 100644 --- a/src/types/external/chrono_tz.rs +++ b/src/types/external/chrono_tz.rs @@ -2,7 +2,11 @@ use chrono_tz::Tz; use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value}; -#[Scalar(internal, name = "TimeZone")] +#[Scalar( + internal, + name = "TimeZone", + specified_by_url = "http://www.iana.org/time-zones" +)] impl ScalarType for Tz { fn parse(value: Value) -> InputValueResult { match value { diff --git a/src/types/external/datetime.rs b/src/types/external/datetime.rs index e56167e3..cdcb2de4 100644 --- a/src/types/external/datetime.rs +++ b/src/types/external/datetime.rs @@ -5,7 +5,11 @@ use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value}; /// Implement the DateTime scalar /// /// The input/output is a string in RFC3339 format. -#[Scalar(internal, name = "DateTime")] +#[Scalar( + internal, + name = "DateTime", + specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339" +)] impl ScalarType for DateTime { fn parse(value: Value) -> InputValueResult { match &value { @@ -22,7 +26,11 @@ impl ScalarType for DateTime { /// Implement the DateTime scalar /// /// The input/output is a string in RFC3339 format. -#[Scalar(internal, name = "DateTime")] +#[Scalar( + internal, + name = "DateTime", + specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339" +)] impl ScalarType for DateTime { fn parse(value: Value) -> InputValueResult { match &value { @@ -39,7 +47,11 @@ impl ScalarType for DateTime { /// Implement the DateTime scalar /// /// The input/output is a string in RFC3339 format. -#[Scalar(internal, name = "DateTime")] +#[Scalar( + internal, + name = "DateTime", + specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339" +)] impl ScalarType for DateTime { fn parse(value: Value) -> InputValueResult { match &value { diff --git a/src/types/external/duration.rs b/src/types/external/duration.rs new file mode 100644 index 00000000..b0624f1e --- /dev/null +++ b/src/types/external/duration.rs @@ -0,0 +1,25 @@ +use chrono::Duration; +use iso8601_duration as iso8601; + +use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value}; + +/// Implement the Duration scalar +/// +/// The input/output is a string in ISO8601 format. +#[Scalar( + internal, + name = "Duration", + specified_by_url = "https://en.wikipedia.org/wiki/ISO_8601#Durations" +)] +impl ScalarType for Duration { + fn parse(value: Value) -> InputValueResult { + match &value { + Value::String(s) => Ok(Duration::from_std(iso8601::Duration::parse(s)?.to_std())?), + _ => Err(InputValueError::expected_type(value)), + } + } + + fn to_value(&self) -> Value { + Value::String(self.to_string()) + } +} diff --git a/src/types/external/json_object/btreemap.rs b/src/types/external/json_object/btreemap.rs index c3f14660..bfaff892 100644 --- a/src/types/external/json_object/btreemap.rs +++ b/src/types/external/json_object/btreemap.rs @@ -2,6 +2,8 @@ use std::collections::BTreeMap; use std::fmt::Display; use std::str::FromStr; +use indexmap::IndexMap; + use crate::{ InputType, InputValueError, InputValueResult, Name, OutputType, Scalar, ScalarType, Value, }; @@ -33,7 +35,7 @@ where } fn to_value(&self) -> Value { - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); for (name, value) in self { map.insert(Name::new(name.to_string()), value.to_value()); } diff --git a/src/types/external/json_object/hashmap.rs b/src/types/external/json_object/hashmap.rs index d805f6ca..9fcc1f05 100644 --- a/src/types/external/json_object/hashmap.rs +++ b/src/types/external/json_object/hashmap.rs @@ -1,8 +1,10 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::fmt::Display; use std::hash::Hash; use std::str::FromStr; +use indexmap::IndexMap; + use crate::{ InputType, InputValueError, InputValueResult, Name, OutputType, Scalar, ScalarType, Value, }; @@ -34,7 +36,7 @@ where } fn to_value(&self) -> Value { - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); for (name, value) in self { map.insert(Name::new(name.to_string()), value.to_value()); } diff --git a/src/types/external/list/array.rs b/src/types/external/list/array.rs index 4d832431..bc870539 100644 --- a/src/types/external/list/array.rs +++ b/src/types/external/list/array.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::convert::TryInto; use crate::parser::types::Field; use crate::resolver_utils::resolve_list; diff --git a/src/types/external/mod.rs b/src/types/external/mod.rs index cf109f39..73b326e5 100644 --- a/src/types/external/mod.rs +++ b/src/types/external/mod.rs @@ -20,6 +20,8 @@ mod chrono_tz; mod datetime; #[cfg(feature = "decimal")] mod decimal; +#[cfg(feature = "chrono-duration")] +mod duration; #[cfg(feature = "chrono")] mod naive_time; #[cfg(feature = "secrecy")] diff --git a/src/types/external/url.rs b/src/types/external/url.rs index b9a8dda8..110a6e23 100644 --- a/src/types/external/url.rs +++ b/src/types/external/url.rs @@ -2,7 +2,7 @@ use url::Url; use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value}; -#[Scalar(internal)] +#[Scalar(internal, specified_by_url = "http://url.spec.whatwg.org/")] /// URL is a String implementing the [URL Standard](http://url.spec.whatwg.org/) impl ScalarType for Url { fn parse(value: Value) -> InputValueResult { diff --git a/src/types/external/uuid.rs b/src/types/external/uuid.rs index 917d4cd2..135c72bd 100644 --- a/src/types/external/uuid.rs +++ b/src/types/external/uuid.rs @@ -2,7 +2,11 @@ use uuid::Uuid; use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value}; -#[Scalar(internal, name = "UUID")] +#[Scalar( + internal, + name = "UUID", + specified_by_url = "http://tools.ietf.org/html/rfc4122" +)] /// A UUID is a unique 128-bit number, stored as 16 octets. UUIDs are parsed as Strings /// within GraphQL. UUIDs are used to assign unique identifiers to entities without requiring a central /// allocating authority. diff --git a/src/types/id.rs b/src/types/id.rs index f0bec09f..04807019 100644 --- a/src/types/id.rs +++ b/src/types/id.rs @@ -1,4 +1,3 @@ -use std::convert::TryFrom; use std::num::ParseIntError; use std::ops::{Deref, DerefMut}; diff --git a/src/types/json.rs b/src/types/json.rs index 065397f9..2444d73f 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -85,6 +85,7 @@ impl Type for OutputJson { description: None, is_valid: |_| true, visible: None, + specified_by_url: None, }) } } diff --git a/src/types/maybe_undefined.rs b/src/types/maybe_undefined.rs index 81a6048f..fe722062 100644 --- a/src/types/maybe_undefined.rs +++ b/src/types/maybe_undefined.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::ops::Deref; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -6,7 +7,7 @@ use crate::{registry, InputType, InputValueError, InputValueResult, Type, Value} /// Similar to `Option`, but it has three states, `undefined`, `null` and `x`. /// -/// **Reference:** +/// **Reference:** /// /// # Examples /// @@ -61,34 +62,34 @@ impl Default for MaybeUndefined { } impl MaybeUndefined { - /// Returns true if the MaybeUndefined is undefined. + /// Returns true if the `MaybeUndefined` is undefined. #[inline] - pub fn is_undefined(&self) -> bool { + pub const fn is_undefined(&self) -> bool { matches!(self, MaybeUndefined::Undefined) } - /// Returns true if the MaybeUndefined is null. + /// Returns true if the `MaybeUndefined` is null. #[inline] - pub fn is_null(&self) -> bool { + pub const fn is_null(&self) -> bool { matches!(self, MaybeUndefined::Null) } - /// Returns true if the MaybeUndefined is value. + /// Returns true if the `MaybeUndefined` contains value. #[inline] - pub fn is_value(&self) -> bool { + pub const fn is_value(&self) -> bool { matches!(self, MaybeUndefined::Value(_)) } - /// Borrow the value, returns `None` if the value is `undefined` or `null`, otherwise returns `Some(T)`. + /// Borrow the value, returns `None` if the the `MaybeUndefined` is `undefined` or `null`, otherwise returns `Some(T)`. #[inline] - pub fn value(&self) -> Option<&T> { + pub const fn value(&self) -> Option<&T> { match self { MaybeUndefined::Value(value) => Some(value), _ => None, } } - /// Convert MaybeUndefined to Option. + /// Converts the `MaybeUndefined` to `Option`. #[inline] pub fn take(self) -> Option { match self { @@ -96,6 +97,81 @@ impl MaybeUndefined { _ => None, } } + + /// Converts the `MaybeUndefined` to `Option>`. + #[inline] + pub const fn as_opt_ref(&self) -> Option> { + match self { + MaybeUndefined::Undefined => None, + MaybeUndefined::Null => Some(None), + MaybeUndefined::Value(value) => Some(Some(value)), + } + } + + /// Converts the `MaybeUndefined` to `Option>`. + #[inline] + pub fn as_opt_deref(&self) -> Option> + where + U: ?Sized, + T: Deref, + { + match self { + MaybeUndefined::Undefined => None, + MaybeUndefined::Null => Some(None), + MaybeUndefined::Value(value) => Some(Some(value.deref())), + } + } + + /// Returns `true` if the `MaybeUndefined` contains the given value. + #[inline] + pub fn contains_value(&self, x: &U) -> bool + where + U: PartialEq, + { + match self { + MaybeUndefined::Value(y) => x == y, + _ => false, + } + } + + /// Returns `true` if the `MaybeUndefined` contains the given nullable value. + #[inline] + pub fn contains(&self, x: &Option) -> bool + where + U: PartialEq, + { + match self { + MaybeUndefined::Value(y) => matches!(x, Some(v) if v == y), + MaybeUndefined::Null => matches!(x, None), + MaybeUndefined::Undefined => false, + } + } + + /// Maps a `MaybeUndefined` to `MaybeUndefined` by applying a function to the contained nullable value + #[inline] + pub fn map) -> Option>(self, f: F) -> MaybeUndefined { + match self { + MaybeUndefined::Value(v) => match f(Some(v)) { + Some(v) => MaybeUndefined::Value(v), + None => MaybeUndefined::Null, + }, + MaybeUndefined::Null => match f(None) { + Some(v) => MaybeUndefined::Value(v), + None => MaybeUndefined::Null, + }, + MaybeUndefined::Undefined => MaybeUndefined::Undefined, + } + } + + /// Maps a `MaybeUndefined` to `MaybeUndefined` by applying a function to the contained value + #[inline] + pub fn map_value U>(self, f: F) -> MaybeUndefined { + match self { + MaybeUndefined::Value(v) => MaybeUndefined::Value(f(v)), + MaybeUndefined::Null => MaybeUndefined::Null, + MaybeUndefined::Undefined => MaybeUndefined::Undefined, + } + } } impl Type for MaybeUndefined { @@ -132,6 +208,24 @@ impl InputType for MaybeUndefined { } } +impl MaybeUndefined> { + /// Transposes a `MaybeUndefined` of a [`Result`] into a [`Result`] of a `MaybeUndefined`. + /// + /// [`MaybeUndefined::Undefined`] will be mapped to [`Ok`]`(`[`MaybeUndefined::Undefined`]`)`. + /// [`MaybeUndefined::Null`] will be mapped to [`Ok`]`(`[`MaybeUndefined::Null`]`)`. + /// [`MaybeUndefined::Value`]`(`[`Ok`]`(_))` and [`MaybeUndefined::Value`]`(`[`Err`]`(_))` will be mapped to + /// [`Ok`]`(`[`MaybeUndefined::Value`]`(_))` and [`Err`]`(_)`. + #[inline] + pub fn transpose(self) -> Result, E> { + match self { + MaybeUndefined::Undefined => Ok(MaybeUndefined::Undefined), + MaybeUndefined::Null => Ok(MaybeUndefined::Null), + MaybeUndefined::Value(Ok(v)) => Ok(MaybeUndefined::Value(v)), + MaybeUndefined::Value(Err(e)) => Err(e), + } + } +} + impl Serialize for MaybeUndefined { fn serialize(&self, serializer: S) -> Result { match self { @@ -166,6 +260,16 @@ impl From> for Option> { } } +impl From>> for MaybeUndefined { + fn from(value: Option>) -> Self { + match value { + Some(Some(value)) => Self::Value(value), + Some(None) => Self::Null, + None => Self::Undefined, + } + } +} + #[cfg(test)] mod tests { use crate::*; @@ -260,4 +364,117 @@ mod tests { Some(Some(42)) ); } + + #[test] + fn test_as_opt_ref() { + let mut value: MaybeUndefined; + let mut r: Option>; + + value = MaybeUndefined::Undefined; + r = value.as_opt_ref(); + assert_eq!(r, None); + + value = MaybeUndefined::Null; + r = value.as_opt_ref(); + assert_eq!(r, Some(None)); + + value = MaybeUndefined::Value("abc".to_string()); + r = value.as_opt_ref(); + assert_eq!(r, Some(Some(&"abc".to_string()))); + } + + #[test] + fn test_as_opt_deref() { + let mut value: MaybeUndefined; + let mut r: Option>; + + value = MaybeUndefined::Undefined; + r = value.as_opt_deref(); + assert_eq!(r, None); + + value = MaybeUndefined::Null; + r = value.as_opt_deref(); + assert_eq!(r, Some(None)); + + value = MaybeUndefined::Value("abc".to_string()); + r = value.as_opt_deref(); + assert_eq!(r, Some(Some("abc"))); + } + + #[test] + fn test_contains_value() { + let test = "abc"; + + let mut value: MaybeUndefined = MaybeUndefined::Undefined; + assert!(!value.contains_value(&test)); + + value = MaybeUndefined::Null; + assert!(!value.contains_value(&test)); + + value = MaybeUndefined::Value("abc".to_string()); + assert!(value.contains_value(&test)); + } + + #[test] + fn test_contains() { + let test = Some("abc"); + let none: Option<&str> = None; + + let mut value: MaybeUndefined = MaybeUndefined::Undefined; + assert!(!value.contains(&test)); + assert!(!value.contains(&none)); + + value = MaybeUndefined::Null; + assert!(!value.contains(&test)); + assert!(value.contains(&none)); + + value = MaybeUndefined::Value("abc".to_string()); + assert!(value.contains(&test)); + assert!(!value.contains(&none)); + } + + #[test] + fn test_map_value() { + let mut value: MaybeUndefined = MaybeUndefined::Undefined; + assert_eq!(value.map_value(|v| v > 2), MaybeUndefined::Undefined); + + value = MaybeUndefined::Null; + assert_eq!(value.map_value(|v| v > 2), MaybeUndefined::Null); + + value = MaybeUndefined::Value(5); + assert_eq!(value.map_value(|v| v > 2), MaybeUndefined::Value(true)); + } + + #[test] + fn test_map() { + let mut value: MaybeUndefined = MaybeUndefined::Undefined; + assert_eq!(value.map(|v| Some(v.is_some())), MaybeUndefined::Undefined); + + value = MaybeUndefined::Null; + assert_eq!( + value.map(|v| Some(v.is_some())), + MaybeUndefined::Value(false) + ); + + value = MaybeUndefined::Value(5); + assert_eq!( + value.map(|v| Some(v.is_some())), + MaybeUndefined::Value(true) + ); + } + + #[test] + fn test_transpose() { + let mut value: MaybeUndefined> = MaybeUndefined::Undefined; + assert_eq!(value.transpose(), Ok(MaybeUndefined::Undefined)); + + value = MaybeUndefined::Null; + assert_eq!(value.transpose(), Ok(MaybeUndefined::Null)); + + value = MaybeUndefined::Value(Ok(5)); + assert_eq!(value.transpose(), Ok(MaybeUndefined::Value(5))); + + value = MaybeUndefined::Value(Err("eror")); + assert_eq!(value.transpose(), Err("eror")); + } } diff --git a/src/types/merged_object.rs b/src/types/merged_object.rs index c3cb026c..7b236ee4 100644 --- a/src/types/merged_object.rs +++ b/src/types/merged_object.rs @@ -50,6 +50,7 @@ impl Type for MergedObject { extends: false, keys: None, visible: None, + is_subscription: false, } }) } diff --git a/src/types/upload.rs b/src/types/upload.rs index 460f8d24..c6ef0fde 100644 --- a/src/types/upload.rs +++ b/src/types/upload.rs @@ -111,6 +111,7 @@ impl Type for Upload { description: None, is_valid: |value| matches!(value, Value::String(_)), visible: None, + specified_by_url: Some("https://github.com/jaydenseric/graphql-multipart-request-spec"), }) } } diff --git a/src/validation/rules/fields_on_correct_type.rs b/src/validation/rules/fields_on_correct_type.rs index 343f75f9..1b4ad9ef 100644 --- a/src/validation/rules/fields_on_correct_type.rs +++ b/src/validation/rules/fields_on_correct_type.rs @@ -329,4 +329,9 @@ mod tests { "#, ); } + + #[test] + fn typename_in_subscription_root() { + expect_fails_rule!(factory, "subscription { __typename }"); + } } diff --git a/src/validation/test_harness.rs b/src/validation/test_harness.rs index 30bf611b..c691be7a 100644 --- a/src/validation/test_harness.rs +++ b/src/validation/test_harness.rs @@ -4,6 +4,7 @@ use once_cell::sync::Lazy; +use crate::futures_util::Stream; use crate::parser::types::ExecutableDocument; use crate::validation::visitor::{visit, RuleError, Visitor, VisitorContext}; use crate::*; @@ -345,8 +346,17 @@ impl MutationRoot { } } -static TEST_HARNESS: Lazy> = - Lazy::new(|| Schema::new(QueryRoot, MutationRoot, EmptySubscription)); +pub struct SubscriptionRoot; + +#[Subscription(internal)] +impl SubscriptionRoot { + async fn values(&self) -> impl Stream { + futures_util::stream::once(async move { 10 }) + } +} + +static TEST_HARNESS: Lazy> = + Lazy::new(|| Schema::new(QueryRoot, MutationRoot, SubscriptionRoot)); pub(crate) fn validate<'a, V, F>( doc: &'a ExecutableDocument, diff --git a/src/validation/visitor.rs b/src/validation/visitor.rs index 772d4518..96c37717 100644 --- a/src/validation/visitor.rs +++ b/src/validation/visitor.rs @@ -611,6 +611,17 @@ fn visit_selection<'a, V: Visitor<'a>>( visit_field(v, ctx, field); }, ); + } else if ctx.current_type().map(|ty| match ty { + MetaType::Object { + is_subscription, .. + } => *is_subscription, + _ => false, + }) == Some(true) + { + ctx.report_error( + vec![field.pos], + "Unknown field \"__typename\" on type \"Subscription\".", + ); } } Selection::FragmentSpread(fragment_spread) => { @@ -840,6 +851,7 @@ impl From for ServerError { fn from(e: RuleError) -> Self { Self { message: e.message, + source: None, locations: e.locations, path: Vec::new(), extensions: e.extensions, diff --git a/tests/derived_field.rs b/tests/derived_field.rs new file mode 100644 index 00000000..36724c81 --- /dev/null +++ b/tests/derived_field.rs @@ -0,0 +1,382 @@ +use async_graphql::*; + +#[tokio::test] +pub async fn test_derived_field_object() { + use serde::{Deserialize, Serialize}; + + struct Query; + + #[derive(Serialize, Deserialize)] + struct ValueDerived(String); + + scalar!(ValueDerived); + + impl From for ValueDerived { + fn from(value: i32) -> Self { + ValueDerived(format!("{}", value)) + } + } + + #[Object] + impl Query { + #[graphql(derived(name = "value2", into = "ValueDerived"))] + async fn value1(&self, #[graphql(default = 100)] input: i32) -> i32 { + input + } + } + + let query = "{ value1 value2 }"; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(query).await.data, + value!({ + "value1": 100, + "value2": "100", + }) + ); + + let query = "{ value1(input: 1) value2(input: 2) }"; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(query).await.data, + value!({ + "value1": 1, + "value2": "2", + }) + ); +} + +#[tokio::test] +pub async fn test_derived_field_object_with() { + use serde::{Deserialize, Serialize}; + + struct Query; + + #[derive(Serialize, Deserialize)] + struct ValueDerived(String); + + scalar!(ValueDerived); + + impl From for ValueDerived { + fn from(value: i32) -> Self { + ValueDerived(format!("{}", value)) + } + } + + fn option_to_option>(value: Option) -> Option { + value.map(|x| x.into()) + } + + #[Object] + impl Query { + #[graphql(derived( + name = "value2", + into = "Option", + with = "option_to_option" + ))] + async fn value1(&self, #[graphql(default = 100)] input: i32) -> Option { + Some(input) + } + } + + let query = "{ value1 value2 }"; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(query).await.data, + value!({ + "value1": 100, + "value2": "100", + }) + ); + + let query = "{ value1(input: 1) value2(input: 2) }"; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(query).await.data, + value!({ + "value1": 1, + "value2": "2", + }) + ); +} + +#[tokio::test] +pub async fn test_derived_field_simple_object() { + use serde::{Deserialize, Serialize}; + + struct Query; + + #[derive(Serialize, Deserialize)] + struct ValueDerived(String); + + scalar!(ValueDerived); + + impl From for ValueDerived { + fn from(value: i32) -> Self { + ValueDerived(format!("{}", value)) + } + } + + #[derive(SimpleObject)] + struct TestObj { + #[graphql(owned, derived(name = "value2", into = "ValueDerived"))] + pub value1: i32, + } + + #[Object] + impl Query { + async fn test(&self, #[graphql(default = 100)] input: i32) -> TestObj { + TestObj { value1: input } + } + } + + let query = "{ test { value1 value2 } }"; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(query).await.data, + value!({ + "test": { + "value1": 100, + "value2": "100", + } + }) + ); + + let query = "{ test(input: 2) { value1 value2 }}"; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + dbg!(schema.execute(query).await); + assert_eq!( + schema.execute(query).await.data, + value!({ + "test": { + "value1": 2, + "value2": "2", + } + }) + ); +} + +#[tokio::test] +pub async fn test_derived_field_simple_object_option() { + use serde::{Deserialize, Serialize}; + + struct Query; + + #[derive(Serialize, Deserialize, Clone)] + struct ValueDerived(String); + + #[derive(Serialize, Deserialize, Clone)] + struct ValueDerived2(String); + + scalar!(ValueDerived); + scalar!(ValueDerived2); + + impl From for ValueDerived2 { + fn from(value: ValueDerived) -> Self { + ValueDerived2(value.0) + } + } + + fn option_to_option>(value: Option) -> Option { + value.map(|x| x.into()) + } + + fn vec_to_vec>(value: Vec) -> Vec { + value.into_iter().map(|x| x.into()).collect() + } + + fn vecopt_to_vecopt>(value: Vec>) -> Vec> { + value.into_iter().map(|x| x.map(|opt| opt.into())).collect() + } + + fn optvec_to_optvec>(value: Option>) -> Option> { + value.map(|x| x.into_iter().map(|y| y.into()).collect()) + } + + #[derive(SimpleObject)] + struct TestObj { + #[graphql(derived( + owned, + name = "value2", + into = "Option", + with = "option_to_option" + ))] + pub value1: Option, + #[graphql(derived( + owned, + name = "value_vec_2", + into = "Vec", + with = "vec_to_vec" + ))] + pub value_vec_1: Vec, + #[graphql(derived( + owned, + name = "value_opt_vec_2", + into = "Option>", + with = "optvec_to_optvec" + ))] + pub value_opt_vec_1: Option>, + #[graphql(derived( + owned, + name = "value_vec_opt_2", + into = "Vec>", + with = "vecopt_to_vecopt" + ))] + pub value_vec_opt_1: Vec>, + } + + #[Object] + impl Query { + async fn test(&self) -> TestObj { + TestObj { + value1: Some(ValueDerived("Test".to_string())), + value_vec_1: vec![ValueDerived("Test".to_string())], + value_opt_vec_1: Some(vec![ValueDerived("Test".to_string())]), + value_vec_opt_1: vec![Some(ValueDerived("Test".to_string()))], + } + } + } + + let query = "{ test { value1 value2 valueVec1 valueVec2 valueOptVec1 valueOptVec2 } }"; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(query).await.data, + value!({ + "test": { + "value1": "Test", + "value2": "Test", + "valueVec1": vec!["Test"], + "valueVec2": vec!["Test"], + "valueOptVec1": vec!["Test"], + "valueOptVec2": vec!["Test"], + } + }) + ); +} + +#[tokio::test] +pub async fn test_derived_field_complex_object() { + use serde::{Deserialize, Serialize}; + + #[derive(SimpleObject)] + #[graphql(complex)] + struct MyObj { + a: i32, + #[graphql(owned, derived(name = "f", into = "ValueDerived"))] + b: i32, + } + + #[derive(Serialize, Deserialize)] + struct ValueDerived(String); + + scalar!(ValueDerived); + + impl From for ValueDerived { + fn from(value: i32) -> Self { + ValueDerived(format!("{}", value)) + } + } + + #[ComplexObject] + impl MyObj { + async fn c(&self) -> i32 { + self.a + self.b + } + + #[graphql(derived(name = "e", into = "ValueDerived"))] + async fn d(&self, v: i32) -> i32 { + self.a + self.b + v + } + } + + struct Query; + + #[Object] + impl Query { + async fn obj(&self) -> MyObj { + MyObj { a: 10, b: 20 } + } + } + + let query = "{ obj { a b c d(v:100) e(v: 200) f } }"; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + dbg!(schema.execute(query).await); + assert_eq!( + schema.execute(query).await.data, + value!({ + "obj": { + "a": 10, + "b": 20, + "c": 30, + "d": 130, + "e": "230", + "f": "20", + }, + }) + ); +} + +#[tokio::test] +pub async fn test_derived_field_complex_object_derived() { + use serde::{Deserialize, Serialize}; + + #[derive(SimpleObject)] + #[graphql(complex)] + struct MyObj { + a: i32, + #[graphql(owned, derived(name = "f", into = "ValueDerived"))] + b: i32, + } + + #[derive(Serialize, Deserialize)] + struct ValueDerived(String); + + scalar!(ValueDerived); + + impl From for ValueDerived { + fn from(value: i32) -> Self { + ValueDerived(format!("{}", value)) + } + } + + fn option_to_option>(value: Option) -> Option { + value.map(|x| x.into()) + } + + #[ComplexObject] + impl MyObj { + async fn c(&self) -> i32 { + self.a + self.b + } + + #[graphql(derived(name = "e", into = "Option", with = "option_to_option"))] + async fn d(&self, v: i32) -> Option { + Some(self.a + self.b + v) + } + } + + struct Query; + + #[Object] + impl Query { + async fn obj(&self) -> MyObj { + MyObj { a: 10, b: 20 } + } + } + + let query = "{ obj { a b c d(v:100) e(v: 200) f } }"; + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema.execute(query).await.data, + value!({ + "obj": { + "a": 10, + "b": 20, + "c": 30, + "d": 130, + "e": "230", + "f": "20", + }, + }) + ); +} diff --git a/tests/error_ext.rs b/tests/error_ext.rs index 6214b08d..4ca56a19 100644 --- a/tests/error_ext.rs +++ b/tests/error_ext.rs @@ -75,3 +75,112 @@ pub async fn test_error_extensions() { }) ); } + +#[tokio::test] +pub async fn test_failure() { + #[derive(thiserror::Error, Debug, PartialEq)] + enum MyError { + #[error("error1")] + Error1, + + #[error("error2")] + Error2, + } + + struct Query; + + #[Object] + impl Query { + async fn failure(&self) -> Result { + Err(ResolverError::new(MyError::Error1).into()) + } + + async fn failure2(&self) -> Result { + Err(ResolverError::new(MyError::Error2))?; + Ok(1) + } + + async fn failure3(&self) -> Result { + Err(ResolverError::new(MyError::Error1) + .extend_with(|_, values| values.set("a", 1)) + .extend_with(|_, values| values.set("b", 2)))?; + Ok(1) + } + + async fn failure4(&self) -> Result { + Err(ResolverError::new(MyError::Error2)) + .extend_err(|_, values| values.set("a", 1)) + .extend_err(|_, values| values.set("b", 2))?; + Ok(1) + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + let err = schema + .execute("{ failure }") + .await + .into_result() + .unwrap_err() + .remove(0); + assert_eq!(err.source::().unwrap(), &MyError::Error1); + + let err = schema + .execute("{ failure2 }") + .await + .into_result() + .unwrap_err() + .remove(0); + assert_eq!(err.source::().unwrap(), &MyError::Error2); + + let err = schema + .execute("{ failure3 }") + .await + .into_result() + .unwrap_err() + .remove(0); + assert_eq!(err.source::().unwrap(), &MyError::Error1); + assert_eq!( + err.extensions, + Some({ + let mut values = ErrorExtensionValues::default(); + values.set("a", 1); + values.set("b", 2); + values + }) + ); + + let err = schema + .execute("{ failure4 }") + .await + .into_result() + .unwrap_err() + .remove(0); + assert_eq!(err.source::().unwrap(), &MyError::Error2); + assert_eq!( + err.extensions, + Some({ + let mut values = ErrorExtensionValues::default(); + values.set("a", 1); + values.set("b", 2); + values + }) + ); +} + +#[tokio::test] +pub async fn test_failure2() { + #[derive(thiserror::Error, Debug, PartialEq)] + enum MyError { + #[error("error1")] + Error1, + } + + struct Query; + + #[Object] + impl Query { + async fn failure(&self) -> Result { + Err(MyError::Error1)? + } + } +} diff --git a/tests/federation.rs b/tests/federation.rs index 36279fa1..d335ad91 100644 --- a/tests/federation.rs +++ b/tests/federation.rs @@ -242,6 +242,7 @@ pub async fn test_find_entity_with_context() { schema.execute(query).await.into_result().unwrap_err(), vec![ServerError { message: "Not found".to_string(), + source: None, locations: vec![Pos { line: 2, column: 13 diff --git a/tests/field_features.rs b/tests/field_features.rs index 8bb3d13e..0329cb47 100644 --- a/tests/field_features.rs +++ b/tests/field_features.rs @@ -85,6 +85,7 @@ pub async fn test_field_features() { vec![ServerError { message: r#"Unknown field "valueAbc" on type "QueryRoot". Did you mean "value"?"# .to_owned(), + source: None, locations: vec![Pos { column: 3, line: 1 }], path: Vec::new(), extensions: None, @@ -113,6 +114,7 @@ pub async fn test_field_features() { vec![ServerError { message: r#"Unknown field "valueAbc" on type "MyObj". Did you mean "value"?"# .to_owned(), + source: None, locations: vec![Pos { column: 9, line: 1 }], path: Vec::new(), extensions: None, @@ -148,6 +150,7 @@ pub async fn test_field_features() { .errors, vec![ServerError { message: r#"Unknown field "valuesAbc" on type "SubscriptionRoot". Did you mean "values", "valuesBson"?"#.to_owned(), + source: None, locations: vec![Pos { column: 16, line: 1 diff --git a/tests/guard.rs b/tests/guard.rs index bd2b88b6..829eb586 100644 --- a/tests/guard.rs +++ b/tests/guard.rs @@ -99,6 +99,7 @@ pub async fn test_guard_simple_rule() { .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, @@ -127,6 +128,7 @@ pub async fn test_guard_simple_rule() { .errors, vec![ServerError { message: "Forbidden".to_string(), + source: None, locations: vec![Pos { line: 1, column: 16 @@ -176,6 +178,7 @@ pub async fn test_guard_and_operator() { .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, @@ -195,6 +198,7 @@ pub async fn test_guard_and_operator() { .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, @@ -214,6 +218,7 @@ pub async fn test_guard_and_operator() { .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, @@ -283,6 +288,7 @@ pub async fn test_guard_or_operator() { .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, @@ -332,6 +338,7 @@ pub async fn test_guard_chain_operator() { .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, @@ -352,6 +359,7 @@ pub async fn test_guard_chain_operator() { .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, @@ -372,6 +380,7 @@ pub async fn test_guard_chain_operator() { .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, @@ -392,6 +401,7 @@ pub async fn test_guard_chain_operator() { .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, @@ -483,6 +493,7 @@ pub async fn test_guard_race_operator() { .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, @@ -538,6 +549,7 @@ pub async fn test_guard_use_params() { .unwrap_err(), vec![ServerError { message: "Forbidden".to_string(), + source: None, locations: vec![Pos { line: 1, column: 3 }], path: vec![PathSegment::Field("get".to_owned())], extensions: None, diff --git a/tests/input_validators.rs b/tests/input_validators.rs index 4807903e..6cbd9743 100644 --- a/tests/input_validators.rs +++ b/tests/input_validators.rs @@ -67,6 +67,7 @@ pub async fn test_input_validator_string_min_length() { .expect_err(&should_fail_msg), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -84,6 +85,7 @@ pub async fn test_input_validator_string_min_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -177,6 +179,7 @@ pub async fn test_input_validator_string_max_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -194,6 +197,7 @@ pub async fn test_input_validator_string_max_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -286,6 +290,7 @@ pub async fn test_input_validator_chars_min_length() { .expect_err(&should_fail_msg), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -303,6 +308,7 @@ pub async fn test_input_validator_chars_min_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -397,6 +403,7 @@ pub async fn test_input_validator_chars_max_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -414,6 +421,7 @@ pub async fn test_input_validator_chars_max_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -534,6 +542,7 @@ pub async fn test_input_validator_string_email() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -552,6 +561,7 @@ pub async fn test_input_validator_string_email() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -682,6 +692,7 @@ pub async fn test_input_validator_string_mac() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg.clone(), + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -700,6 +711,7 @@ pub async fn test_input_validator_string_mac() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg.clone(), + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -717,6 +729,7 @@ pub async fn test_input_validator_string_mac() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -735,6 +748,7 @@ pub async fn test_input_validator_string_mac() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -792,6 +806,7 @@ pub async fn test_input_validator_string_mac() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -810,6 +825,7 @@ pub async fn test_input_validator_string_mac() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -851,6 +867,7 @@ pub async fn test_input_validator_string_mac() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -869,6 +886,7 @@ pub async fn test_input_validator_string_mac() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -929,6 +947,7 @@ pub async fn test_input_validator_int_range() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -946,6 +965,7 @@ pub async fn test_input_validator_int_range() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -1034,6 +1054,7 @@ pub async fn test_input_validator_int_less_than() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -1051,6 +1072,7 @@ pub async fn test_input_validator_int_less_than() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -1141,6 +1163,7 @@ pub async fn test_input_validator_int_greater_than() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -1158,6 +1181,7 @@ pub async fn test_input_validator_int_greater_than() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -1241,6 +1265,7 @@ pub async fn test_input_validator_int_nonzero() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -1258,6 +1283,7 @@ pub async fn test_input_validator_int_nonzero() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -1345,6 +1371,7 @@ pub async fn test_input_validator_int_equal() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -1362,6 +1389,7 @@ pub async fn test_input_validator_int_equal() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -1461,6 +1489,7 @@ pub async fn test_input_validator_list_max_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -1478,6 +1507,7 @@ pub async fn test_input_validator_list_max_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -1577,6 +1607,7 @@ pub async fn test_input_validator_list_min_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -1594,6 +1625,7 @@ pub async fn test_input_validator_list_min_length() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -1701,6 +1733,7 @@ pub async fn test_input_validator_operator_or() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -1718,6 +1751,7 @@ pub async fn test_input_validator_operator_or() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -1818,6 +1852,7 @@ pub async fn test_input_validator_operator_and() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 17 @@ -1835,6 +1870,7 @@ pub async fn test_input_validator_operator_and() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 14 @@ -1938,6 +1974,7 @@ pub async fn test_input_validator_variable() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: field_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 37 @@ -1955,6 +1992,7 @@ pub async fn test_input_validator_variable() { .expect_err(&should_fail_msg[..]), vec![ServerError { message: object_error_msg, + source: None, locations: vec!(Pos { line: 1, column: 34 @@ -2041,6 +2079,7 @@ pub async fn test_custom_input_validator_with_extensions() { .expect_err(should_fail_msg), vec![ServerError { message: field_error_msg.into(), + source: None, locations: vec!(Pos { line: 1, column: 17 diff --git a/tests/input_value.rs b/tests/input_value.rs index aa3e04bb..6ea3f871 100644 --- a/tests/input_value.rs +++ b/tests/input_value.rs @@ -18,6 +18,7 @@ pub async fn test_input_value_custom_error() { vec![ServerError { message: "Failed to parse \"Int\": Only integers from -128 to 127 are accepted." .to_owned(), + source: None, locations: vec![Pos { line: 1, column: 14, diff --git a/tests/list.rs b/tests/list.rs index b48c5c16..90a62a09 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -167,6 +167,7 @@ pub async fn test_array_type() { vec![ServerError { message: r#"Failed to parse "[Int!]": Expected input type "[Int; 6]", found [Int; 5]."# .to_owned(), + source: None, locations: vec![Pos { line: 1, column: 22, diff --git a/tests/preserve_order.rs b/tests/preserve_order.rs new file mode 100644 index 00000000..f4aa96bd --- /dev/null +++ b/tests/preserve_order.rs @@ -0,0 +1,60 @@ +use async_graphql::*; + +#[tokio::test] +pub async fn test_preserve_order() { + #[derive(SimpleObject)] + struct Root { + a: i32, + b: i32, + c: i32, + } + + let schema = Schema::new(Root { a: 1, b: 2, c: 3 }, EmptyMutation, EmptySubscription); + assert_eq!( + schema + .execute("{ a c b }") + .await + .into_result() + .unwrap() + .data, + value!({ + "a": 1, "c": 3, "b": 2 + }) + ); + assert_eq!( + serde_json::to_string( + &schema + .execute("{ a c b }") + .await + .into_result() + .unwrap() + .data + ) + .unwrap(), + r#"{"a":1,"c":3,"b":2}"# + ); + + assert_eq!( + schema + .execute("{ c b a }") + .await + .into_result() + .unwrap() + .data, + value!({ + "c": 3, "b": 2, "a": 1 + }) + ); + assert_eq!( + serde_json::to_string( + &schema + .execute("{ c b a }") + .await + .into_result() + .unwrap() + .data + ) + .unwrap(), + r#"{"c":3,"b":2,"a":1}"# + ); +} diff --git a/tests/result.rs b/tests/result.rs index 71616e91..c4608ae9 100644 --- a/tests/result.rs +++ b/tests/result.rs @@ -34,12 +34,14 @@ pub async fn test_fieldresult() { errors: vec![ ServerError { message: "TestError".to_string(), + source: None, locations: vec![Pos { line: 1, column: 3 }], path: vec![PathSegment::Field("error1".to_owned())], extensions: None, }, ServerError { message: "TestError".to_string(), + source: None, locations: vec![Pos { line: 1, column: 19, @@ -60,6 +62,7 @@ pub async fn test_fieldresult() { .unwrap_err(), vec![ServerError { message: "TestError".to_string(), + source: None, locations: vec![Pos { line: 1, column: 3 }], path: vec![PathSegment::Field("optError".to_owned())], extensions: None, @@ -74,6 +77,7 @@ pub async fn test_fieldresult() { .unwrap_err(), vec![ServerError { message: "TestError".to_string(), + source: None, locations: vec![Pos { line: 1, column: 3 }], path: vec![ PathSegment::Field("vecError".to_owned()), @@ -188,6 +192,7 @@ pub async fn test_error_propagation() { cache_control: Default::default(), errors: vec![ServerError { message: "myerror".to_string(), + source: None, locations: vec![Pos { line: 1, column: 20, @@ -215,6 +220,7 @@ pub async fn test_error_propagation() { cache_control: Default::default(), errors: vec![ServerError { message: "myerror".to_string(), + source: None, locations: vec![Pos { line: 1, column: 23, @@ -238,6 +244,7 @@ pub async fn test_error_propagation() { cache_control: Default::default(), errors: vec![ServerError { message: "myerror".to_string(), + source: None, locations: vec![Pos { line: 1, column: 23, @@ -267,6 +274,7 @@ pub async fn test_error_propagation() { cache_control: Default::default(), errors: vec![ServerError { message: "myerror".to_string(), + source: None, locations: vec![Pos { line: 1, column: 23, diff --git a/tests/scalar.rs b/tests/scalar.rs index 414dd109..410d37bb 100644 --- a/tests/scalar.rs +++ b/tests/scalar.rs @@ -11,7 +11,12 @@ mod test_mod { #[tokio::test] pub async fn test_scalar_macro() { - scalar!(test_mod::MyValue, "MV", "DESC"); + scalar!( + test_mod::MyValue, + "MV", + "DESC", + "https://tools.ietf.org/html/rfc4122" + ); struct Query; @@ -26,7 +31,7 @@ pub async fn test_scalar_macro() { let schema = Schema::new(Query, EmptyMutation, EmptySubscription); assert_eq!( schema - .execute(r#"{ __type(name:"MV") { name description } }"#) + .execute(r#"{ __type(name:"MV") { name description specifiedByURL } }"#) .await .into_result() .unwrap() @@ -35,6 +40,7 @@ pub async fn test_scalar_macro() { "__type": { "name": "MV", "description": "DESC", + "specifiedByURL": "https://tools.ietf.org/html/rfc4122", } }) ); diff --git a/tests/subscription.rs b/tests/subscription.rs index 7d43c3f1..750c9405 100644 --- a/tests/subscription.rs +++ b/tests/subscription.rs @@ -345,6 +345,7 @@ pub async fn test_subscription_error() { stream.next().await, Some(Err(vec![ServerError { message: "TestError".to_string(), + source: None, locations: vec![Pos { line: 1, column: 25 @@ -390,6 +391,7 @@ pub async fn test_subscription_fieldresult() { cache_control: Default::default(), errors: vec![ServerError { message: "StreamErr".to_string(), + source: None, locations: vec![Pos { line: 1, column: 16 diff --git a/value/Cargo.toml b/value/Cargo.toml index ab403c89..e1da1d2e 100644 --- a/value/Cargo.toml +++ b/value/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "async-graphql-value" -version = "2.10.4" +version = "2.11.0" authors = ["sunli ", "Koxiaet"] -edition = "2018" +edition = "2021" description = "GraphQL value for async-graphql" license = "MIT/Apache-2.0" documentation = "https://docs.rs/async-graphql/" @@ -15,3 +15,4 @@ categories = ["network-programming", "asynchronous"] serde_json = "1.0.64" serde = { version = "1.0.125", features = ["derive"] } bytes = { version = "1.0.1", features = ["serde"] } +indexmap = { version = "1.7.0", features = ["serde"] } diff --git a/value/src/deserializer.rs b/value/src/deserializer.rs index e473cae1..e0cf01e7 100644 --- a/value/src/deserializer.rs +++ b/value/src/deserializer.rs @@ -1,6 +1,7 @@ -use std::collections::BTreeMap; use std::{fmt, vec}; +use indexmap::IndexMap; + use crate::{ConstValue, Name}; use serde::de::{ @@ -78,7 +79,7 @@ where } fn visit_object<'de, V>( - object: BTreeMap, + object: IndexMap, visitor: V, ) -> Result where @@ -365,13 +366,13 @@ impl<'de> SeqAccess<'de> for SeqDeserializer { } struct MapDeserializer { - iter: as IntoIterator>::IntoIter, + iter: as IntoIterator>::IntoIter, value: Option, } impl MapDeserializer { #[inline] - fn new(map: BTreeMap) -> Self { + fn new(map: IndexMap) -> Self { MapDeserializer { iter: map.into_iter(), value: None, diff --git a/value/src/lib.rs b/value/src/lib.rs index fd44e9e6..bf40e624 100644 --- a/value/src/lib.rs +++ b/value/src/lib.rs @@ -10,17 +10,17 @@ mod value_serde; mod variables; use std::borrow::{Borrow, Cow}; -use std::collections::BTreeMap; -use std::convert::{TryFrom, TryInto}; use std::fmt::{self, Display, Formatter, Write}; -use std::iter::FromIterator; use std::ops::Deref; use std::sync::Arc; use bytes::Bytes; +use indexmap::IndexMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub use deserializer::{from_value, DeserializerError}; +#[doc(hidden)] +pub use indexmap; pub use serde_json::Number; pub use serializer::{to_value, SerializerError}; @@ -139,7 +139,7 @@ pub enum ConstValue { /// A list of values. List(Vec), /// An object. This is a map of keys to values. - Object(BTreeMap), + Object(IndexMap), } impl PartialEq for ConstValue { @@ -254,8 +254,8 @@ impl> From> for ConstValue { } } -impl From> for ConstValue { - fn from(f: BTreeMap) -> Self { +impl From> for ConstValue { + fn from(f: IndexMap) -> Self { ConstValue::Object(f) } } @@ -364,7 +364,7 @@ pub enum Value { /// A list of values. List(Vec), /// An object. This is a map of keys to values. - Object(BTreeMap), + Object(IndexMap), } impl Value { diff --git a/value/src/macros.rs b/value/src/macros.rs index d39da636..4be7cc12 100644 --- a/value/src/macros.rs +++ b/value/src/macros.rs @@ -191,7 +191,7 @@ macro_rules! value_internal { ({ $($tt:tt)+ }) => { $crate::ConstValue::Object({ - let mut object = std::collections::BTreeMap::new(); + let mut object = $crate::indexmap::IndexMap::new(); $crate::value_internal!(@object object () ($($tt)+) ($($tt)+)); object }) @@ -227,7 +227,7 @@ macro_rules! value_expect_expr_comma { #[cfg(test)] mod tests { use crate::{ConstValue, Name}; - use std::collections::BTreeMap; + use indexmap::IndexMap; #[test] fn test_macro() { @@ -254,7 +254,7 @@ mod tests { ) ); assert_eq!(value!({"a": 10, "b": true}), { - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); map.insert(Name::new("a"), ConstValue::Number(10.into())); map.insert(Name::new("b"), ConstValue::Boolean(true)); ConstValue::Object(map) diff --git a/value/src/serializer.rs b/value/src/serializer.rs index 1db14dfe..ac198822 100644 --- a/value/src/serializer.rs +++ b/value/src/serializer.rs @@ -1,7 +1,7 @@ -use std::collections::BTreeMap; use std::error::Error; use std::fmt; +use indexmap::IndexMap; use serde::ser::{self, Impossible}; use serde::Serialize; @@ -180,7 +180,7 @@ impl ser::Serializer for Serializer { T: ser::Serialize, { value.serialize(self).map(|v| { - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); map.insert(Name::new(variant), v); ConstValue::Object(map) }) @@ -222,7 +222,7 @@ impl ser::Serializer for Serializer { #[inline] fn serialize_map(self, _len: Option) -> Result { Ok(SerializeMap { - map: BTreeMap::new(), + map: IndexMap::new(), key: None, }) } @@ -233,7 +233,7 @@ impl ser::Serializer for Serializer { _name: &'static str, _len: usize, ) -> Result { - Ok(SerializeStruct(BTreeMap::new())) + Ok(SerializeStruct(IndexMap::new())) } #[inline] @@ -244,7 +244,7 @@ impl ser::Serializer for Serializer { variant: &'static str, _len: usize, ) -> Result { - Ok(SerializeStructVariant(Name::new(variant), BTreeMap::new())) + Ok(SerializeStructVariant(Name::new(variant), IndexMap::new())) } #[inline] @@ -337,14 +337,14 @@ impl ser::SerializeTupleVariant for SerializeTupleVariant { #[inline] fn end(self) -> Result { - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); map.insert(self.0, ConstValue::List(self.1)); Ok(ConstValue::Object(map)) } } struct SerializeMap { - map: BTreeMap, + map: IndexMap, key: Option, } @@ -378,7 +378,7 @@ impl ser::SerializeMap for SerializeMap { } } -struct SerializeStruct(BTreeMap); +struct SerializeStruct(IndexMap); impl ser::SerializeStruct for SerializeStruct { type Ok = ConstValue; @@ -405,7 +405,7 @@ impl ser::SerializeStruct for SerializeStruct { } } -struct SerializeStructVariant(Name, BTreeMap); +struct SerializeStructVariant(Name, IndexMap); impl ser::SerializeStructVariant for SerializeStructVariant { type Ok = ConstValue; @@ -428,7 +428,7 @@ impl ser::SerializeStructVariant for SerializeStructVariant { #[inline] fn end(self) -> Result { - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); map.insert(self.0, ConstValue::Object(self.1)); Ok(ConstValue::Object(map)) } diff --git a/value/src/value_serde.rs b/value/src/value_serde.rs index 7e2ffb7a..3d6aa76a 100644 --- a/value/src/value_serde.rs +++ b/value/src/value_serde.rs @@ -1,6 +1,6 @@ -use std::collections::BTreeMap; use std::fmt::{self, Formatter}; +use indexmap::IndexMap; use serde::de::{Error as DeError, MapAccess, SeqAccess, Visitor}; use serde::ser::Error as SerError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -137,7 +137,7 @@ impl<'de> Deserialize<'de> for ConstValue { where A: MapAccess<'de>, { - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); while let Some((name, value)) = visitor.next_entry()? { map.insert(name, value); } @@ -280,7 +280,7 @@ impl<'de> Deserialize<'de> for Value { where A: MapAccess<'de>, { - let mut map = BTreeMap::new(); + let mut map = IndexMap::new(); while let Some((name, value)) = visitor.next_entry()? { map.insert(name, value); } diff --git a/value/src/variables.rs b/value/src/variables.rs index 7e9d7d1e..8599085c 100644 --- a/value/src/variables.rs +++ b/value/src/variables.rs @@ -36,7 +36,7 @@ impl Variables { #[must_use] pub fn from_value(value: ConstValue) -> Self { match value { - ConstValue::Object(obj) => Self(obj), + ConstValue::Object(obj) => Self(obj.into_iter().collect()), _ => Self::default(), } } @@ -55,7 +55,7 @@ impl Variables { /// Get the variables as a GraphQL value. #[must_use] pub fn into_value(self) -> ConstValue { - ConstValue::Object(self.0) + ConstValue::Object(self.0.into_iter().collect()) } }