diff --git a/integration_tests/juniper_tests/src/codegen/impl_object.rs b/integration_tests/juniper_tests/src/codegen/impl_object.rs new file mode 100644 index 000000000..28d259be5 --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/impl_object.rs @@ -0,0 +1,160 @@ +use super::util; +use juniper::{EmptyMutation, Executor, GraphQLType}; + +#[derive(GraphQLObject)] +struct SomeType { + a: bool, +} + +#[derive(Default)] +struct Context { + version: Option, +} + +#[derive(Default)] +struct TheQuery { + b: bool, +} + +#[juniper::impl_object( + scalar = juniper::DefaultScalarValue, + name = "Query", + description = "Type Description", + context = Context, +)] +impl TheQuery { + #[graphql(description = "With Self Description")] + fn with_self(&self) -> bool { + self.b + } + + fn independent() -> i32 { + 100 + } + + fn with_executor(exec: &Executor) -> bool { + true + } + + fn with_executor_and_self(&self, exec: &Executor) -> bool { + true + } + + fn with_context(exec: &Context) -> bool { + true + } + + fn with_context_and_self(&self, exec: &Context) -> bool { + true + } + + #[graphql(name = "renamed")] + fn has_custom_name() -> bool { + true + } + + #[graphql(description = "attr")] + fn has_description_attr() -> bool { + true + } + + /// Doc description + fn has_description_doc_comment() -> bool { + true + } + + fn has_argument(arg1: bool) -> bool { + true + } + + fn has_object_return(&self) -> SomeType { + SomeType { a: true } + } +} + +#[derive(Default)] +struct Mutation; + +#[juniper::impl_object(context = Context)] +impl Mutation { + fn empty() -> bool { + true + } +} + +#[test] +fn impl_object_full_introspect() { + let res = util::run_info_query::("Query"); + assert_eq!( + res, + juniper::graphql_value!({ + "name": "Query", + "description": "Type Description", + "fields": [ + { + "name": "withSelf", + "description": "With Self Description", + "args": [], + }, + { + "name": "independent", + "description": None, + "args": [], + }, + { + "name": "withExecutor", + "description": None, + "args": [], + }, + { + "name": "withExecutorAndSelf", + "description": None, + "args": [], + }, + { + "name": "withContext", + "description": None, + "args": [], + }, + { + "name": "withContextAndSelf", + "description": None, + "args": [], + }, + { + "name": "renamed", + "description": None, + "args": [], + }, + { + "name": "hasDescriptionAttr", + "description": "attr", + "args": [], + }, + { + "name": "hasDescriptionDocComment", + "description": "Doc description", + "args": [], + }, + { + "name": "hasArgument", + "description": None, + "args": [ + { + "name": "arg1", + "description": None, + "type": { + "name": None, + }, + } + ], + }, + { + "name": "hasObjectReturn", + "description": None, + "args": [], + }, + ] + }) + ); +} diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index 596af99fd..b39b4b344 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -1,3 +1,6 @@ +mod util; + mod derive_enum; mod derive_input_object; mod derive_object; +mod impl_object; diff --git a/integration_tests/juniper_tests/src/codegen/util.rs b/integration_tests/juniper_tests/src/codegen/util.rs new file mode 100644 index 000000000..b03e1410d --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/util.rs @@ -0,0 +1,55 @@ +use fnv::FnvHashMap; +use juniper::{DefaultScalarValue, GraphQLType, RootNode, Value, Variables}; +use std::default::Default; + +pub fn run_query(query: &str) -> Value +where + Query: GraphQLType + Default, + Mutation: GraphQLType + Default, + Context: Default, +{ + let schema = RootNode::new(Query::default(), Mutation::default()); + let (result, errs) = + juniper::execute(query, None, &schema, &Variables::new(), &Context::default()) + .expect("Execution failed"); + + assert_eq!(errs, []); + result +} + +pub fn run_info_query(type_name: &str) -> Value +where + Query: GraphQLType + Default, + Mutation: GraphQLType + Default, + Context: Default, +{ + let query = format!( + r#" + {{ + __type(name: "{}") {{ + name, + description, + fields {{ + name + description + args {{ + name + description + type {{ + name + }} + }} + }} + }} + }} + "#, + type_name + ); + let result = run_query::(&query); + result + .as_object_value() + .expect("Result is not an object") + .get_field_value("__type") + .expect("__type field missing") + .clone() +} diff --git a/integration_tests/juniper_tests/src/lib.rs b/integration_tests/juniper_tests/src/lib.rs index dd47c596d..7d6835aa0 100644 --- a/integration_tests/juniper_tests/src/lib.rs +++ b/integration_tests/juniper_tests/src/lib.rs @@ -9,5 +9,6 @@ extern crate fnv; #[cfg(test)] extern crate indexmap; +#[cfg(test)] mod codegen; mod custom_scalar; diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index fec422031..37eae1429 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -22,7 +22,6 @@ harness = false path = "benches/bench.rs" [features] -nightly = [] expose-test-schema = [] default = [ "chrono", diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index 39f9cc511..c96eca439 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -15,7 +15,7 @@ proc-macro = true [dependencies] proc-macro2 = "0.4" -syn = { version = "0.14", features = ["full", "extra-traits"] } +syn = { version = "0.15.28", features = ["full", "extra-traits", "parsing"] } quote = "0.6" regex = "1" lazy_static = "1.0.0" diff --git a/juniper_codegen/src/derive_object_impl.rs b/juniper_codegen/src/derive_object_impl.rs deleted file mode 100644 index 68e6ba045..000000000 --- a/juniper_codegen/src/derive_object_impl.rs +++ /dev/null @@ -1,248 +0,0 @@ -use proc_macro::TokenStream; -use quote::Tokens; -use std::collections::HashMap; -use syn::{ - parse, Attribute, FnArg, Ident, ImplItem, ImplItemMethod, Item, ItemImpl, Lit, Meta, - NestedMeta, Pat, ReturnType, Type, TypeReference, -}; - -fn get_attr_map(attr: &Attribute) -> Option<(String, HashMap)> { - let meta = attr.interpret_meta(); - - let meta_list = match meta { - Some(Meta::List(ref meta_list)) => meta_list, - _ => return None, - }; - - let ident = meta_list.ident.to_string(); - - let mut attr_map = HashMap::new(); - - for meta in meta_list.nested.iter() { - let value = match meta { - NestedMeta::Meta(Meta::NameValue(ref value)) => value, - _ => continue, - }; - - let name = value.ident.to_string(); - - let ident = match value.lit { - Lit::Str(ref string) => string.value(), - _ => continue, - }; - - attr_map.insert(name, ident); - } - - Some((ident, attr_map)) -} - -pub fn impl_gql_object(ast: Item) -> Tokens { - let ItemImpl { - attrs, - defaultness, - unsafety, - impl_token, - generics, - trait_, - self_ty, - mut items, - brace_token, - } = if let Item::Impl(imp) = ast { - imp - } else { - panic!("#[gql_object] Can only be applied to impl blocks"); - }; - - let (context, description) = if let Some((_, map)) = attrs - .iter() - .filter_map(get_attr_map) - .find(|(name, _)| name == "graphql") - { - ( - map.get("context").map(|st| Ident::from(st.clone())), - map.get("description").map(|i| i.to_string()), - ) - } else { - (None, None) - }; - - let parsed: TypeReference = parse(quote!(&Executor<#context>).into()).unwrap(); - - let mut fns = Vec::new(); - - for item in &mut items { - match item { - ImplItem::Const(..) => panic!("Unexpected const item"), - ImplItem::Macro(..) => panic!("Unexpected macro item"), - ImplItem::Verbatim(..) => panic!("Unexpected verbatim item"), - ImplItem::Type(..) => panic!("Unexpected type item"), - ImplItem::Method(ImplItemMethod { - sig, ref mut attrs, .. - }) => { - let (description, deprecated) = if let Some((_, map)) = attrs - .iter() - .filter_map(get_attr_map) - .find(|(name, _)| name == "graphql") - { - ( - map.get("description").map(|i| i.to_string()), - map.get("deprecated").map(|i| i.to_string()), - ) - } else { - (None, None) - }; - - attrs.clear(); - - match sig.decl.inputs[0] { - FnArg::Captured(ref arg) => match arg.ty { - Type::Reference(ref ty) if ty == &parsed => {} - _ => continue, - }, - _ => continue, - } - - let ret: Type = match sig.decl.output { - ReturnType::Type(_, ref ret) => (**ret).clone(), - _ => continue, - }; - - let mut fn_args = Vec::new(); - - for arg in sig.decl.inputs.iter().skip(1) { - if let FnArg::Captured(arg) = arg { - fn_args.push((arg.pat.clone(), arg.ty.clone())); - } else { - panic!("invalid arg {:?}", stringify!(arg)); - } - } - - fns.push((sig.ident, fn_args, ret, description, deprecated)); - } - } - } - - let name = (*self_ty).clone(); - - let item = Item::Impl(ItemImpl { - attrs: Vec::new(), - defaultness, - unsafety, - impl_token, - generics, - trait_, - self_ty, - items, - brace_token, - }); - - let exec_fns = fns.iter().map(|(name, args, _, _, _)| { - let get_args = args.iter().map(|(arg_name, arg_type)| { - quote! { - let #arg_name: #arg_type = args.get(&to_camel_case(stringify!(#arg_name))).expect("Argument missing - validation must have failed"); - } - }); - - let arg_names = args.iter().map(|(name, _)| name); - - quote! { - if field == &to_camel_case(stringify!(#name)) { - #(#get_args)* - - let result = Self::#name(&executor, #( #arg_names ),*); - return (IntoResolvable::into(result, executor.context())).and_then(|res| - match res { - Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), - None => Ok(Value::null()), - }); - } - } - }); - - let register_fns = fns - .iter() - .map(|(name, args, ret, description, deprecation)| { - let args = args.iter().map(|(arg_name, arg_type)| { - quote! { - .argument(registry.arg::<#arg_type>(&to_camel_case(stringify!(#arg_name)), info)) - } - }); - - let description = match description { - Some(description) => quote!(.description(#description)), - None => quote!(), - }; - - let deprecation = match deprecation { - Some(deprecation) => quote!(.deprecation(#deprecation)), - None => quote!(), - }; - - quote! { - fields.push( - registry - .field_convert::<#ret, _, Self::Context>(&to_camel_case(stringify!(#name)), info) - #(#args)* - #description - #deprecation - ); - } - }); - - let description = match description { - Some(description) => quote!(mt = mt.description(#description);), - None => quote!(), - }; - - let gql_impl = quote! { - impl GraphQLType for #name { - type Context = #context; - type TypeInfo = (); - - fn name(info: &::TypeInfo) -> Option<&str> { - Some(stringify!(#name)) - } - - #[allow(unused_assignments)] - #[allow(unused_mut)] - fn meta<'r>(info: &Self::TypeInfo, registry: &mut Registry<'r>) -> MetaType<'r> { - let mut fields = Vec::new(); - let mut interfaces: Option> = None; - #(#register_fns)* - let mut mt = registry.build_object_type::<#name>(info, &fields); - - #description - - if let Some(interfaces) = interfaces { - mt = mt.interfaces(&interfaces); - } - - mt.into_meta() - } - - #[allow(unused_variables)] - #[allow(unused_mut)] - fn resolve_field( - &self, - info: &(), - field: &str, - args: &Arguments, - executor: &Executor, - ) -> ExecutionResult { - #(#exec_fns)* - - panic!("Field {} not found on type {}", field, "Mutation"); - } - - fn concrete_type_name(&self, _: &Self::Context) -> String { - strin gify!(#name).to_string() - } - } - }; - - quote! { - #item - #gql_impl - } -} diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs new file mode 100644 index 000000000..0ec235a13 --- /dev/null +++ b/juniper_codegen/src/impl_object.rs @@ -0,0 +1,431 @@ +use crate::util; +use proc_macro::TokenStream; +use quote::quote; +use syn::{Lit, Meta, NestedMeta}; + +#[derive(Debug)] +pub struct ImplAttributes { + name: Option, + description: Option, + context: Option, + scalar: Option, +} + +impl syn::parse::Parse for ImplAttributes { + fn parse(input: syn::parse::ParseStream) -> syn::parse::Result { + let mut output = ImplAttributes { + name: None, + description: None, + context: None, + scalar: None, + }; + + while !input.is_empty() { + let ident: syn::Ident = input.parse()?; + match ident.to_string().as_str() { + "name" => { + input.parse::()?; + let val = input.parse::()?; + output.name = Some(val.value()); + } + "description" => { + input.parse::()?; + let val = input.parse::()?; + output.description = Some(val.value()); + } + "context" => { + input.parse::()?; + let val = input.parse::()?; + output.context = Some(val); + } + "scalar" => { + input.parse::()?; + let val = input.parse::()?; + output.scalar = Some(val); + } + other => { + return Err(input.error(format!("Unknown attribute for impl_object: {}", other))); + } + } + if input.lookahead1().peek(syn::Token![,]) { + input.parse::()?; + } + } + + Ok(output) + } +} + +#[derive(Default)] +pub struct FieldAttributes { + pub name: Option, + pub description: Option, + pub deprecation: Option, +} + +impl FieldAttributes { + pub fn from_attr(attr: &syn::Attribute) -> Self { + let mut output = Self { + name: None, + description: None, + deprecation: None, + }; + let meta_list = match attr.interpret_meta() { + Some(Meta::List(meta_list)) => meta_list, + _ => { + panic!("Invalid #[graphql(_)] attribute"); + } + }; + + for meta in meta_list.nested.iter() { + let item = match meta { + NestedMeta::Meta(Meta::NameValue(ref item)) => item, + _ => { + panic!("Invalid #[graphql(_)] attribute"); + } + }; + let name = item.ident.to_string(); + + let value = match item.lit { + Lit::Str(ref string) => string.value(), + _ => { + panic!( + "Invalid #[graphql(_)] attribute: expected ({} = \"...\"", + name + ); + } + }; + + match name.as_str() { + "name" => { + output.name = Some(value); + } + "description" => { + output.description = Some(value); + } + "deprecation" => { + output.deprecation = Some(value); + } + other => { + panic!( + "Invalid #[graphql(..)] attribute: unknown property {}", + other + ); + } + } + } + output + } + + pub fn from_attrs(attrs: &Vec) -> Self { + attrs + .iter() + .find(|attr| util::path_eq_single(&attr.path, "graphql")) + .map(Self::from_attr) + .unwrap_or(Self::default()) + } +} + +pub struct GraphQLTypeDefinitionFieldArg { + pub name: String, + pub description: Option, + pub _type: syn::Type, +} + +pub struct GraphQLTypeDefinitionField { + pub name: String, + pub _type: syn::Type, + pub description: Option, + pub deprecation: Option, + pub args: Vec, + pub resolver_code: proc_macro2::TokenStream, +} + +/// Definition of a graphql type based on information extracted +/// by various macros. +/// The definition can be rendered to Rust code. +pub struct GraphQLTypeDefiniton { + pub name: String, + pub _type: syn::Type, + pub context: Option, + pub scalar: Option, + pub description: Option, + pub fields: Vec, +} + +impl GraphQLTypeDefiniton { + fn to_tokens(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let ty = &self._type; + let context = self + .context + .as_ref() + .map(|ctx| quote!( #ctx )) + .unwrap_or(quote!(())); + let scalar = self + .scalar + .as_ref() + .map(|s| quote!( #s )) + .unwrap_or(quote!(juniper::DefaultScalarValue)); + + let field_definitions = self.fields.iter().map(|field| { + let args = field.args.iter().map(|arg| { + let arg_type = &arg._type; + let arg_name = &arg.name; + quote! { + .argument(registry.arg::<#arg_type>(#arg_name, info)) + } + }); + + let description = match field.description.as_ref() { + Some(description) => quote!( .description(#description) ), + None => quote!(), + }; + + let deprecation = match field.deprecation.as_ref() { + Some(deprecation) => quote!( .deprecation(#deprecation) ), + None => quote!(), + }; + + let field_name = &field.name; + + let _type = &field._type; + quote! { + registry + .field_convert::<#_type, _, Self::Context>(#field_name, info) + #(#args)* + #description + #deprecation + } + }); + + let resolve_matches = self.fields.iter().map(|field| { + let name = &field.name; + let code = &field.resolver_code; + + quote!( + #name => { + let res = #code; + juniper::IntoResolvable::into( + res, + executor.context() + ) + .and_then(|res| { + match res { + Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), + None => Ok(juniper::Value::null()), + } + }) + }, + ) + }); + + let description = match self.description.as_ref() { + Some(description) => quote!( .description(#description) ), + None => quote!(), + }; + + quote!( + impl juniper::GraphQLType<#scalar> for #ty { + type Context = #context; + type TypeInfo = (); + + fn name(_: &Self::TypeInfo) -> Option<&str> { + Some(#name) + } + + fn meta<'r>( + info: &Self::TypeInfo, + registry: &mut juniper::Registry<'r, #scalar> + ) -> juniper::meta::MetaType<'r, #scalar> + where #scalar : 'r, + for<'z> &'z juniper::DefaultScalarValue: juniper::ScalarRefValue<'z>, + { + let fields = vec![ + #( #field_definitions ),* + ]; + let meta = registry.build_object_type::<#ty>( info, &fields ); + let meta = meta # description; + meta.into_meta() + } + + #[allow(unused_variables)] + #[allow(unused_mut)] + fn resolve_field( + &self, + _info: &(), + field: &str, + args: &juniper::Arguments, + executor: &juniper::Executor, + ) -> juniper::ExecutionResult { + match field { + #( #resolve_matches )* + _ => { + panic!("Field {} not found on type {}", field, "Mutation"); + } + } + } + + fn concrete_type_name(&self, _: &Self::Context, _: &Self::TypeInfo) -> String { + #name.to_string() + } + + } + ) + } +} + +/// Generate code for the juniper::impl_object macro. +pub fn build(args: TokenStream, body: TokenStream) -> TokenStream { + let impl_attrs = match syn::parse::(args) { + Ok(attrs) => attrs, + Err(e) => { + panic!("Invalid attributes: {}", e); + } + }; + + let item = syn::parse::(body).unwrap(); + let mut _impl = match item { + syn::Item::Impl(_impl) => _impl, + _ => { + panic!("#[juniper::object] can only be applied to impl blocks"); + } + }; + + let type_name = match &*_impl.self_ty { + syn::Type::Path(ref type_path) => type_path + .path + .segments + .iter() + .last() + .unwrap() + .ident + .to_string(), + _ => { + panic!("Invalid impl target: expected a path"); + } + }; + let name = impl_attrs.name.unwrap_or(type_name); + + let target_type = *_impl.self_ty.clone(); + + let mut definition = GraphQLTypeDefiniton { + name, + _type: target_type.clone(), + context: impl_attrs.context, + scalar: impl_attrs.scalar, + description: impl_attrs.description, + fields: Vec::new(), + }; + + for item in &mut _impl.items { + match item { + syn::ImplItem::Type(_ty) => { + if _ty.ident == "Context" { + definition.context = Some(_ty.ty.clone()); + } else if _ty.ident == "Scalar" { + definition.scalar = Some(_ty.ty.clone()); + } else { + panic!("Invalid 'type {} = _' specification. only 'Context' and 'Scalar' are allowed."); + } + } + syn::ImplItem::Method(method) => { + let _type = match &method.sig.decl.output { + syn::ReturnType::Type(_, ref t) => (**t).clone(), + syn::ReturnType::Default => { + panic!( + "Invalid field method {}: must return a value", + method.sig.ident + ); + } + }; + + let attrs = FieldAttributes::from_attrs(&method.attrs); + // Remove graphql attribute from final output. + method + .attrs + .retain(|item| util::path_eq_single(&item.path, "graphql") == false); + + let mut args = Vec::new(); + let mut resolve_args = Vec::new(); + + for arg in &method.sig.decl.inputs { + match arg { + syn::FnArg::SelfRef(_) => { + // Can be ignored. + resolve_args.push(quote!(self)); + } + syn::FnArg::SelfValue(_) => { + panic!( + "Invalid method receiver in {}: self is not allowed, did you mean '&self'?", + method.sig.ident + ); + } + syn::FnArg::Captured(ref captured) => { + let arg_name = match &captured.pat { + syn::Pat::Ident(ref pat_ident) => pat_ident.ident.to_string(), + _ => { + panic!("Invalid token for function argument"); + } + }; + + // Check if the argument refers to the Executor or the context. + if util::type_is_identifier_ref(&captured.ty, "Executor") { + resolve_args.push(quote!(executor)); + } else if definition + .context + .as_ref() + .map(|ctx| util::type_is_ref_of(&captured.ty, ctx)) + .unwrap_or(false) + { + resolve_args.push(quote!(executor.context())); + } else { + let ty = &captured.ty; + resolve_args.push(quote!( + args + .get::<#ty>(#arg_name) + .expect("Argument missing - validation must have failed") + )); + args.push(GraphQLTypeDefinitionFieldArg { + name: arg_name, + description: None, + _type: ty.clone(), + }) + } + } + _ => panic!("Invalid argument type in method {}", method.sig.ident), + } + } + + let method_ident = &method.sig.ident; + let resolver_code = quote!( + #target_type :: #method_ident ( #( #resolve_args ),* ) + ); + + let name = attrs + .name + .unwrap_or(util::to_camel_case(&method_ident.to_string())); + let description = attrs.description.or(util::get_doc_comment(&method.attrs)); + + definition.fields.push(GraphQLTypeDefinitionField { + name, + _type, + args, + description, + deprecation: attrs.deprecation, + resolver_code, + }); + } + _ => { + panic!("Invalid item for GraphQL Object: only type declarations and methods are allowed"); + } + } + } + + let graphql_type_impl = definition.to_tokens(); + let body = quote!( + #_impl + #graphql_type_impl + ); + body.into() +} diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 939cdb648..8e670bd5a 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -19,8 +19,8 @@ extern crate regex; mod derive_enum; mod derive_input_object; mod derive_object; -mod derive_object_impl; mod derive_scalar_value; +mod impl_object; mod util; use proc_macro::TokenStream; @@ -62,13 +62,6 @@ pub fn derive_object(input: TokenStream) -> TokenStream { gen.into() } -#[proc_macro_attribute] -pub fn gql_object(_metadata: TokenStream, input: TokenStream) -> TokenStream { - let ast = syn::parse::(input).unwrap(); - let gen = derive_object_impl::impl_gql_object(ast); - gen.into() -} - #[proc_macro_derive(GraphQLScalarValue)] pub fn derive_scalar_value(input: TokenStream) -> TokenStream { let ast = syn::parse::(input).unwrap(); @@ -89,3 +82,10 @@ pub fn derive_scalar_value_internal(input: TokenStream) -> TokenStream { let gen = derive_scalar_value::impl_scalar_value(&ast, true); gen.into() } + +/// A proc macro for defining a GraphQL object. +#[proc_macro_attribute] +pub fn impl_object(args: TokenStream, input: TokenStream) -> TokenStream { + let gen = impl_object::build(args, input); + gen.into() +} diff --git a/juniper_codegen/src/util.rs b/juniper_codegen/src/util.rs index 12e9175dc..a3c87b99a 100644 --- a/juniper_codegen/src/util.rs +++ b/juniper_codegen/src/util.rs @@ -1,6 +1,36 @@ use regex::Regex; use syn::{Attribute, Lit, Meta, MetaList, MetaNameValue, NestedMeta}; +/// Compares a path to a one-segment string value, +/// return true if equal. +pub fn path_eq_single(path: &syn::Path, value: &str) -> bool { + path.segments.len() == 1 && path.segments[0].ident == value +} + +/// Check if a type is a reference to another type. +pub fn type_is_ref_of(ty: &syn::Type, target: &syn::Type) -> bool { + match ty { + syn::Type::Reference(_ref) => &*_ref.elem == target, + _ => false, + } +} + +/// Check if a Type is a simple identifier. +pub fn type_is_identifier(ty: &syn::Type, name: &str) -> bool { + match ty { + syn::Type::Path(ref type_path) => path_eq_single(&type_path.path, name), + _ => false, + } +} + +/// Check if a Type is a reference to a given identifier. +pub fn type_is_identifier_ref(ty: &syn::Type, name: &str) -> bool { + match ty { + syn::Type::Reference(_ref) => type_is_identifier(&*_ref.elem, name), + _ => false, + } +} + pub enum AttributeValidation { Any, Bare, @@ -16,6 +46,12 @@ pub struct DeprecationAttr { pub reason: Option, } +pub fn find_graphql_attr(attrs: &Vec) -> Option<&Attribute> { + attrs + .iter() + .find(|attr| path_eq_single(&attr.path, "graphql")) +} + pub fn get_deprecated(attrs: &Vec) -> Option { for attr in attrs { match attr.interpret_meta() {