feat: add basic coercion for SimpleObject derived arg

This commit is contained in:
Miaxos 2021-10-27 13:37:13 +00:00
parent 16f216c18f
commit fa34a8ae68
7 changed files with 259 additions and 12 deletions

View File

@ -23,3 +23,5 @@ Inflector = "0.11.4"
proc-macro-crate = "1.0.0"
darling = "0.12.2"
thiserror = "1.0.24"
regex = "1.5"
once_cell = "1.8.0"

View File

@ -255,6 +255,8 @@ pub struct ObjectField {
pub struct DerivedField {
pub name: Option<Ident>,
pub into: Option<String>,
#[darling(default)]
pub owned: Option<bool>,
}
#[derive(FromDeriveInput)]

View File

@ -7,12 +7,27 @@ use syn::{Error, Ident, Type};
use crate::args::{self, RenameRuleExt, RenameTarget, SimpleObjectField};
use crate::utils::{
gen_deprecation, generate_guards, get_crate_name, get_rustdoc, visible_fn, GeneratorResult,
derive_type_coercion, gen_deprecation, generate_guards, get_crate_name, get_rustdoc,
visible_fn, DerivedIntoCoercion, GeneratorResult,
};
#[derive(Debug)]
struct DerivedFieldMetadata {
ident: Ident,
into: Type,
owned: Option<bool>,
// The into argument for a derive field won't be able to transform everythings:
// Without the specialization from Rust, we can't implement things like From between Vec<T> ->
// Vec<U> or Option<T> -> Option<U>.
// But there are cases which you want to have this coercion derived, so to have it working
// until the specialization feature comes, we manually check coercion for the most usual cases
// which are:
//
// - Vec<T> -> Vec<U>
// - Option<T> -> Option<U>
// - Option<Vec<T>> -> Option<Vec<U>>
// - Vec<Option<T>> -> Vec<Option<U>>
coercion: DerivedIntoCoercion,
}
struct SimpleObjectFieldGenerator<'a> {
@ -60,10 +75,30 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
for derived in &field.derived {
if derived.name.is_some() && derived.into.is_some() {
let name = derived.name.clone().unwrap();
let into = Type::Verbatim(
let into = match syn::parse2::<Type>(
proc_macro2::TokenStream::from_str(&derived.into.clone().unwrap()).unwrap(),
);
let derived = DerivedFieldMetadata { ident: name, into };
) {
Ok(e) => e,
_ => {
return Err(Error::new_spanned(
&name,
"derived into must be a valid type.",
)
.into());
}
};
let ty = &field.ty;
let derived_type = quote! { #into }.to_string();
let base_type = quote! { #ty }.to_string();
let derived = DerivedFieldMetadata {
ident: name,
into,
owned: derived.owned,
coercion: derive_type_coercion(&base_type, &derived_type),
};
processed_fields.push(SimpleObjectFieldGenerator {
field,
@ -83,7 +118,6 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
None => return Err(Error::new_spanned(&ident, "All fields must be named.").into()),
};
let is_derived = derived.is_some();
let ident = if let Some(derived) = derived {
&derived.ident
} else {
@ -109,12 +143,24 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
None => quote! { ::std::option::Option::None },
};
let vis = &field.vis;
let derived_coercion = if let Some(derived) = derived {
Some(&derived.coercion)
} else {
None
};
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();
let max_age = field.cache_control.max_age;
@ -152,7 +198,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|guard| quote! { #guard.check(ctx).await.map_err(|err| err.into_server_error(ctx.item.pos))?; },
);
let mut block = match !field.owned {
let mut block = match !owned {
true => quote! {
&self.#base_ident
},
@ -161,16 +207,45 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
},
};
block = match is_derived {
false => quote! {
#block
},
true => quote! {
// If the field is derived, it means it has a derived_coercion, depending on the coercion
// we have to implement several way to coerce it.
block = match derived_coercion {
Some(DerivedIntoCoercion::Unknown) => quote! {
::std::convert::Into::into(#block)
},
Some(DerivedIntoCoercion::OptionToOption) => quote! {
::std::option::Option::and_then(#block, |value| ::std::option::Option::Some(::std::convert::Into::into(value)))
},
Some(DerivedIntoCoercion::VecToVec) => quote! {
{
let mut result = vec![];
::std::iter::Extend::extend(&mut result, ::std::iter::Iterator::map(::std::iter::IntoIterator::into_iter(#block), |x| ::std::convert::Into::into(::std::clone::Clone::clone(&x))));
result
}
},
Some(DerivedIntoCoercion::OptionVecToOptionVec) => quote! {
::std::option::Option::and_then(#block, |value| ::std::option::Option::Some({
let mut result = vec![];
::std::iter::Extend::extend(&mut result, ::std::iter::Iterator::map(::std::iter::IntoIterator::into_iter(value), |x| ::std::convert::Into::into(::std::clone::Clone::clone(&x))));
result
}))
},
Some(DerivedIntoCoercion::VecOptionToVecOption) => quote! {
{
let mut result = vec![];
::std::iter::Extend::extend(&mut result, ::std::iter::Iterator::map(::std::iter::IntoIterator::into_iter(#block), |x| ::std::option::Option::and_then(x, |value| ::std::option::Option::Some(
::std::convert::Into::into(value)
))));
result
}
},
// If the field is not derived, we follow the normal process.
_ => quote! {
#block
},
};
let ty = match !field.owned {
let ty = match !owned {
true => quote! { &#ty },
false => quote! { #ty },
};

View File

@ -1,9 +1,11 @@
use std::collections::HashSet;
use darling::FromMeta;
use once_cell::sync::Lazy;
use proc_macro2::{Span, TokenStream, TokenTree};
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use regex::Regex;
use syn::visit::Visit;
use syn::{
Attribute, Error, Expr, ExprPath, FnArg, Ident, ImplItemMethod, Lit, LitStr, Meta, NestedMeta,
@ -533,3 +535,61 @@ pub fn extract_input_args(
Ok(args)
}
#[derive(Debug)]
pub enum DerivedIntoCoercion {
Unknown = 1,
VecToVec = 2,
OptionToOption = 3,
OptionVecToOptionVec = 4,
VecOptionToVecOption = 5,
}
static CHECK_OPTION: Lazy<Regex> = Lazy::new(|| Regex::new("^Option <(.*?) >$").unwrap());
static CHECK_VEC: Lazy<Regex> = Lazy::new(|| Regex::new("^Vec <(.*?) >$").unwrap());
static CHECK_VEC_OPTION: Lazy<Regex> =
Lazy::new(|| Regex::new("^Vec < Option <(.*?)> >$").unwrap());
static CHECK_OPTION_VEC: Lazy<Regex> =
Lazy::new(|| Regex::new("^Option < Vec <(.*?)> >$").unwrap());
/// The into argument for a derive field won't be able to transform everythings:
/// Without the specialization from Rust, we can't implement things like From between Vec<T> ->
/// Vec<U> or Option<T> -> Option<U>.
/// But there are cases which you want to have this coercion derived, so to have it working
/// until the specialization feature comes, we manually check coercion for the most usual cases
/// which are:
///
/// - Vec<T> -> Vec<U>
/// - Option<T> -> Option<U>
/// - Option<Vec<T>> -> Option<Vec<U>>
/// - Vec<Option<T>> -> Vec<Option<U>>
pub fn derive_type_coercion<S1: AsRef<str>, S2: AsRef<str>>(
base_type: S1,
target_type: S2,
) -> DerivedIntoCoercion {
if CHECK_OPTION_VEC.find(base_type.as_ref()).is_some()
&& CHECK_OPTION_VEC.find(target_type.as_ref()).is_some()
{
return DerivedIntoCoercion::OptionVecToOptionVec;
}
if CHECK_VEC_OPTION.find(base_type.as_ref()).is_some()
&& CHECK_VEC_OPTION.find(target_type.as_ref()).is_some()
{
return DerivedIntoCoercion::VecOptionToVecOption;
}
if CHECK_VEC.find(base_type.as_ref()).is_some()
&& CHECK_VEC.find(target_type.as_ref()).is_some()
{
return DerivedIntoCoercion::VecToVec;
}
if CHECK_OPTION.find(base_type.as_ref()).is_some()
&& CHECK_OPTION.find(target_type.as_ref()).is_some()
{
return DerivedIntoCoercion::OptionToOption;
}
DerivedIntoCoercion::Unknown
}

View File

@ -59,3 +59,47 @@ type Query {
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<Vec<U>> for Vec<T> {
...
}
```
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<U> for T` you should be able to derived a `From<Vec<U>> for Vec<T>` and a `From<Option<U>> for Option<T>`, so a coercion mecanism has been included so you'll be able to use the derived macro argument with `Vec` and `Option`.
This coercion mecanism impose these derived to be `owned`.
### Example
```rust
#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived(String);
#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived2(String);
scalar!(ValueDerived);
scalar!(ValueDerived2);
impl From<ValueDerived> for ValueDerived2 {
fn from(value: ValueDerived) -> Self {
ValueDerived2(value.0)
}
}
#[derive(SimpleObject)]
struct TestObj {
#[graphql(derived(owned, name = "value2", into = "Option<ValueDerived2>"))]
pub value1: Option<ValueDerived>,
#[graphql(derived(owned, name = "value_vec_2", into = "Vec<ValueDerived2>"))]
pub value_vec_1: Vec<ValueDerived>,
#[graphql(derived(owned, name = "value_opt_vec_2", into = "Option<Vec<ValueDerived2>>"))]
pub value_opt_vec_1: Option<Vec<ValueDerived>>,
#[graphql(derived(owned, name = "value_vec_opt_2", into = "Vec<Option<ValueDerived2>>"))]
pub value_vec_opt_1: Vec<Option<ValueDerived>>,
}
```

View File

@ -467,6 +467,8 @@ pub use async_graphql_derive::Object;
/// |--------------|------------------------------------------|------------ |----------|
/// | name | Generated derived field name | string | N |
/// | into | Type to derived an into | string | Y |
/// | into | Type to derived an into | string | Y |
/// | owned | Field resolver return a ownedship value | bool | Y |
///
///
/// # Examples

View File

@ -102,6 +102,68 @@ pub async fn test_derived_field_simple_object() {
);
}
#[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<ValueDerived> for ValueDerived2 {
fn from(value: ValueDerived) -> Self {
ValueDerived2(value.0)
}
}
#[derive(SimpleObject)]
struct TestObj {
#[graphql(derived(owned, name = "value2", into = "Option<ValueDerived2>"))]
pub value1: Option<ValueDerived>,
#[graphql(derived(owned, name = "value_vec_2", into = "Vec<ValueDerived2>"))]
pub value_vec_1: Vec<ValueDerived>,
#[graphql(derived(owned, name = "value_opt_vec_2", into = "Option<Vec<ValueDerived2>>"))]
pub value_opt_vec_1: Option<Vec<ValueDerived>>,
#[graphql(derived(owned, name = "value_vec_opt_2", into = "Vec<Option<ValueDerived2>>"))]
pub value_vec_opt_1: Vec<Option<ValueDerived>>,
}
#[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};