feat: add derived field for complex object

This commit is contained in:
Miaxos 2021-10-22 13:06:54 +00:00
parent 120bd43143
commit e1c577de44
6 changed files with 218 additions and 5 deletions

View File

@ -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<String> },
@ -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<Ident>,
@ -243,6 +243,16 @@ pub struct ObjectField {
pub guard: Option<Meta>,
pub visible: Option<Visible>,
pub complexity: Option<ComplexityType>,
#[darling(default, multiple)]
pub derived: Vec<DerivedField>,
}
#[derive(FromMeta, Default, Clone)]
#[darling(default)]
/// Derivied fields arguments: are used to generate derivied fields.
pub struct DerivedField {
pub name: Option<Ident>,
pub into: Option<String>,
}
#[derive(FromDeriveInput)]

View File

@ -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,86 @@ 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 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::<ReturnType>(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::<FnArg>(quote! { ctx: &Context<'_> })
.expect("invalid arg type");
new_impl.sig.inputs.insert(1, arg_ctx);
}
let other_atts: Punctuated<Ident, Token![,]> = 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",
)
.into())),
},
FnArg::Receiver(_) => None,
})
.collect::<Result<Vec<Ident>, Error>>()?
.into_iter(),
);
let new_block = quote!({
{
::std::result::Result::Ok(#self_ty::#base_function_name(&self, #other_atts).await?.into())
}
});
new_impl.block = syn::parse2::<Block>(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 =

View File

@ -8,6 +8,7 @@
- [Context](context.md)
- [Error handling](error_handling.md)
- [Merging Objects / Subscriptions](merging_objects.md)
- [Derived fields](derived_fields.md)
- [Enum](define_enum.md)
- [Interface](define_interface.md)
- [Union](define_union.md)

View File

@ -0,0 +1,61 @@
# Derived fields
When you are working on a GraphQL project, you usually have to explain and share how your scalars should
be interpreted by your consumers. Sometimes, you event want to have the same data and the same logic exposing
the data in another type.
Within `async-graphql` you can create derivated fields for objects to generate derivated fields.
Consider you want to create a `Date` scalar, to represent an event of time.
How will you represent and format this date? You could create a scalar `Date` where you specified it's the RFCXXX
implemented to format it.
With derivated fields there is a simple way to support multiple representation of a `Date` easily:
```rust
struct DateRFC3339(chrono::DateTime);
struct DateRFC2822(chrono::DateTime);
#[Scalar]
impl ScalarType for DateRFC3339 {
fn parse(value: Value) -> 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<DateRFC2822> 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!
}
```

View File

@ -279,6 +279,7 @@ pub type FieldResult<T> = Result<T>;
/// | 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,13 @@ pub type FieldResult<T> = Result<T>;
/// | 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 |
///
/// # Valid field return types
///
/// - Scalar values, such as `i32` and `bool`. `usize`, `isize`, `u128` and `i128` are not

47
tests/derived_field.rs Normal file
View File

@ -0,0 +1,47 @@
use async_graphql::*;
#[tokio::test]
pub async fn test_derived_field() {
use serde::{Deserialize, Serialize};
struct Query;
#[derive(Serialize, Deserialize)]
struct ValueDerived(String);
scalar!(ValueDerived);
impl From<i32> 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",
})
);
}