From 02009c401bf153f10c45021614d24be92146f61b Mon Sep 17 00:00:00 2001 From: Miaxos Date: Fri, 22 Oct 2021 13:06:54 +0000 Subject: [PATCH] feat: add derived field for complex object --- derive/src/args.rs | 18 +++++-- derive/src/object.rs | 88 ++++++++++++++++++++++++++++++++++- docs/en/src/SUMMARY.md | 1 + docs/en/src/derived_fields.md | 61 ++++++++++++++++++++++++ src/lib.rs | 8 ++++ tests/derived_field.rs | 47 +++++++++++++++++++ 6 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 docs/en/src/derived_fields.md create mode 100644 tests/derived_field.rs diff --git a/derive/src/args.rs b/derive/src/args.rs index 0cdcf5b2..9beffb47 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, @@ -243,6 +243,16 @@ 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, } #[derive(FromDeriveInput)] diff --git a/derive/src/object.rs b/derive/src/object.rs index 2f44632a..940ac4f4 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,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::(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", + ) + .into())), + }, + FnArg::Receiver(_) => None, + }) + .collect::, 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::(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/docs/en/src/SUMMARY.md b/docs/en/src/SUMMARY.md index c11a8739..61cfb56c 100644 --- a/docs/en/src/SUMMARY.md +++ b/docs/en/src/SUMMARY.md @@ -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) diff --git a/docs/en/src/derived_fields.md b/docs/en/src/derived_fields.md new file mode 100644 index 00000000..b8174ec9 --- /dev/null +++ b/docs/en/src/derived_fields.md @@ -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 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! +} +``` diff --git a/src/lib.rs b/src/lib.rs index 37fa27b5..9b83da75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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,13 @@ 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 | +/// /// # Valid field return types /// /// - Scalar values, such as `i32` and `bool`. `usize`, `isize`, `u128` and `i128` are not diff --git a/tests/derived_field.rs b/tests/derived_field.rs new file mode 100644 index 00000000..ee1860e6 --- /dev/null +++ b/tests/derived_field.rs @@ -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 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", + }) + ); +}