Add federation support
This commit is contained in:
parent
d9a3cb4743
commit
f1d0b3f641
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
12
src/base.rs
12
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<serde_json::Value> {
|
||||
Err(QueryError::EntityNotFound.into_error(pos))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a GraphQL input object
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
|
232
src/registry.rs
232
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<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
54
src/scalars/any.rs
Normal 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(),
|
||||
),
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
|
|
|
@ -186,6 +186,9 @@ where
|
|||
Subscription::create_type_info(&mut registry);
|
||||
}
|
||||
|
||||
// federation
|
||||
registry.create_federation_types();
|
||||
|
||||
SchemaBuilder(SchemaInner {
|
||||
validation_mode: ValidationMode::Strict,
|
||||
query: QueryRoot {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@ impl Type for EmptyMutation {
|
|||
description: None,
|
||||
fields: Default::default(),
|
||||
cache_control: Default::default(),
|
||||
extends: false,
|
||||
keys: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ impl Type for EmptySubscription {
|
|||
description: None,
|
||||
fields: Default::default(),
|
||||
cache_control: Default::default(),
|
||||
extends: false,
|
||||
keys: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
93
tests/federation.rs
Normal file
93
tests/federation.rs
Normal 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"},
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user