feat: add basic coercion for SimpleObject derived arg
This commit is contained in:
parent
91f50c92a3
commit
2941b7283d
|
@ -23,3 +23,5 @@ Inflector = "0.11.4"
|
||||||
proc-macro-crate = "1.0.0"
|
proc-macro-crate = "1.0.0"
|
||||||
darling = "0.12.2"
|
darling = "0.12.2"
|
||||||
thiserror = "1.0.24"
|
thiserror = "1.0.24"
|
||||||
|
regex = "1.5"
|
||||||
|
once_cell = "1.8.0"
|
||||||
|
|
|
@ -255,6 +255,8 @@ pub struct ObjectField {
|
||||||
pub struct DerivedField {
|
pub struct DerivedField {
|
||||||
pub name: Option<Ident>,
|
pub name: Option<Ident>,
|
||||||
pub into: Option<String>,
|
pub into: Option<String>,
|
||||||
|
#[darling(default)]
|
||||||
|
pub owned: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromDeriveInput)]
|
#[derive(FromDeriveInput)]
|
||||||
|
|
|
@ -7,12 +7,27 @@ use syn::{Error, Ident, Type};
|
||||||
|
|
||||||
use crate::args::{self, RenameRuleExt, RenameTarget, SimpleObjectField};
|
use crate::args::{self, RenameRuleExt, RenameTarget, SimpleObjectField};
|
||||||
use crate::utils::{
|
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 {
|
struct DerivedFieldMetadata {
|
||||||
ident: Ident,
|
ident: Ident,
|
||||||
into: Type,
|
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> {
|
struct SimpleObjectFieldGenerator<'a> {
|
||||||
|
@ -60,10 +75,30 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|
||||||
for derived in &field.derived {
|
for derived in &field.derived {
|
||||||
if derived.name.is_some() && derived.into.is_some() {
|
if derived.name.is_some() && derived.into.is_some() {
|
||||||
let name = derived.name.clone().unwrap();
|
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(),
|
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 {
|
processed_fields.push(SimpleObjectFieldGenerator {
|
||||||
field,
|
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()),
|
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 {
|
let ident = if let Some(derived) = derived {
|
||||||
&derived.ident
|
&derived.ident
|
||||||
} else {
|
} else {
|
||||||
|
@ -109,12 +143,24 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|
||||||
None => quote! { ::std::option::Option::None },
|
None => quote! { ::std::option::Option::None },
|
||||||
};
|
};
|
||||||
let vis = &field.vis;
|
let vis = &field.vis;
|
||||||
|
let derived_coercion = if let Some(derived) = derived {
|
||||||
|
Some(&derived.coercion)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let ty = if let Some(derived) = derived {
|
let ty = if let Some(derived) = derived {
|
||||||
&derived.into
|
&derived.into
|
||||||
} else {
|
} else {
|
||||||
&field.ty
|
&field.ty
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let owned = if let Some(derived) = derived {
|
||||||
|
derived.owned.unwrap_or(field.owned)
|
||||||
|
} else {
|
||||||
|
field.owned
|
||||||
|
};
|
||||||
|
|
||||||
let cache_control = {
|
let cache_control = {
|
||||||
let public = field.cache_control.is_public();
|
let public = field.cache_control.is_public();
|
||||||
let max_age = field.cache_control.max_age;
|
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))?; },
|
|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! {
|
true => quote! {
|
||||||
&self.#base_ident
|
&self.#base_ident
|
||||||
},
|
},
|
||||||
|
@ -161,16 +207,45 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
block = match is_derived {
|
// If the field is derived, it means it has a derived_coercion, depending on the coercion
|
||||||
false => quote! {
|
// we have to implement several way to coerce it.
|
||||||
#block
|
block = match derived_coercion {
|
||||||
},
|
Some(DerivedIntoCoercion::Unknown) => quote! {
|
||||||
true => quote! {
|
|
||||||
::std::convert::Into::into(#block)
|
::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 },
|
true => quote! { &#ty },
|
||||||
false => quote! { #ty },
|
false => quote! { #ty },
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use darling::FromMeta;
|
use darling::FromMeta;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use proc_macro2::{Span, TokenStream, TokenTree};
|
use proc_macro2::{Span, TokenStream, TokenTree};
|
||||||
use proc_macro_crate::{crate_name, FoundCrate};
|
use proc_macro_crate::{crate_name, FoundCrate};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
use regex::Regex;
|
||||||
use syn::visit::Visit;
|
use syn::visit::Visit;
|
||||||
use syn::{
|
use syn::{
|
||||||
Attribute, Error, Expr, ExprPath, FnArg, Ident, ImplItemMethod, Lit, LitStr, Meta, NestedMeta,
|
Attribute, Error, Expr, ExprPath, FnArg, Ident, ImplItemMethod, Lit, LitStr, Meta, NestedMeta,
|
||||||
|
@ -533,3 +535,61 @@ pub fn extract_input_args(
|
||||||
|
|
||||||
Ok(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
|
||||||
|
}
|
||||||
|
|
|
@ -59,3 +59,47 @@ type Query {
|
||||||
duration_rfc3339(arg: String): DateRFC3339!
|
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>>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -467,6 +467,8 @@ pub use async_graphql_derive::Object;
|
||||||
/// |--------------|------------------------------------------|------------ |----------|
|
/// |--------------|------------------------------------------|------------ |----------|
|
||||||
/// | name | Generated derived field name | string | N |
|
/// | name | Generated derived field name | string | N |
|
||||||
/// | into | Type to derived an into | string | Y |
|
/// | 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
|
/// # Examples
|
||||||
|
|
|
@ -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]
|
#[tokio::test]
|
||||||
pub async fn test_derived_field_complex_object() {
|
pub async fn test_derived_field_complex_object() {
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user