Merge branch 'master' into lets-avoid-dupes

This commit is contained in:
Sunli 2021-12-03 09:08:58 +08:00 committed by GitHub
commit bd578d59f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 415 additions and 69 deletions

View File

@ -4,7 +4,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.0.7] 2021-11-23
## [3.0.11] 2021-12-02
- Fix panic on f32-64::INFINITE/f32-64::NEG_INFINITE/f32-64::NAN output. [#735](https://github.com/async-graphql/async-graphql/issues/735)
## [3.0.10] 2021-11-30
- Fix the custom validator cannot work on `Option<Vec<T>>`.
## [3.0.9] 2021-11-30
- Fix the validator cannot work on `Option<Vec<T>>`.
## [3.0.8] 2021-11-30
- `#[graphql(validator(list))]` no longer applies to `max_items` and `min_items`.
- Implement `InputValue`/`OutputValue` for `serde_json::Value`.
- Add support for `SmolStr` via a feature. [#730](https://github.com/async-graphql/async-graphql/pull/730)
## [3.0.7] 2021-11-23
- Fix error extensions cause stack overflow. [#719](https://github.com/async-graphql/async-graphql/issues/719)

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql"
version = "3.0.7"
version = "3.0.11"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "A GraphQL server library implemented in Rust"
@ -61,6 +61,7 @@ opentelemetry = { version = "0.16.0", optional = true, default-features = false,
uuid = { version = "0.8.2", optional = true, features = ["v4", "serde"] }
rust_decimal = { version = "1.14.3", optional = true }
url = { version = "2.2.1", optional = true }
smol_str = { version = "0.1.21", optional = true }
# Non-feature optional dependencies
blocking = { version = "1.0.2", optional = true }

View File

@ -78,6 +78,7 @@ This crate offers the following features, all of which are not activated by defa
- `secrecy`: Integrate with the [`secrecy` crate](https://crates.io/crates/secrecy).
- `decimal`: Integrate with the [`rust_decimal` crate](https://crates.io/crates/rust_decimal).
- `cbor`: Support for [serde_cbor](https://crates.io/crates/serde_cbor).
- `smol_str`: Integrate with the [`smol_str` crate](https://crates.io/crates/smol_str).
## Apollo Studio

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-derive"
version = "3.0.8"
version = "3.0.10"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2021"
description = "Macros for async-graphql"

View File

@ -71,115 +71,147 @@ impl Validators {
ty: TokenStream,
map_err: Option<TokenStream>,
) -> Result<TokenStream> {
let mut list_validators = Vec::new();
let mut elem_validators = Vec::new();
let mut codes = Vec::new();
if let Some(n) = &self.multiple_of {
codes.push(quote! {
#crate_name::validators::multiple_of(__raw_value, #n)
});
}
if let Some(n) = &self.maximum {
codes.push(quote! {
#crate_name::validators::maximum(__raw_value, #n)
});
}
if let Some(n) = &self.minimum {
codes.push(quote! {
#crate_name::validators::minimum(__raw_value, #n)
});
}
if let Some(n) = &self.max_length {
codes.push(quote! {
#crate_name::validators::max_length(__raw_value, #n)
});
}
if let Some(n) = &self.min_length {
codes.push(quote! {
#crate_name::validators::min_length(__raw_value, #n)
});
}
if let Some(n) = &self.max_items {
codes.push(quote! {
list_validators.push(quote! {
#crate_name::validators::max_items(__raw_value, #n)
});
}
if let Some(n) = &self.min_items {
codes.push(quote! {
list_validators.push(quote! {
#crate_name::validators::min_items(__raw_value, #n)
});
}
if let Some(n) = &self.multiple_of {
elem_validators.push(quote! {
#crate_name::validators::multiple_of(__raw_value, #n)
});
}
if let Some(n) = &self.maximum {
elem_validators.push(quote! {
#crate_name::validators::maximum(__raw_value, #n)
});
}
if let Some(n) = &self.minimum {
elem_validators.push(quote! {
#crate_name::validators::minimum(__raw_value, #n)
});
}
if let Some(n) = &self.max_length {
elem_validators.push(quote! {
#crate_name::validators::max_length(__raw_value, #n)
});
}
if let Some(n) = &self.min_length {
elem_validators.push(quote! {
#crate_name::validators::min_length(__raw_value, #n)
});
}
if let Some(n) = &self.chars_max_length {
codes.push(quote! {
elem_validators.push(quote! {
#crate_name::validators::chars_max_length(__raw_value, #n)
});
}
if let Some(n) = &self.chars_min_length {
codes.push(quote! {
elem_validators.push(quote! {
#crate_name::validators::chars_min_length(__raw_value, #n)
});
}
if self.email {
codes.push(quote! {
elem_validators.push(quote! {
#crate_name::validators::email(__raw_value)
});
}
if self.url {
codes.push(quote! {
elem_validators.push(quote! {
#crate_name::validators::url(__raw_value)
});
}
if self.ip {
codes.push(quote! {
elem_validators.push(quote! {
#crate_name::validators::ip(__raw_value)
});
}
if let Some(re) = &self.regex {
codes.push(quote! {
elem_validators.push(quote! {
#crate_name::validators::regex(__raw_value, #re)
});
}
if !list_validators.is_empty() {
codes.push(quote! {
if let ::std::option::Option::Some(__raw_value) = #crate_name::InputType::as_raw_value(#value) {
#(#list_validators #map_err ?;)*
}
});
}
if !elem_validators.is_empty() {
if self.list {
codes.push(quote! {
if let ::std::option::Option::Some(value) = #crate_name::InputType::as_raw_value(#value) {
for __item in value {
if let ::std::option::Option::Some(__raw_value) = #crate_name::InputType::as_raw_value(__item) {
#(#elem_validators #map_err ?;)*
}
}
}
});
} else {
codes.push(quote! {
if let ::std::option::Option::Some(__raw_value) = #crate_name::InputType::as_raw_value(#value) {
#(#elem_validators #map_err ?;)*
}
});
}
}
for s in &self.custom {
let __create_custom_validator: Expr =
syn::parse_str(s).map_err(|err| Error::new(s.span(), err.to_string()))?;
codes.push(quote! {
#crate_name::CustomValidator::check(&(#__create_custom_validator), __raw_value)
.map_err(|err_msg| #crate_name::InputValueError::<#ty>::custom(err_msg))
});
if self.list {
codes.push(quote! {
let __custom_validator = #__create_custom_validator;
if let ::std::option::Option::Some(value) = #crate_name::InputType::as_raw_value(#value) {
for __item in value {
if let ::std::option::Option::Some(__raw_value) = #crate_name::InputType::as_raw_value(__item) {
#crate_name::CustomValidator::check(&__custom_validator, __raw_value)
.map_err(|err_msg| #crate_name::InputValueError::<#ty>::custom(err_msg)) #map_err ?;
}
}
}
});
} else {
codes.push(quote! {
let __custom_validator = #__create_custom_validator;
if let ::std::option::Option::Some(__raw_value) = #crate_name::InputType::as_raw_value(#value) {
#crate_name::CustomValidator::check(&__custom_validator, __raw_value)
.map_err(|err_msg| #crate_name::InputValueError::<#ty>::custom(err_msg)) #map_err ?;
}
});
}
}
if codes.is_empty() {
return Ok(quote!());
}
let codes = codes.into_iter().map(|s| quote!(#s #map_err ?));
if self.list {
Ok(quote! {
for __item in #value {
if let ::std::option::Option::Some(__raw_value) = #crate_name::InputType::as_raw_value(__item) {
#(#codes;)*
}
}
})
} else {
Ok(quote! {
if let ::std::option::Option::Some(__raw_value) = #crate_name::InputType::as_raw_value(#value) {
#(#codes;)*
}
})
}
Ok(quote!(#(#codes)*))
}
}

View File

@ -58,9 +58,8 @@ impl MyValidator {
}
}
#[async_trait::async_trait]
impl CustomValidator<i32> for MyValidator {
async fn check(&self, value: &i32) -> Result<(), String> {
fn check(&self, value: &i32) -> Result<(), String> {
if *value == self.expect {
Ok(())
} else {

View File

@ -1,4 +1,4 @@
# Poem
# Actix-web
## Request example

View File

@ -58,9 +58,8 @@ impl MyValidator {
}
}
#[async_trait::async_trait]
impl CustomValidator<i32> for MyValidator {
async fn check(&self, value: &i32) -> Result<(), String> {
fn check(&self, value: &i32) -> Result<(), String> {
if *value == self.expect {
Ok(())
} else {

View File

@ -73,6 +73,7 @@
//! - `dataloader`: Support [DataLoader](dataloader/struct.DataLoader.html).
//! - `decimal`: Integrate with the [`rust_decimal` crate](https://crates.io/crates/rust_decimal).
//! - `cbor`: Support for [serde_cbor](https://crates.io/crates/serde_cbor).
//! - `smol_str`: Integrate with the [`smol_str` crate](https://crates.io/crates/smol_str).
//!
//! ## Integrations
//!

View File

@ -18,7 +18,10 @@ impl ScalarType for f32 {
}
fn to_value(&self) -> Value {
Value::Number(Number::from_f64(*self as f64).unwrap())
match Number::from_f64(*self as f64) {
Some(n) => Value::Number(n),
None => Value::Null,
}
}
}
@ -40,6 +43,9 @@ impl ScalarType for f64 {
}
fn to_value(&self) -> Value {
Value::Number(Number::from_f64(*self as f64).unwrap())
match Number::from_f64(*self as f64) {
Some(n) => Value::Number(n),
None => Value::Null,
}
}
}

View File

@ -26,6 +26,8 @@ mod duration;
mod naive_time;
#[cfg(feature = "secrecy")]
mod secrecy;
#[cfg(feature = "smol_str")]
mod smol_str;
#[cfg(feature = "url")]
mod url;
#[cfg(feature = "uuid")]

25
src/types/external/smol_str.rs vendored Normal file
View File

@ -0,0 +1,25 @@
use smol_str::SmolStr;
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
#[Scalar(
internal,
name = "SmolStr",
specified_by_url = "https://docs.rs/smol_str/latest/smol_str/struct.SmolStr.html"
)]
impl ScalarType for SmolStr {
fn parse(value: Value) -> InputValueResult<Self> {
match value {
Value::String(s) => Ok(SmolStr::new(s)),
_ => Err(InputValueError::expected_type(value)),
}
}
fn is_valid(value: &Value) -> bool {
matches!(value, Value::String(_))
}
fn to_value(&self) -> Value {
Value::String(self.to_string())
}
}

View File

@ -93,6 +93,61 @@ impl<T: Serialize + Send + Sync> OutputType for Json<T> {
}
}
impl InputType for serde_json::Value {
type RawValueType = serde_json::Value;
fn type_name() -> Cow<'static, str> {
Cow::Borrowed("JSON")
}
fn create_type_info(registry: &mut Registry) -> String {
registry.create_output_type::<serde_json::Value, _>(|_| MetaType::Scalar {
name: <Self as InputType>::type_name().to_string(),
description: None,
is_valid: |_| true,
visible: None,
specified_by_url: None,
})
}
fn parse(value: Option<Value>) -> InputValueResult<Self> {
Ok(from_value(value.unwrap_or_default())?)
}
fn to_value(&self) -> Value {
to_value(&self).unwrap_or_default()
}
fn as_raw_value(&self) -> Option<&Self::RawValueType> {
Some(&self)
}
}
#[async_trait::async_trait]
impl OutputType for serde_json::Value {
fn type_name() -> Cow<'static, str> {
Cow::Borrowed("JSON")
}
fn create_type_info(registry: &mut Registry) -> String {
registry.create_output_type::<serde_json::Value, _>(|_| MetaType::Scalar {
name: <Self as OutputType>::type_name().to_string(),
description: None,
is_valid: |_| true,
visible: None,
specified_by_url: None,
})
}
async fn resolve(
&self,
_ctx: &ContextSelectionSet<'_>,
_field: &Positioned<Field>,
) -> ServerResult<Value> {
Ok(to_value(&self).ok().unwrap_or_default())
}
}
#[cfg(test)]
mod test {
use crate::*;

View File

@ -45,3 +45,26 @@ pub async fn test_scalar_macro() {
})
);
}
#[tokio::test]
pub async fn test_float_inf() {
struct Query;
#[Object]
impl Query {
async fn value(&self) -> f32 {
f32::INFINITY
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema
.execute("{ value }")
.await
.into_result()
.unwrap()
.data,
value!({ "value": null })
);
}

View File

@ -341,6 +341,20 @@ pub async fn test_custom_validator() {
async fn input(&self, input: MyInput) -> i32 {
input.n
}
async fn value2(
&self,
#[graphql(validator(list, custom = "MyValidator::new(100)"))] values: Vec<i32>,
) -> i32 {
values.into_iter().sum()
}
async fn value3(
&self,
#[graphql(validator(list, custom = "MyValidator::new(100)"))] values: Option<Vec<i32>>,
) -> i32 {
values.into_iter().flatten().sum()
}
}
struct Subscription;
@ -444,6 +458,54 @@ pub async fn test_custom_validator() {
extensions: None
}]
);
assert_eq!(
schema
.execute("{ value2(values: [77, 88] ) }")
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "[Int!]": expect 100, actual 77"#.to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 18
}],
path: vec![PathSegment::Field("value2".to_string())],
extensions: None
}]
);
assert_eq!(
schema
.execute("{ value3(values: [77, 88] ) }")
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "[Int!]": expect 100, actual 77"#.to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 18
}],
path: vec![PathSegment::Field("value3".to_string())],
extensions: None
}]
);
assert_eq!(
schema
.execute("{ value3(values: null ) }")
.await
.into_result()
.unwrap()
.data,
value!({
"value3": 0
})
);
}
#[tokio::test]
@ -605,3 +667,125 @@ pub async fn test_validate_wrapper_types() {
}]
);
}
#[tokio::test]
pub async fn test_list_both_max_items_and_max_length() {
struct Query;
#[Object]
impl Query {
async fn value(
&self,
#[graphql(validator(list, max_length = 3, max_items = 2))] values: Vec<String>,
) -> String {
values.into_iter().collect()
}
async fn value2(
&self,
#[graphql(validator(list, max_length = 3, max_items = 2))] values: Option<Vec<String>>,
) -> String {
values.into_iter().flatten().collect()
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema
.execute(r#"{ value(values: ["a", "b", "cdef"])}"#)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "[String!]": the value length is 3, must be less than or equal to 2"#.to_string(),
source: None,
locations: vec![Pos { column: 17, line: 1}],
path: vec![PathSegment::Field("value".to_string())],
extensions: None
}]
);
assert_eq!(
schema
.execute(r#"{ value(values: ["a", "cdef"])}"#)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "String": the string length is 4, must be less than or equal to 3"#.to_string(),
source: None,
locations: vec![Pos { column: 17, line: 1}],
path: vec![PathSegment::Field("value".to_string())],
extensions: None
}]
);
assert_eq!(
schema
.execute(r#"{ value(values: ["a", "b"])}"#)
.await
.into_result()
.unwrap()
.data,
value!({
"value": "ab"
})
);
assert_eq!(
schema
.execute(r#"{ value2(values: ["a", "b", "cdef"])}"#)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "[String!]": the value length is 3, must be less than or equal to 2"#.to_string(),
source: None,
locations: vec![Pos { column: 18, line: 1}],
path: vec![PathSegment::Field("value2".to_string())],
extensions: None
}]
);
assert_eq!(
schema
.execute(r#"{ value2(values: ["a", "cdef"])}"#)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "String": the string length is 4, must be less than or equal to 3"#.to_string(),
source: None,
locations: vec![Pos { column: 18, line: 1}],
path: vec![PathSegment::Field("value2".to_string())],
extensions: None
}]
);
assert_eq!(
schema
.execute(r#"{ value2(values: ["a", "b", "cdef"])}"#)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: r#"Failed to parse "[String!]": the value length is 3, must be less than or equal to 2"#.to_string(),
source: None,
locations: vec![Pos { column: 18, line: 1}],
path: vec![PathSegment::Field("value2".to_string())],
extensions: None
}]
);
assert_eq!(
schema
.execute(r#"{ value2(values: null)}"#)
.await
.into_result()
.unwrap()
.data,
value!({
"value2": ""
})
);
}