From f1d0b3f6411af3a7cb6a4a6f5c3dcb92590fa5eb Mon Sep 17 00:00:00 2001 From: sunli Date: Thu, 9 Apr 2020 22:03:09 +0800 Subject: [PATCH] Add federation support --- async-graphql-derive/src/args.rs | 73 ++++++- async-graphql-derive/src/interface.rs | 17 ++ async-graphql-derive/src/object.rs | 163 ++++++++++++++- async-graphql-derive/src/simple_object.rs | 15 ++ async-graphql-derive/src/subscription.rs | 5 + src/base.rs | 12 +- src/error.rs | 6 + src/lib.rs | 6 +- src/registry.rs | 232 +++++++++++++++++++++- src/scalars/any.rs | 54 +++++ src/scalars/mod.rs | 2 + src/schema.rs | 3 + src/types/connection/connection_type.rs | 14 ++ src/types/connection/edge.rs | 8 + src/types/empty_mutation.rs | 2 + src/types/empty_subscription.rs | 2 + src/types/query_root.rs | 33 +++ src/validation/visitors/cache_control.rs | 5 +- tests/federation.rs | 93 +++++++++ 19 files changed, 737 insertions(+), 8 deletions(-) create mode 100644 src/scalars/any.rs create mode 100644 tests/federation.rs diff --git a/async-graphql-derive/src/args.rs b/async-graphql-derive/src/args.rs index e92eb4b9..c5c8a2a6 100644 --- a/async-graphql-derive/src/args.rs +++ b/async-graphql-derive/src/args.rs @@ -66,6 +66,7 @@ pub struct Object { pub name: Option, pub desc: Option, pub cache_control: CacheControl, + pub extends: bool, } impl Object { @@ -74,12 +75,16 @@ impl Object { let mut name = None; let mut desc = None; let mut cache_control = CacheControl::default(); + let mut extends = false; for arg in args { match arg { NestedMeta::Meta(Meta::Path(p)) if p.is_ident("internal") => { internal = true; } + NestedMeta::Meta(Meta::Path(p)) if p.is_ident("extends") => { + extends = true; + } NestedMeta::Meta(Meta::NameValue(nv)) => { if nv.path.is_ident("name") { if let syn::Lit::Str(lit) = nv.lit { @@ -115,6 +120,7 @@ impl Object { name, desc, cache_control, + extends, }) } } @@ -205,6 +211,9 @@ pub struct Field { pub desc: Option, pub deprecation: Option, pub cache_control: CacheControl, + pub external: bool, + pub provides: Option, + pub requires: Option, } impl Field { @@ -214,6 +223,9 @@ impl Field { let mut desc = None; let mut deprecation = None; let mut cache_control = CacheControl::default(); + let mut external = false; + let mut provides = None; + let mut requires = None; for attr in attrs { match attr.parse_meta()? { @@ -224,6 +236,9 @@ impl Field { is_field = true; for meta in &ls.nested { match meta { + NestedMeta::Meta(Meta::Path(p)) if p.is_ident("external") => { + external = true; + } NestedMeta::Meta(Meta::NameValue(nv)) => { if nv.path.is_ident("name") { if let syn::Lit::Str(lit) = &nv.lit { @@ -252,6 +267,24 @@ impl Field { "Attribute 'deprecation' should be a string.", )); } + } else if nv.path.is_ident("provides") { + if let syn::Lit::Str(lit) = &nv.lit { + provides = Some(lit.value()); + } else { + return Err(Error::new_spanned( + &nv.lit, + "Attribute 'provides' should be a string.", + )); + } + } else if nv.path.is_ident("requires") { + if let syn::Lit::Str(lit) = &nv.lit { + requires = Some(lit.value()); + } else { + return Err(Error::new_spanned( + &nv.lit, + "Attribute 'requires' should be a string.", + )); + } } } NestedMeta::Meta(Meta::List(ls)) => { @@ -273,6 +306,9 @@ impl Field { desc, deprecation, cache_control, + external, + provides, + requires, })) } else { Ok(None) @@ -630,6 +666,9 @@ pub struct InterfaceField { pub args: Vec, pub deprecation: Option, pub context: bool, + pub external: bool, + pub provides: Option, + pub requires: Option, } impl InterfaceField { @@ -640,12 +679,18 @@ impl InterfaceField { let mut args = Vec::new(); let mut deprecation = None; let mut context = false; + let mut external = false; + let mut provides = None; + let mut requires = None; for meta in &ls.nested { match meta { NestedMeta::Meta(Meta::Path(p)) if p.is_ident("context") => { context = true; } + NestedMeta::Meta(Meta::Path(p)) if p.is_ident("external") => { + external = true; + } NestedMeta::Meta(Meta::NameValue(nv)) => { if nv.path.is_ident("name") { if let syn::Lit::Str(lit) = &nv.lit { @@ -687,6 +732,24 @@ impl InterfaceField { "Attribute 'deprecation' should be a string.", )); } + } else if nv.path.is_ident("provides") { + if let syn::Lit::Str(lit) = &nv.lit { + provides = Some(lit.value()); + } else { + return Err(Error::new_spanned( + &nv.lit, + "Attribute 'provides' should be a string.", + )); + } + } else if nv.path.is_ident("requires") { + if let syn::Lit::Str(lit) = &nv.lit { + requires = Some(lit.value()); + } else { + return Err(Error::new_spanned( + &nv.lit, + "Attribute 'requires' should be a string.", + )); + } } } NestedMeta::Meta(Meta::List(ls)) if ls.path.is_ident("arg") => { @@ -711,6 +774,9 @@ impl InterfaceField { args, deprecation, context, + external, + requires, + provides, }) } } @@ -721,6 +787,7 @@ pub struct Interface { pub name: Option, pub desc: Option, pub fields: Vec, + pub extends: bool, } impl Interface { @@ -729,14 +796,15 @@ impl Interface { let mut name = None; let mut desc = None; let mut fields = Vec::new(); + let mut extends = false; for arg in args { match arg { NestedMeta::Meta(Meta::Path(p)) if p.is_ident("internal") => { internal = true; } - NestedMeta::Meta(Meta::Path(p)) if p.is_ident("internal") => { - internal = true; + NestedMeta::Meta(Meta::Path(p)) if p.is_ident("extends") => { + extends = true; } NestedMeta::Meta(Meta::NameValue(nv)) => { if nv.path.is_ident("name") { @@ -771,6 +839,7 @@ impl Interface { name, desc, fields, + extends, }) } } diff --git a/async-graphql-derive/src/interface.rs b/async-graphql-derive/src/interface.rs index b52e49ad..f19f2ab6 100644 --- a/async-graphql-derive/src/interface.rs +++ b/async-graphql-derive/src/interface.rs @@ -22,6 +22,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result Fields::Unnamed(fields) => fields, _ => return Err(Error::new_spanned(input, "All fields must be unnamed.")), }; + let extends = interface_args.extends; let mut enum_names = Vec::new(); let mut enum_items = Vec::new(); let mut type_into_impls = Vec::new(); @@ -86,6 +87,9 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result args, deprecation, context, + external, + provides, + requires, } in &interface_args.fields { let method_name = Ident::new(name, Span::call_site()); @@ -95,6 +99,14 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result let mut decl_params = Vec::new(); let mut get_params = Vec::new(); let mut schema_args = Vec::new(); + let requires = match &requires { + Some(requires) => quote! { Some(#requires) }, + None => quote! { None }, + }; + let provides = match &provides { + Some(provides) => quote! { Some(#provides) }, + None => quote! { None }, + }; if *context { decl_params.push(quote! { ctx: &'ctx #crate_name::Context<'ctx> }); @@ -190,6 +202,9 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result ty: <#schema_ty as #crate_name::Type>::create_type_info(registry), deprecation: #deprecation, cache_control: Default::default(), + external: #external, + provides: #provides, + requires: #requires, }); }); @@ -252,6 +267,8 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result #(#possible_types)* possible_types }, + extends: #extends, + keys: None, } }) } diff --git a/async-graphql-derive/src/object.rs b/async-graphql-derive/src/object.rs index 77113377..b379cacd 100644 --- a/async-graphql-derive/src/object.rs +++ b/async-graphql-derive/src/object.rs @@ -20,6 +20,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< _ => return Err(Error::new_spanned(&item_impl.self_ty, "Invalid type")), }; let generics = &item_impl.generics; + let extends = object_args.extends; let gql_typename = object_args .name @@ -35,6 +36,9 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< let mut resolvers = Vec::new(); let mut schema_fields = Vec::new(); + let mut find_entities = Vec::new(); + let mut add_keys = Vec::new(); + let mut create_entity_types = Vec::new(); for item in &mut item_impl.items { if let ImplItem::Method(method) = item { @@ -60,6 +64,15 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< .as_ref() .map(|s| quote! {Some(#s)}) .unwrap_or_else(|| quote! {None}); + let external = field.external; + let requires = match &field.requires { + Some(requires) => quote! { Some(#requires) }, + None => quote! { None }, + }; + let provides = match &field.provides { + Some(provides) => quote! { Some(#provides) }, + None => quote! { None }, + }; let ty = match &method.sig.output { ReturnType::Type(_, ty) => OutputType::parse(ty)?, ReturnType::Default => { @@ -192,6 +205,9 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< ty: <#schema_ty as #crate_name::Type>::create_type_info(registry), deprecation: #field_deprecation, cache_control: #cache_control, + external: #external, + provides: #provides, + requires: #requires, }); }); @@ -233,6 +249,127 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< .map(|(idx, _)| idx) .unwrap(), ); + } else if method.attrs.iter().any(|attr| attr.path.is_ident("entity")) { + let ty = match &method.sig.output { + ReturnType::Type(_, ty) => OutputType::parse(ty)?, + ReturnType::Default => { + return Err(Error::new_spanned(&method.sig.output, "Missing type")) + } + }; + let mut arg_ctx = false; + let mut args = Vec::new(); + + for (idx, arg) in method.sig.inputs.iter_mut().enumerate() { + if let FnArg::Receiver(receiver) = arg { + if idx != 0 { + return Err(Error::new_spanned( + receiver, + "The self receiver must be the first parameter.", + )); + } + } else if let FnArg::Typed(pat) = arg { + if idx == 0 { + return Err(Error::new_spanned( + pat, + "The self receiver must be the first parameter.", + )); + } + + match (&*pat.pat, &*pat.ty) { + (Pat::Ident(arg_ident), Type::Path(arg_ty)) => { + args.push(( + arg_ident, + arg_ty, + args::Argument::parse(&crate_name, &pat.attrs)?, + )); + pat.attrs.clear(); + } + (_, Type::Reference(TypeReference { elem, .. })) => { + if let Type::Path(path) = elem.as_ref() { + if idx != 1 + || path.path.segments.last().unwrap().ident != "Context" + { + return Err(Error::new_spanned( + arg, + "The Context must be the second argument.", + )); + } + arg_ctx = true; + } + } + _ => return Err(Error::new_spanned(arg, "Invalid argument type.")), + } + } + } + + let entity_type = ty.value_type(); + let mut key_pat = Vec::new(); + let mut key_getter = Vec::new(); + let mut use_keys = Vec::new(); + let mut keys = Vec::new(); + let mut keys_str = String::new(); + + for (ident, ty, args::Argument { name, .. }) in &args { + let name = name + .clone() + .unwrap_or_else(|| ident.ident.to_string().to_camel_case()); + + if !keys_str.is_empty() { + keys_str.push(' '); + } + keys_str.push_str(&name); + + key_pat.push(quote! { + Some(#ident) + }); + key_getter.push(quote! { + params.get(#name).and_then(|value| { + let value: Option<#ty> = #crate_name::InputValueType::parse(value); + value + }) + }); + keys.push(name); + use_keys.push(ident); + } + add_keys.push(quote! { registry.add_keys(&#entity_type::type_name(), #keys_str); }); + create_entity_types.push(quote! { #entity_type::create_type_info(registry); }); + + let field_ident = &method.sig.ident; + let ctx_param = if arg_ctx { + quote! { &ctx, } + } else { + quote! {} + }; + let do_find = match &ty { + OutputType::Value(_) => quote! { + self.#field_ident(#ctx_param #(#use_keys),*).await + }, + OutputType::Result(_, _) => { + quote! { self.#field_ident(#ctx_param #(#use_keys),*).await? } + } + }; + + find_entities.push(( + args.len(), + quote! { + if typename == &#entity_type::type_name() { + if let (#(#key_pat),*) = (#(#key_getter),*) { + let ctx_obj = ctx.with_selection_set(&ctx.selection_set); + return #crate_name::OutputValueType::resolve(&#do_find, &ctx_obj, pos).await; + } + } + }, + )); + + method.attrs.remove( + method + .attrs + .iter() + .enumerate() + .find(|(_, a)| a.path.is_ident("entity")) + .map(|(idx, _)| idx) + .unwrap(), + ); } } } @@ -248,6 +385,9 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< } }; + find_entities.sort_by(|(a, _), (b, _)| b.cmp(a)); + let find_entities_iter = find_entities.iter().map(|(_, code)| code); + let expanded = quote! { #item_impl @@ -257,7 +397,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> String { - registry.create_type::(|registry| #crate_name::registry::Type::Object { + let ty = registry.create_type::(|registry| #crate_name::registry::Type::Object { name: #gql_typename.to_string(), description: #desc, fields: { @@ -266,7 +406,12 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< fields }, cache_control: #cache_control, - }) + extends: #extends, + keys: None, + }); + #(#create_entity_types)* + #(#add_keys)* + ty } } @@ -279,6 +424,20 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< object: #gql_typename.to_string(), }.into_error(field.position)) } + + async fn find_entity(&self, ctx: &#crate_name::Context<'_>, pos: #crate_name::Pos, params: &#crate_name::Value) -> #crate_name::Result { + let params = match params { + #crate_name::Value::Object(params) => params, + _ => return Err(#crate_name::QueryError::EntityNotFound.into_error(pos)), + }; + let typename = if let Some(#crate_name::Value::String(typename)) = params.get("__typename") { + typename + } else { + return Err(#crate_name::QueryError::TypeNameNotExists.into_error(pos)); + }; + #(#find_entities_iter)* + Err(#crate_name::QueryError::EntityNotFound.into_error(pos)) + } } #[#crate_name::async_trait::async_trait] diff --git a/async-graphql-derive/src/simple_object.rs b/async-graphql-derive/src/simple_object.rs index 99a546d7..cc51a00d 100644 --- a/async-graphql-derive/src/simple_object.rs +++ b/async-graphql-derive/src/simple_object.rs @@ -9,6 +9,7 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result e, _ => return Err(Error::new_spanned(input, "It should be a struct")), @@ -48,6 +49,15 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result quote! { Some(#requires) }, + None => quote! { None }, + }; + let provides = match &field.provides { + Some(provides) => quote! { Some(#provides) }, + None => quote! { None }, + }; let ty = &item.ty; let cache_control = { @@ -69,6 +79,9 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result::create_type_info(registry), deprecation: #field_deprecation, cache_control: #cache_control, + external: #external, + provides: #provides, + requires: #requires, }); }); @@ -120,6 +133,8 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result Result< ty: <#stream_ty as #crate_name::futures::stream::Stream>::Item::create_type_info(registry), deprecation: #field_deprecation, cache_control: Default::default(), + external: false, + requires: None, + provides: None, }); }); @@ -240,6 +243,8 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< fields }, cache_control: Default::default(), + extends: false, + keys: None, }) } } diff --git a/src/base.rs b/src/base.rs index f92fa5bb..7a62a31e 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,5 +1,5 @@ use crate::registry::Registry; -use crate::{registry, Context, ContextSelectionSet, Result, ID}; +use crate::{registry, Context, ContextSelectionSet, QueryError, Result, ID}; use graphql_parser::query::{Field, Value}; use graphql_parser::Pos; use std::borrow::Cow; @@ -92,6 +92,16 @@ pub trait ObjectType: OutputValueType { { crate::collect_fields(ctx, self, futures) } + + /// Query entities with params + async fn find_entity( + &self, + _ctx: &Context<'_>, + pos: Pos, + _params: &Value, + ) -> Result { + Err(QueryError::EntityNotFound.into_error(pos)) + } } /// Represents a GraphQL input object diff --git a/src/error.rs b/src/error.rs index 910160ac..496b3b27 100644 --- a/src/error.rs +++ b/src/error.rs @@ -256,6 +256,12 @@ pub enum QueryError { err: String, extended_error: Option, }, + + #[error("Entity not found")] + EntityNotFound, + + #[error("\"__typename\" must be an existing string")] + TypeNameNotExists, } impl QueryError { diff --git a/src/lib.rs b/src/lib.rs index 7ed74548..9ecb16d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,7 +109,7 @@ pub use graphql_parser::query::Value; pub use graphql_parser::Pos; pub use query::{QueryBuilder, QueryResponse}; pub use registry::CacheControl; -pub use scalars::ID; +pub use scalars::{Any, ID}; pub use schema::Schema; pub use subscription::{ SimpleBroker, SubscriptionStream, SubscriptionStreams, SubscriptionTransport, @@ -149,6 +149,7 @@ pub use types::{EnumItem, EnumType}; /// | name | Object name | string | Y | /// | desc | Object description | string | Y | /// | cache_control | Object cache control | [`CacheControl`](struct.CacheControl.html) | Y | +/// | extends | Add fields to an entity that's defined in another service | bool | Y | /// /// # Field parameters /// @@ -158,6 +159,9 @@ pub use types::{EnumItem, EnumType}; /// | desc | Field description | string | Y | /// | deprecation | Field deprecation reason | string | Y | /// | cache_control | Field cache control | [`CacheControl`](struct.CacheControl.html) | Y | +/// | external | Mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. | bool | Y | +/// | provides | Annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | string | Y | +/// | requires | Annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. | string | Y | /// /// # Field argument parameters /// diff --git a/src/registry.rs b/src/registry.rs index b709665c..723f76bd 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -1,7 +1,8 @@ use crate::validators::InputValueValidator; -use crate::{model, Value}; +use crate::{model, Any, Type as _, Value}; use graphql_parser::query::Type as ParsedType; use std::collections::{HashMap, HashSet}; +use std::fmt::Write; use std::sync::Arc; fn parse_non_null(type_name: &str) -> Option<&str> { @@ -103,6 +104,9 @@ pub struct Field { pub ty: String, pub deprecation: Option<&'static str>, pub cache_control: CacheControl, + pub external: bool, + pub requires: Option<&'static str>, + pub provides: Option<&'static str>, } #[derive(Clone)] @@ -199,12 +203,16 @@ pub enum Type { description: Option<&'static str>, fields: HashMap, cache_control: CacheControl, + extends: bool, + keys: Option>, }, Interface { name: String, description: Option<&'static str>, fields: HashMap, possible_types: HashSet, + extends: bool, + keys: Option>, }, Union { name: String, @@ -347,6 +355,8 @@ impl Registry { description: None, fields: Default::default(), cache_control: Default::default(), + extends: false, + keys: None, }, ); let ty = f(self); @@ -373,6 +383,19 @@ impl Registry { }); } + pub fn add_keys(&mut self, ty: &str, keys: &str) { + let all_keys = match self.types.get_mut(ty) { + Some(Type::Object { keys: all_keys, .. }) => all_keys, + Some(Type::Interface { keys: all_keys, .. }) => all_keys, + _ => return, + }; + if let Some(all_keys) = all_keys { + all_keys.push(keys.to_string()); + } else { + *all_keys = Some(vec![keys.to_string()]); + } + } + pub fn concrete_type_by_name(&self, type_name: &str) -> Option<&Type> { self.types.get(TypeName::concrete_typename(type_name)) } @@ -384,4 +407,211 @@ impl Registry { ParsedType::NamedType(name) => self.types.get(name.as_str()), } } + + fn create_federation_fields<'a, I: Iterator>(sdl: &mut String, it: I) { + for field in it { + write!(sdl, "\t{}: {}", field.name, field.ty).ok(); + if field.external { + write!(sdl, " @external").ok(); + } + if let Some(requires) = field.requires { + write!(sdl, " @requires(fields: \"{}\")", requires).ok(); + } + if let Some(provides) = field.provides { + write!(sdl, " @provides(fields: \"{}\")", provides).ok(); + } + write!(sdl, "\n").ok(); + } + } + + fn create_federation_type(&self, ty: &Type, sdl: &mut String) { + match ty { + Type::Object { + name, + fields, + extends, + keys, + .. + } => { + if name.starts_with("__") { + return; + } + if name == "_Service" { + return; + } + if fields.len() == 4 { + // Is empty query root, only __schema, __type, _service, _entities fields + return; + } + + if *extends { + write!(sdl, "extend ").ok(); + } + write!(sdl, "type {} ", name).ok(); + if let Some(keys) = keys { + for key in keys { + write!(sdl, "@key(fields: \"{}\") ", key).ok(); + } + } + write!(sdl, "{{\n").ok(); + Self::create_federation_fields(sdl, fields.values()); + write!(sdl, "}}\n").ok(); + } + Type::Interface { + name, + fields, + extends, + keys, + .. + } => { + if *extends { + write!(sdl, "extend ").ok(); + } + write!(sdl, "interface {} ", name).ok(); + if let Some(keys) = keys { + for key in keys { + write!(sdl, "@key(fields: \"{}\") ", key).ok(); + } + } + write!(sdl, "{{\n").ok(); + Self::create_federation_fields(sdl, fields.values()); + write!(sdl, "}}\n").ok(); + } + _ => {} + } + } + + pub fn create_federation_sdl(&self) -> String { + let mut sdl = String::new(); + for ty in self.types.values() { + self.create_federation_type(ty, &mut sdl); + } + sdl + } + + fn has_entities(&self) -> bool { + self.types.values().any(|ty| match ty { + Type::Object { + keys: Some(keys), .. + } => !keys.is_empty(), + Type::Interface { + keys: Some(keys), .. + } => !keys.is_empty(), + _ => false, + }) + } + + fn create_entity_type(&mut self) { + let possible_types = self + .types + .values() + .filter_map(|ty| match ty { + Type::Object { + name, + keys: Some(keys), + .. + } if !keys.is_empty() => Some(name.clone()), + Type::Interface { + name, + keys: Some(keys), + .. + } if !keys.is_empty() => Some(name.clone()), + _ => None, + }) + .collect(); + + self.types.insert( + "_Entity".to_string(), + Type::Union { + name: "_Entity".to_string(), + description: None, + possible_types, + }, + ); + } + + pub fn create_federation_types(&mut self) { + if !self.has_entities() { + return; + } + + Any::create_type_info(self); + + self.types.insert( + "_Service".to_string(), + Type::Object { + name: "_Service".to_string(), + description: None, + fields: { + let mut fields = HashMap::new(); + fields.insert( + "sdl".to_string(), + Field { + name: "sdl".to_string(), + description: None, + args: Default::default(), + ty: "String".to_string(), + deprecation: None, + cache_control: Default::default(), + external: false, + requires: None, + provides: None, + }, + ); + fields + }, + cache_control: Default::default(), + extends: false, + keys: None, + }, + ); + + self.create_entity_type(); + + let query_root = self.types.get_mut(&self.query_type).unwrap(); + if let Type::Object { fields, .. } = query_root { + fields.insert( + "_service".to_string(), + Field { + name: "_service".to_string(), + description: None, + args: Default::default(), + ty: "_Service!".to_string(), + deprecation: None, + cache_control: Default::default(), + external: false, + requires: None, + provides: None, + }, + ); + + fields.insert( + "_entities".to_string(), + Field { + name: "_entities".to_string(), + description: None, + args: { + let mut args = HashMap::new(); + args.insert( + "representations", + InputValue { + name: "representations", + description: None, + ty: "[_Any!]!".to_string(), + default_value: None, + validator: None, + }, + ); + args + }, + ty: "[_Entity]!".to_string(), + deprecation: None, + cache_control: Default::default(), + external: false, + requires: None, + provides: None, + }, + ); + } + } } diff --git a/src/scalars/any.rs b/src/scalars/any.rs new file mode 100644 index 00000000..e1776206 --- /dev/null +++ b/src/scalars/any.rs @@ -0,0 +1,54 @@ +use crate::{impl_scalar_internal, Result, Scalar, Value}; +use itertools::Itertools; + +/// Any scalar +/// +/// The `Any` scalar is used to pass representations of entities from external services into the root `_entities` field for execution. +#[derive(Clone, PartialEq, Debug)] +pub struct Any(pub Value); + +impl Scalar for Any { + fn type_name() -> &'static str { + "_Any" + } + + fn description() -> Option<&'static str> { + Some("The `_Any` scalar is used to pass representations of entities from external services into the root `_entities` field for execution.") + } + + fn parse(value: &Value) -> Option { + Some(Self(value.clone())) + } + + fn is_valid(_value: &Value) -> bool { + true + } + + fn to_json(&self) -> Result { + Ok(gql_value_to_json_value(&self.0)) + } +} + +impl_scalar_internal!(Any); + +fn gql_value_to_json_value(value: &Value) -> serde_json::Value { + match value { + Value::Null => serde_json::Value::Null, + Value::Variable(name) => name.clone().into(), + Value::Int(n) => n.as_i64().unwrap().into(), + Value::Float(n) => (*n).into(), + Value::String(s) => s.clone().into(), + Value::Boolean(v) => (*v).into(), + Value::Enum(e) => e.clone().into(), + Value::List(values) => values + .into_iter() + .map(|value| gql_value_to_json_value(value)) + .collect_vec() + .into(), + Value::Object(obj) => serde_json::Value::Object( + obj.into_iter() + .map(|(k, v)| (k.clone(), gql_value_to_json_value(v))) + .collect(), + ), + } +} diff --git a/src/scalars/mod.rs b/src/scalars/mod.rs index e1fe6d4a..ba236388 100644 --- a/src/scalars/mod.rs +++ b/src/scalars/mod.rs @@ -1,3 +1,4 @@ +mod any; mod bool; mod chrono_tz; mod datetime; @@ -12,6 +13,7 @@ mod bson; #[cfg(feature = "uuid")] mod uuid; +pub use any::Any; pub use id::ID; #[cfg(test)] diff --git a/src/schema.rs b/src/schema.rs index 213cf5b5..dd08f0b5 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -186,6 +186,9 @@ where Subscription::create_type_info(&mut registry); } + // federation + registry.create_federation_types(); + SchemaBuilder(SchemaInner { validation_mode: ValidationMode::Strict, query: QueryRoot { diff --git a/src/types/connection/connection_type.rs b/src/types/connection/connection_type.rs index e29bf9c5..52759b00 100644 --- a/src/types/connection/connection_type.rs +++ b/src/types/connection/connection_type.rs @@ -80,6 +80,9 @@ impl Type for Con ty: PageInfo::create_type_info(registry), deprecation: None, cache_control: Default::default(), + external: false, + requires: None, + provides: None }, ); @@ -92,6 +95,9 @@ impl Type for Con ty: >>> as Type>::create_type_info(registry), deprecation: None, cache_control: Default::default(), + external: false, + requires: None, + provides: None }, ); @@ -104,6 +110,9 @@ impl Type for Con ty: Option::::create_type_info(registry), deprecation: None, cache_control: Default::default(), + external: false, + requires: None, + provides: None }, ); @@ -115,11 +124,16 @@ impl Type for Con ty: Vec::::type_name().to_string(), deprecation: None, cache_control: Default::default(), + external: false, + requires: None, + provides: None }); fields }, cache_control: Default::default(), + extends: false, + keys: None }) } } diff --git a/src/types/connection/edge.rs b/src/types/connection/edge.rs index 78ac6d21..31a10ca2 100644 --- a/src/types/connection/edge.rs +++ b/src/types/connection/edge.rs @@ -48,6 +48,9 @@ where ty: T::create_type_info(registry), deprecation: None, cache_control: Default::default(), + external: false, + requires: None, + provides: None, }, ); @@ -60,6 +63,9 @@ where ty: String::create_type_info(registry), deprecation: None, cache_control: Default::default(), + external: false, + requires: None, + provides: None, }, ); @@ -67,6 +73,8 @@ where fields }, cache_control: Default::default(), + extends: false, + keys: None, } }) } diff --git a/src/types/empty_mutation.rs b/src/types/empty_mutation.rs index 618c5d58..9104e920 100644 --- a/src/types/empty_mutation.rs +++ b/src/types/empty_mutation.rs @@ -37,6 +37,8 @@ impl Type for EmptyMutation { description: None, fields: Default::default(), cache_control: Default::default(), + extends: false, + keys: None, }) } } diff --git a/src/types/empty_subscription.rs b/src/types/empty_subscription.rs index fd954321..3c515fe9 100644 --- a/src/types/empty_subscription.rs +++ b/src/types/empty_subscription.rs @@ -25,6 +25,8 @@ impl Type for EmptySubscription { description: None, fields: Default::default(), cache_control: Default::default(), + extends: false, + keys: None, }) } } diff --git a/src/types/query_root.rs b/src/types/query_root.rs index 54d0fec1..47fd8f81 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -1,13 +1,22 @@ use crate::model::{__Schema, __Type}; +use crate::scalars::Any; use crate::{ do_resolve, registry, Context, ContextSelectionSet, Error, ObjectType, OutputValueType, QueryError, Result, Type, Value, }; +use async_graphql_derive::SimpleObject; use graphql_parser::query::Field; use graphql_parser::Pos; use std::borrow::Cow; use std::collections::HashMap; +/// Federation service +#[SimpleObject(internal)] +struct Service { + #[field] + sdl: Option, +} + pub struct QueryRoot { pub inner: T, pub disable_introspection: bool, @@ -33,6 +42,9 @@ impl Type for QueryRoot { ty: schema_type, deprecation: None, cache_control: Default::default(), + external: false, + requires: None, + provides: None, }, ); @@ -58,6 +70,9 @@ impl Type for QueryRoot { ty: "__Type".to_string(), deprecation: None, cache_control: Default::default(), + external: false, + requires: None, + provides: None, }, ); } @@ -101,6 +116,24 @@ impl ObjectType for QueryRoot { field.position, ) .await; + } else if field.name.as_str() == "_entities" { + let representations: Vec = + ctx.param_value("representations", field.position, || Value::Null)?; + let mut res = Vec::new(); + for item in representations { + res.push(self.inner.find_entity(ctx, field.position, &item.0).await?); + } + return Ok(res.into()); + } else if field.name.as_str() == "_service" { + let ctx_obj = ctx.with_selection_set(&field.selection_set); + return OutputValueType::resolve( + &Service { + sdl: Some(ctx.registry.create_federation_sdl()), + }, + &ctx_obj, + field.position, + ) + .await; } self.inner.resolve_field(ctx, field).await diff --git a/src/validation/visitors/cache_control.rs b/src/validation/visitors/cache_control.rs index 79510500..6edc427f 100644 --- a/src/validation/visitors/cache_control.rs +++ b/src/validation/visitors/cache_control.rs @@ -17,7 +17,10 @@ impl<'ctx, 'a> Visitor<'ctx> for CacheControlCalculate<'a> { } fn enter_field(&mut self, ctx: &mut VisitorContext<'_>, field: &Field) { - if let Some(registry_field) = ctx.parent_type().unwrap().field_by_name(&field.name) { + if let Some(registry_field) = ctx + .parent_type() + .and_then(|parent| parent.field_by_name(&field.name)) + { self.cache_control.merge(®istry_field.cache_control); } } diff --git a/tests/federation.rs b/tests/federation.rs new file mode 100644 index 00000000..b46bfc5f --- /dev/null +++ b/tests/federation.rs @@ -0,0 +1,93 @@ +use async_graphql::*; + +struct User { + id: ID, +} + +#[Object(extends)] +impl User { + #[field(external)] + async fn id(&self) -> &ID { + &self.id + } + + #[field] + async fn reviews(&self) -> Vec { + todo!() + } +} + +struct Review; + +#[Object] +impl Review { + #[field] + async fn body(&self) -> String { + todo!() + } + + #[field(provides = "username")] + async fn author(&self) -> User { + todo!() + } + + #[field] + async fn product(&self) -> Product { + todo!() + } +} + +struct Product { + upc: String, +} + +#[Object(extends)] +impl Product { + #[field(external)] + async fn upc(&self) -> &str { + &self.upc + } + + #[field] + async fn reviews(&self) -> Vec { + todo!() + } +} + +struct QueryRoot; + +#[Object] +impl QueryRoot { + #[entity] + async fn find_user_by_id(&self, id: ID) -> User { + User { id } + } + + #[entity] + async fn find_product_by_upc(&self, upc: String) -> Product { + Product { upc } + } +} + +#[async_std::test] +pub async fn test_federation() { + let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); + let query = format!( + r#"{{ + _entities(representations: [{{__typename: "Product", upc: "B00005N5PF"}}]) {{ + __typename + ... on Product {{ + upc + }} + }} + }}"# + ); + assert_eq!( + schema.execute(&query).await.unwrap().data, + serde_json::json!({ + "_entities": [ + {"__typename": "Product", "upc": "B00005N5PF"}, + ] + }) + ); +}