Merge pull request #667 from Miaxos/derived_fields
feat: add derived field for object
This commit is contained in:
commit
057a29c145
|
@ -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)]
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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)
|
||||
|
|
61
docs/en/src/derived_fields.md
Normal file
61
docs/en/src/derived_fields.md
Normal 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 derived fields for objects to generate derived 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 derived 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!
|
||||
}
|
||||
```
|
|
@ -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
47
tests/derived_field.rs
Normal 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",
|
||||
})
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user