Add federation support

This commit is contained in:
sunli 2020-04-09 22:03:09 +08:00
parent d9a3cb4743
commit f1d0b3f641
19 changed files with 737 additions and 8 deletions

View File

@ -66,6 +66,7 @@ pub struct Object {
pub name: Option<String>,
pub desc: Option<String>,
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<String>,
pub deprecation: Option<String>,
pub cache_control: CacheControl,
pub external: bool,
pub provides: Option<String>,
pub requires: Option<String>,
}
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<InterfaceFieldArgument>,
pub deprecation: Option<String>,
pub context: bool,
pub external: bool,
pub provides: Option<String>,
pub requires: Option<String>,
}
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<String>,
pub desc: Option<String>,
pub fields: Vec<InterfaceField>,
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,
})
}
}

View File

@ -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,
}
})
}

View File

@ -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::<Self, _>(|registry| #crate_name::registry::Type::Object {
let ty = registry.create_type::<Self, _>(|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<serde_json::Value> {
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]

View File

@ -9,6 +9,7 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result<T
let crate_name = get_crate_name(object_args.internal);
let ident = &input.ident;
let generics = &input.generics;
let extends = object_args.extends;
let s = match &mut input.data {
Data::Struct(e) => 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<T
.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 = &item.ty;
let cache_control = {
@ -69,6 +79,9 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result<T
ty: <#ty as #crate_name::Type>::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<T
fields
},
cache_control: #cache_control,
extends: #extends,
keys: None,
})
}
}

View File

@ -176,6 +176,9 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> 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,
})
}
}

View File

@ -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<serde_json::Value> {
Err(QueryError::EntityNotFound.into_error(pos))
}
}
/// Represents a GraphQL input object

View File

@ -256,6 +256,12 @@ pub enum QueryError {
err: String,
extended_error: Option<serde_json::Value>,
},
#[error("Entity not found")]
EntityNotFound,
#[error("\"__typename\" must be an existing string")]
TypeNameNotExists,
}
impl QueryError {

View File

@ -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
///

View File

@ -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<String, Field>,
cache_control: CacheControl,
extends: bool,
keys: Option<Vec<String>>,
},
Interface {
name: String,
description: Option<&'static str>,
fields: HashMap<String, Field>,
possible_types: HashSet<String>,
extends: bool,
keys: Option<Vec<String>>,
},
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<Item = &'a Field>>(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,
},
);
}
}
}

54
src/scalars/any.rs Normal file
View File

@ -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<Self> {
Some(Self(value.clone()))
}
fn is_valid(_value: &Value) -> bool {
true
}
fn to_json(&self) -> Result<serde_json::Value> {
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(),
),
}
}

View File

@ -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)]

View File

@ -186,6 +186,9 @@ where
Subscription::create_type_info(&mut registry);
}
// federation
registry.create_federation_types();
SchemaBuilder(SchemaInner {
validation_mode: ValidationMode::Strict,
query: QueryRoot {

View File

@ -80,6 +80,9 @@ impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> 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<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> Type for Con
ty: <Option::<Vec<Option<Edge<T,E>>>> as Type>::create_type_info(registry),
deprecation: None,
cache_control: Default::default(),
external: false,
requires: None,
provides: None
},
);
@ -104,6 +110,9 @@ impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> Type for Con
ty: Option::<i32>::create_type_info(registry),
deprecation: None,
cache_control: Default::default(),
external: false,
requires: None,
provides: None
},
);
@ -115,11 +124,16 @@ impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> Type for Con
ty: Vec::<T>::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
})
}
}

View File

@ -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,
}
})
}

View File

@ -37,6 +37,8 @@ impl Type for EmptyMutation {
description: None,
fields: Default::default(),
cache_control: Default::default(),
extends: false,
keys: None,
})
}
}

View File

@ -25,6 +25,8 @@ impl Type for EmptySubscription {
description: None,
fields: Default::default(),
cache_control: Default::default(),
extends: false,
keys: None,
})
}
}

View File

@ -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<String>,
}
pub struct QueryRoot<T> {
pub inner: T,
pub disable_introspection: bool,
@ -33,6 +42,9 @@ impl<T: Type> Type for QueryRoot<T> {
ty: schema_type,
deprecation: None,
cache_control: Default::default(),
external: false,
requires: None,
provides: None,
},
);
@ -58,6 +70,9 @@ impl<T: Type> Type for QueryRoot<T> {
ty: "__Type".to_string(),
deprecation: None,
cache_control: Default::default(),
external: false,
requires: None,
provides: None,
},
);
}
@ -101,6 +116,24 @@ impl<T: ObjectType + Send + Sync> ObjectType for QueryRoot<T> {
field.position,
)
.await;
} else if field.name.as_str() == "_entities" {
let representations: Vec<Any> =
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

View File

@ -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(&registry_field.cache_control);
}
}

93
tests/federation.rs Normal file
View File

@ -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<Review> {
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<Review> {
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"},
]
})
);
}