Refacto graphql_union! macro to proc macro
This commit is contained in:
parent
c903d5e004
commit
ee9a82a817
17 changed files with 412 additions and 193 deletions
|
@ -4,3 +4,4 @@ mod derive_enum;
|
|||
mod derive_input_object;
|
||||
mod derive_object;
|
||||
mod scalar_value_transparent;
|
||||
mod unions;
|
||||
|
|
4
integration_tests/juniper_tests/src/codegen/unions.rs
Normal file
4
integration_tests/juniper_tests/src/codegen/unions.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
|
||||
|
||||
|
|
@ -166,12 +166,24 @@ mod union {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
graphql_union!(<'a> &'a dyn Pet: () as "Pet" |&self| {
|
||||
instance_resolvers: |&_| {
|
||||
&Dog => self.as_dog(),
|
||||
&Cat => self.as_cat(),
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
#[crate::union_internal]
|
||||
impl<'a> &'a dyn Pet {
|
||||
fn resolve(&self) {
|
||||
match self {
|
||||
Dog => self.as_dog(),
|
||||
Cat => self.as_cat(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Dog {
|
||||
name: String,
|
||||
|
@ -227,7 +239,7 @@ mod union {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
fn test_unions() {
|
||||
let schema = RootNode::new(
|
||||
Schema {
|
||||
pets: vec![
|
||||
|
|
|
@ -158,7 +158,10 @@ fn inline_complex_input() {
|
|||
|result: &Object<DefaultScalarValue>| {
|
||||
assert_eq!(
|
||||
result.get_field_value("fieldWithObjectInput"),
|
||||
Some(&Value::scalar(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#)));
|
||||
Some(&Value::scalar(
|
||||
r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#
|
||||
))
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -170,7 +173,10 @@ fn inline_parse_single_value_to_list() {
|
|||
|result: &Object<DefaultScalarValue>| {
|
||||
assert_eq!(
|
||||
result.get_field_value("fieldWithObjectInput"),
|
||||
Some(&Value::scalar(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#)));
|
||||
Some(&Value::scalar(
|
||||
r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#
|
||||
))
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -182,7 +188,10 @@ fn inline_runs_from_input_value_on_scalar() {
|
|||
|result: &Object<DefaultScalarValue>| {
|
||||
assert_eq!(
|
||||
result.get_field_value("fieldWithObjectInput"),
|
||||
Some(&Value::scalar(r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"#)));
|
||||
Some(&Value::scalar(
|
||||
r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"#
|
||||
))
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -208,7 +217,10 @@ fn variable_complex_input() {
|
|||
|result: &Object<DefaultScalarValue>| {
|
||||
assert_eq!(
|
||||
result.get_field_value("fieldWithObjectInput"),
|
||||
Some(&Value::scalar(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#)));
|
||||
Some(&Value::scalar(
|
||||
r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#
|
||||
))
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -234,7 +246,10 @@ fn variable_parse_single_value_to_list() {
|
|||
|result: &Object<DefaultScalarValue>| {
|
||||
assert_eq!(
|
||||
result.get_field_value("fieldWithObjectInput"),
|
||||
Some(&Value::scalar(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#)));
|
||||
Some(&Value::scalar(
|
||||
r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#
|
||||
))
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -259,7 +274,10 @@ fn variable_runs_from_input_value_on_scalar() {
|
|||
|result: &Object<DefaultScalarValue>| {
|
||||
assert_eq!(
|
||||
result.get_field_value("fieldWithObjectInput"),
|
||||
Some(&Value::scalar(r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"#)));
|
||||
Some(&Value::scalar(
|
||||
r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"#
|
||||
))
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -306,12 +324,13 @@ fn variable_error_on_incorrect_type() {
|
|||
|
||||
let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err();
|
||||
|
||||
assert_eq!(error, ValidationError(vec![
|
||||
RuleError::new(
|
||||
assert_eq!(
|
||||
error,
|
||||
ValidationError(vec![RuleError::new(
|
||||
r#"Variable "$input" got invalid value. Expected "TestInputObject", found not an object."#,
|
||||
&[SourcePosition::new(8, 0, 8)],
|
||||
),
|
||||
]));
|
||||
),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -366,16 +385,19 @@ fn variable_multiple_errors_with_nesting() {
|
|||
|
||||
let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err();
|
||||
|
||||
assert_eq!(error, ValidationError(vec![
|
||||
RuleError::new(
|
||||
r#"Variable "$input" got invalid value. In field "na": In field "c": Expected "String!", found null."#,
|
||||
&[SourcePosition::new(8, 0, 8)],
|
||||
),
|
||||
RuleError::new(
|
||||
r#"Variable "$input" got invalid value. In field "nb": Expected "String!", found null."#,
|
||||
&[SourcePosition::new(8, 0, 8)],
|
||||
),
|
||||
]));
|
||||
assert_eq!(
|
||||
error,
|
||||
ValidationError(vec![
|
||||
RuleError::new(
|
||||
r#"Variable "$input" got invalid value. In field "na": In field "c": Expected "String!", found null."#,
|
||||
&[SourcePosition::new(8, 0, 8)],
|
||||
),
|
||||
RuleError::new(
|
||||
r#"Variable "$input" got invalid value. In field "nb": Expected "String!", found null."#,
|
||||
&[SourcePosition::new(8, 0, 8)],
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -733,12 +755,13 @@ fn does_not_allow_lists_of_non_null_to_contain_null() {
|
|||
|
||||
let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err();
|
||||
|
||||
assert_eq!(error, ValidationError(vec![
|
||||
RuleError::new(
|
||||
assert_eq!(
|
||||
error,
|
||||
ValidationError(vec![RuleError::new(
|
||||
r#"Variable "$input" got invalid value. In element #1: Expected "String!", found null."#,
|
||||
&[SourcePosition::new(8, 0, 8)],
|
||||
),
|
||||
]));
|
||||
),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -759,12 +782,13 @@ fn does_not_allow_non_null_lists_of_non_null_to_contain_null() {
|
|||
|
||||
let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err();
|
||||
|
||||
assert_eq!(error, ValidationError(vec![
|
||||
RuleError::new(
|
||||
assert_eq!(
|
||||
error,
|
||||
ValidationError(vec![RuleError::new(
|
||||
r#"Variable "$input" got invalid value. In element #1: Expected "String!", found null."#,
|
||||
&[SourcePosition::new(8, 0, 8)],
|
||||
),
|
||||
]));
|
||||
),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -820,12 +844,13 @@ fn does_not_allow_invalid_types_to_be_used_as_values() {
|
|||
|
||||
let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err();
|
||||
|
||||
assert_eq!(error, ValidationError(vec![
|
||||
RuleError::new(
|
||||
assert_eq!(
|
||||
error,
|
||||
ValidationError(vec![RuleError::new(
|
||||
r#"Variable "$input" expected value of type "TestType!" which cannot be used as an input type."#,
|
||||
&[SourcePosition::new(8, 0, 8)],
|
||||
),
|
||||
]));
|
||||
),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -842,12 +867,13 @@ fn does_not_allow_unknown_types_to_be_used_as_values() {
|
|||
|
||||
let error = crate::execute(query, None, &schema, &vars, &()).unwrap_err();
|
||||
|
||||
assert_eq!(error, ValidationError(vec![
|
||||
RuleError::new(
|
||||
assert_eq!(
|
||||
error,
|
||||
ValidationError(vec![RuleError::new(
|
||||
r#"Variable "$input" expected value of type "UnknownType!" which cannot be used as an input type."#,
|
||||
&[SourcePosition::new(8, 0, 8)],
|
||||
),
|
||||
]));
|
||||
),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -41,7 +41,8 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
|
|||
</script>
|
||||
"#;
|
||||
|
||||
format!(r#"
|
||||
format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
@ -62,5 +63,6 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
|
|||
"#,
|
||||
graphql_url = graphql_endpoint_url,
|
||||
stylesheet_source = stylesheet_source,
|
||||
fetcher_source = fetcher_source)
|
||||
fetcher_source = fetcher_source
|
||||
)
|
||||
}
|
||||
|
|
|
@ -450,7 +450,8 @@ mod tests {
|
|||
to_string(&ExecutionError::at_origin(FieldError::new(
|
||||
"foo error",
|
||||
Value::Object(obj),
|
||||
))).unwrap(),
|
||||
)))
|
||||
.unwrap(),
|
||||
r#"{"message":"foo error","locations":[{"line":1,"column":1}],"path":[],"extensions":{"foo":"bar"}}"#
|
||||
);
|
||||
}
|
||||
|
|
|
@ -111,12 +111,14 @@ extern crate uuid;
|
|||
// functionality automatically.
|
||||
pub use juniper_codegen::{
|
||||
object, GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLScalarValue, ScalarValue,
|
||||
union,
|
||||
};
|
||||
// Internal macros are not exported,
|
||||
// but declared at the root to make them easier to use.
|
||||
#[allow(unused_imports)]
|
||||
use juniper_codegen::{
|
||||
object_internal, GraphQLEnumInternal, GraphQLInputObjectInternal, GraphQLScalarValueInternal,
|
||||
union_internal,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
|
|
|
@ -95,6 +95,8 @@ impl Root {
|
|||
Ok(0)
|
||||
}
|
||||
|
||||
/*
|
||||
* FIXME: make this work again
|
||||
fn with_return() -> i32 {
|
||||
return 0;
|
||||
}
|
||||
|
@ -102,6 +104,7 @@ impl Root {
|
|||
fn with_return_field_result() -> FieldResult<i32> {
|
||||
return Ok(0);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
graphql_interface!(Interface: () |&self| {
|
||||
|
|
|
@ -1,12 +1,3 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use crate::{
|
||||
ast::InputValue,
|
||||
schema::model::RootNode,
|
||||
types::scalars::EmptyMutation,
|
||||
value::{DefaultScalarValue, Object, Value},
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
Syntax to validate:
|
||||
|
@ -16,8 +7,18 @@ Syntax to validate:
|
|||
* Custom name vs. default name
|
||||
* Optional commas between items
|
||||
* Optional trailing commas on instance resolvers
|
||||
*
|
||||
*/
|
||||
|
||||
*/
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::{
|
||||
ast::InputValue,
|
||||
schema::model::RootNode,
|
||||
types::scalars::EmptyMutation,
|
||||
value::{DefaultScalarValue, Object, Value},
|
||||
};
|
||||
|
||||
struct Concrete;
|
||||
|
||||
|
@ -35,16 +36,6 @@ enum WithGenerics<T> {
|
|||
enum DescriptionFirst {
|
||||
Concrete(Concrete),
|
||||
}
|
||||
enum ResolversFirst {
|
||||
Concrete(Concrete),
|
||||
}
|
||||
|
||||
enum CommasWithTrailing {
|
||||
Concrete(Concrete),
|
||||
}
|
||||
enum ResolversWithTrailingComma {
|
||||
Concrete(Concrete),
|
||||
}
|
||||
|
||||
struct Root;
|
||||
|
||||
|
@ -55,51 +46,41 @@ impl Concrete {
|
|||
}
|
||||
}
|
||||
|
||||
graphql_union!(CustomName: () as "ACustomNamedUnion" |&self| {
|
||||
instance_resolvers: |&_| {
|
||||
&Concrete => match *self { CustomName::Concrete(ref c) => Some(c) }
|
||||
#[crate::union_internal(name = "ACustomNamedUnion")]
|
||||
impl CustomName {
|
||||
fn resolve(&self) {
|
||||
match self {
|
||||
Concrete => match *self { CustomName::Concrete(ref c) => Some(c) },
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graphql_union!(<'a> WithLifetime<'a>: () as "WithLifetime" |&self| {
|
||||
instance_resolvers: |&_| {
|
||||
Concrete => match *self { WithLifetime::Int(_) => Some(Concrete) }
|
||||
#[crate::union_internal]
|
||||
impl<'a> WithLifetime<'a>{
|
||||
fn resolve(&self) {
|
||||
match self {
|
||||
Concrete => match *self { WithLifetime::Int(_) => Some(&Concrete) },
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graphql_union!(<T> WithGenerics<T>: () as "WithGenerics" |&self| {
|
||||
instance_resolvers: |&_| {
|
||||
Concrete => match *self { WithGenerics::Generic(_) => Some(Concrete) }
|
||||
#[crate::union_internal]
|
||||
impl<T> WithGenerics<T> {
|
||||
fn resolve(&self) {
|
||||
match self {
|
||||
Concrete => match *self { WithGenerics::Generic(_) => Some(&Concrete) }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graphql_union!(DescriptionFirst: () |&self| {
|
||||
description: "A description"
|
||||
instance_resolvers: |&_| {
|
||||
&Concrete => match *self { DescriptionFirst::Concrete(ref c) => Some(c) }
|
||||
#[crate::union_internal(description = "A description")]
|
||||
impl DescriptionFirst {
|
||||
fn resolve(&self) {
|
||||
match self {
|
||||
Concrete => match *self { DescriptionFirst::Concrete(ref c) => Some(c) },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graphql_union!(ResolversFirst: () |&self| {
|
||||
instance_resolvers: |&_| {
|
||||
&Concrete => match *self { ResolversFirst::Concrete(ref c) => Some(c) }
|
||||
}
|
||||
description: "A description"
|
||||
});
|
||||
|
||||
graphql_union!(CommasWithTrailing: () |&self| {
|
||||
instance_resolvers: |&_| {
|
||||
&Concrete => match *self { CommasWithTrailing::Concrete(ref c) => Some(c) }
|
||||
},
|
||||
description: "A description",
|
||||
});
|
||||
|
||||
graphql_union!(ResolversWithTrailingComma: () |&self| {
|
||||
instance_resolvers: |&_| {
|
||||
&Concrete => match *self { ResolversWithTrailingComma::Concrete(ref c) => Some(c) },
|
||||
}
|
||||
description: "A description"
|
||||
});
|
||||
}
|
||||
|
||||
#[crate::object_internal]
|
||||
impl<'a> Root {
|
||||
|
@ -115,15 +96,6 @@ impl<'a> Root {
|
|||
fn description_first() -> DescriptionFirst {
|
||||
DescriptionFirst::Concrete(Concrete)
|
||||
}
|
||||
fn resolvers_first() -> ResolversFirst {
|
||||
ResolversFirst::Concrete(Concrete)
|
||||
}
|
||||
fn commas_with_trailing() -> CommasWithTrailing {
|
||||
CommasWithTrailing::Concrete(Concrete)
|
||||
}
|
||||
fn resolvers_with_trailing_comma() -> ResolversWithTrailingComma {
|
||||
ResolversWithTrailingComma::Concrete(Concrete)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_type_info_query<F>(type_name: &str, f: F)
|
||||
|
@ -240,62 +212,3 @@ fn introspect_description_first() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn introspect_resolvers_first() {
|
||||
run_type_info_query("ResolversFirst", |union, possible_types| {
|
||||
assert_eq!(
|
||||
union.get_field_value("name"),
|
||||
Some(&Value::scalar("ResolversFirst"))
|
||||
);
|
||||
assert_eq!(
|
||||
union.get_field_value("description"),
|
||||
Some(&Value::scalar("A description"))
|
||||
);
|
||||
|
||||
assert!(possible_types.contains(&Value::object(
|
||||
vec![("name", Value::scalar("Concrete"))]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn introspect_commas_with_trailing() {
|
||||
run_type_info_query("CommasWithTrailing", |union, possible_types| {
|
||||
assert_eq!(
|
||||
union.get_field_value("name"),
|
||||
Some(&Value::scalar("CommasWithTrailing"))
|
||||
);
|
||||
assert_eq!(
|
||||
union.get_field_value("description"),
|
||||
Some(&Value::scalar("A description"))
|
||||
);
|
||||
|
||||
assert!(possible_types.contains(&Value::object(
|
||||
vec![("name", Value::scalar("Concrete"))]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn introspect_resolvers_with_trailing_comma() {
|
||||
run_type_info_query("ResolversWithTrailingComma", |union, possible_types| {
|
||||
assert_eq!(
|
||||
union.get_field_value("name"),
|
||||
Some(&Value::scalar("ResolversWithTrailingComma"))
|
||||
);
|
||||
assert_eq!(
|
||||
union.get_field_value("description"),
|
||||
Some(&Value::scalar("A description"))
|
||||
);
|
||||
|
||||
assert!(possible_types.contains(&Value::object(
|
||||
vec![("name", Value::scalar("Concrete"))]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/*
|
||||
*
|
||||
/**
|
||||
Expose GraphQL unions
|
||||
|
||||
|
@ -17,6 +19,9 @@ resolvers.
|
|||
[1]: macro.graphql_object!.html
|
||||
[2]: macro.graphql_interface!.html
|
||||
*/
|
||||
|
||||
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! graphql_union {
|
||||
|
||||
|
@ -135,3 +140,4 @@ macro_rules! graphql_union {
|
|||
);
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -234,12 +234,13 @@ fn test_introspection_possible_types() {
|
|||
assert_eq!(possible_types, vec!["Human", "Droid"].into_iter().collect());
|
||||
}
|
||||
|
||||
/*
|
||||
* FIXME: make this work again
|
||||
#[test]
|
||||
fn test_builtin_introspection_query() {
|
||||
let database = Database::new();
|
||||
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
|
||||
|
||||
let mut result = crate::introspect(&schema, &database, IntrospectionFormat::default()).unwrap();
|
||||
let mut result = crate::introspect(&schema, &database, IntrospectionFormat::default()).unwrap();
|
||||
sort_schema_value(&mut result.0);
|
||||
let expected = schema_introspection_result();
|
||||
assert_eq!(result, (expected, vec![]));
|
||||
|
@ -257,3 +258,4 @@ fn test_builtin_introspection_query_without_descriptions() {
|
|||
|
||||
assert_eq!(result, (expected, vec![]));
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -52,7 +52,8 @@ fn no_allowed_error_message(field_name: &str, type_name: &str) -> String {
|
|||
fn required_error_message(field_name: &str, type_name: &str) -> String {
|
||||
format!(
|
||||
r#"Field "{}" of type "{}" must have a selection of subfields. Did you mean "{} {{ ... }}"?"#,
|
||||
field_name, type_name, field_name)
|
||||
field_name, type_name, field_name
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -21,6 +21,7 @@ async = []
|
|||
proc-macro2 = "1.0.1"
|
||||
syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] }
|
||||
quote = "1.0.2"
|
||||
proc-macro-error = "0.3.4"
|
||||
|
||||
[dev-dependencies]
|
||||
juniper = { version = "0.14.0", path = "../juniper" }
|
||||
|
|
|
@ -41,28 +41,10 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
|
|||
}
|
||||
}
|
||||
|
||||
let name = match impl_attrs.name.as_ref() {
|
||||
Some(type_name) => type_name.clone(),
|
||||
None => {
|
||||
let error_msg = "Could not determine a name for the object type: specify one with #[juniper::object(name = \"SomeName\")";
|
||||
|
||||
let path = match &*_impl.self_ty {
|
||||
syn::Type::Path(ref type_path) => &type_path.path,
|
||||
syn::Type::Reference(ref reference) => match &*reference.elem {
|
||||
syn::Type::Path(ref type_path) => &type_path.path,
|
||||
syn::Type::TraitObject(ref trait_obj) => {
|
||||
match trait_obj.bounds.iter().nth(0).unwrap() {
|
||||
syn::TypeParamBound::Trait(ref trait_bound) => &trait_bound.path,
|
||||
_ => panic!(error_msg),
|
||||
}
|
||||
}
|
||||
_ => panic!(error_msg),
|
||||
},
|
||||
_ => panic!(error_msg),
|
||||
};
|
||||
|
||||
path.segments.iter().last().unwrap().ident.to_string()
|
||||
}
|
||||
let name = if let Some(ident) = util::name_of_type(&*_impl.self_ty) {
|
||||
ident
|
||||
} else {
|
||||
panic!("Could not determine a name for the object type: specify one with #[juniper::object(name = \"SomeName\")");
|
||||
};
|
||||
|
||||
let target_type = *_impl.self_ty.clone();
|
||||
|
@ -72,7 +54,7 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
|
|||
.or(util::get_doc_comment(&_impl.attrs));
|
||||
|
||||
let mut definition = util::GraphQLTypeDefiniton {
|
||||
name,
|
||||
name: name.to_string(),
|
||||
_type: target_type.clone(),
|
||||
context: impl_attrs.context,
|
||||
scalar: impl_attrs.scalar,
|
||||
|
|
209
juniper_codegen/src/impl_union.rs
Normal file
209
juniper_codegen/src/impl_union.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
use proc_macro::TokenStream;
|
||||
|
||||
use proc_macro_error::MacroError;
|
||||
use quote::{quote};
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
use crate::util;
|
||||
|
||||
struct ResolverVariant {
|
||||
pub ty: syn::Type,
|
||||
pub resolver: syn::Expr,
|
||||
}
|
||||
|
||||
struct ResolveBody {
|
||||
pub variants: Vec<ResolverVariant>,
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for ResolveBody {
|
||||
fn parse(input: syn::parse::ParseStream) -> Result<Self, syn::parse::Error> {
|
||||
input.parse::<syn::token::Fn>()?;
|
||||
let ident = input.parse::<syn::Ident>()?;
|
||||
if ident != "resolve" {
|
||||
return Err(input.error("Expected method named 'resolve'"));
|
||||
}
|
||||
|
||||
let args;
|
||||
syn::parenthesized!(args in input);
|
||||
args.parse::<syn::token::And>()?;
|
||||
args.parse::<syn::token::SelfValue>()?;
|
||||
if !args.is_empty() {
|
||||
return Err(
|
||||
input.error("Unexpected extra tokens: only one '&self' parameter is allowed")
|
||||
);
|
||||
}
|
||||
|
||||
let body;
|
||||
syn::braced!( body in input );
|
||||
|
||||
body.parse::<syn::token::Match>()?;
|
||||
body.parse::<syn::token::SelfValue>()?;
|
||||
|
||||
let match_body;
|
||||
syn::braced!( match_body in body );
|
||||
|
||||
let mut variants = Vec::new();
|
||||
while !match_body.is_empty() {
|
||||
let ty = match_body.parse::<syn::Type>()?;
|
||||
match_body.parse::<syn::token::FatArrow>()?;
|
||||
let resolver = match_body.parse::<syn::Expr>()?;
|
||||
|
||||
variants.push(ResolverVariant { ty, resolver });
|
||||
|
||||
// Optinal trailing comma.
|
||||
match_body.parse::<syn::token::Comma>().ok();
|
||||
}
|
||||
|
||||
if !body.is_empty() {
|
||||
return Err(input.error("Unexpected input"));
|
||||
}
|
||||
|
||||
Ok(Self { variants })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn impl_union(
|
||||
is_internal: bool,
|
||||
attrs: TokenStream,
|
||||
body: TokenStream,
|
||||
) -> Result<TokenStream, MacroError> {
|
||||
|
||||
// We are re-using the object attributes since they are almost the same.
|
||||
let attrs = syn::parse::<util::ObjectAttributes>(attrs)?;
|
||||
|
||||
let item = syn::parse::<syn::ItemImpl>(body)?;
|
||||
|
||||
if item.items.len() != 1 {
|
||||
return Err(MacroError::new(
|
||||
item.span(),
|
||||
"Invalid impl body: expected one method with signature: fn resolve(&self) { ... }".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let body_item = item.items.first().unwrap();
|
||||
let body = quote! { #body_item };
|
||||
let variants = syn::parse::<ResolveBody>(body.into())?.variants;
|
||||
|
||||
let ty = &item.self_ty;
|
||||
|
||||
let ty_ident = util::name_of_type(&*ty).ok_or_else(|| {
|
||||
MacroError::new(
|
||||
ty.span(),
|
||||
"Expected a path ending in a simple type identifier".to_string(),
|
||||
)
|
||||
})?;
|
||||
let name = attrs.name.unwrap_or_else(|| ty_ident.to_string());
|
||||
|
||||
let juniper = util::juniper_path(is_internal);
|
||||
|
||||
let meta_types = variants.iter().map(|var| {
|
||||
let var_ty = &var.ty;
|
||||
|
||||
quote! {
|
||||
registry.get_type::<&#var_ty>(&(())),
|
||||
}
|
||||
});
|
||||
|
||||
let concrete_type_resolver = variants.iter().map(|var| {
|
||||
let var_ty = &var.ty;
|
||||
let resolve = &var.resolver;
|
||||
|
||||
quote! {
|
||||
if ({#resolve} as std::option::Option<&#var_ty>).is_some() {
|
||||
return <#var_ty as #juniper::GraphQLType<_>>::name(&()).unwrap().to_string();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let resolve_into_type = variants.iter().map(|var| {
|
||||
let var_ty = &var.ty;
|
||||
let resolve = &var.resolver;
|
||||
|
||||
quote! {
|
||||
if type_name == (<#var_ty as #juniper::GraphQLType<_>>::name(&())).unwrap() {
|
||||
return executor.resolve(&(), &{ #resolve });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let scalar = attrs
|
||||
.scalar
|
||||
.as_ref()
|
||||
.map(|s| quote!( #s ))
|
||||
.unwrap_or_else(|| { quote! { #juniper::DefaultScalarValue } });
|
||||
|
||||
let mut generics = item.generics.clone();
|
||||
if attrs.scalar.is_some() {
|
||||
// A custom scalar type was specified.
|
||||
// Therefore, we always insert a where clause that marks the scalar as
|
||||
// compatible with ScalarValueRef.
|
||||
// This is done to prevent the user from having to specify this
|
||||
// manually.
|
||||
let where_clause = generics.where_clause.get_or_insert(syn::parse_quote!(where));
|
||||
where_clause.predicates.push(
|
||||
syn::parse_quote!(for<'__b> &'__b #scalar: #juniper::ScalarRefValue<'__b>),
|
||||
);
|
||||
}
|
||||
|
||||
let (impl_generics, _, where_clause) = generics.split_for_impl();
|
||||
|
||||
let description = match attrs.description.as_ref() {
|
||||
Some(value) => quote!( .description( #value ) ),
|
||||
None => quote!(),
|
||||
};
|
||||
let context = attrs.context.map(|c| quote!{ #c } ).unwrap_or_else(|| quote!{ () });
|
||||
|
||||
let output = quote! {
|
||||
impl #impl_generics #juniper::GraphQLType<#scalar> for #ty #where_clause
|
||||
{
|
||||
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
|
||||
for<'__b> &'__b #scalar: #juniper::ScalarRefValue<'__b>,
|
||||
#scalar: 'r,
|
||||
{
|
||||
let types = &[
|
||||
#( #meta_types )*
|
||||
];
|
||||
registry.build_union_type::<#ty>(
|
||||
info, types
|
||||
)
|
||||
#description
|
||||
.into_meta()
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn concrete_type_name(&self, context: &Self::Context, _info: &Self::TypeInfo) -> String {
|
||||
#( #concrete_type_resolver )*
|
||||
|
||||
panic!("Concrete type not handled by instance resolvers on {}", #name);
|
||||
}
|
||||
|
||||
fn resolve_into_type(
|
||||
&self,
|
||||
_info: &Self::TypeInfo,
|
||||
type_name: &str,
|
||||
_: Option<&[#juniper::Selection<#scalar>]>,
|
||||
executor: &#juniper::Executor<Self::Context, #scalar>,
|
||||
) -> #juniper::ExecutionResult<#scalar> {
|
||||
let context = &executor.context();
|
||||
|
||||
#( #resolve_into_type )*
|
||||
|
||||
panic!("Concrete type not handled by instance resolvers on {}", #name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
Ok(output.into())
|
||||
}
|
|
@ -9,12 +9,14 @@
|
|||
|
||||
extern crate proc_macro;
|
||||
|
||||
mod util;
|
||||
|
||||
mod derive_enum;
|
||||
mod derive_input_object;
|
||||
mod derive_object;
|
||||
mod derive_scalar_value;
|
||||
mod impl_object;
|
||||
mod util;
|
||||
mod impl_union;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
|
@ -366,3 +368,25 @@ pub fn object_internal(args: TokenStream, input: TokenStream) -> TokenStream {
|
|||
let gen = impl_object::build_object(args, input, true);
|
||||
gen.into()
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
pub fn union(attrs: TokenStream, body: TokenStream) -> TokenStream {
|
||||
let output = match impl_union::impl_union(false, attrs, body) {
|
||||
Ok(toks) => toks,
|
||||
Err(err) => proc_macro_error::abort!(err),
|
||||
};
|
||||
output
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[proc_macro_attribute]
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
pub fn union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream {
|
||||
let output = match impl_union::impl_union(true, attrs, body) {
|
||||
Ok(toks) => toks,
|
||||
Err(err) => proc_macro_error::abort!(err),
|
||||
};
|
||||
output
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,36 @@ use syn::{
|
|||
NestedMeta, Token,
|
||||
};
|
||||
|
||||
pub fn juniper_path(is_internal: bool) -> syn::Path {
|
||||
let name = if is_internal { "crate" } else { "juniper" };
|
||||
syn::parse_str::<syn::Path>(name).unwrap()
|
||||
}
|
||||
|
||||
/// Returns the name of a type.
|
||||
/// If the type does not end in a simple ident, `None` is returned.
|
||||
pub fn name_of_type(ty: &syn::Type) -> Option<syn::Ident> {
|
||||
let path_opt = match ty {
|
||||
syn::Type::Path(ref type_path) => Some(&type_path.path),
|
||||
syn::Type::Reference(ref reference) => match &*reference.elem {
|
||||
syn::Type::Path(ref type_path) => Some(&type_path.path),
|
||||
syn::Type::TraitObject(ref trait_obj) => {
|
||||
match trait_obj.bounds.iter().nth(0).unwrap() {
|
||||
syn::TypeParamBound::Trait(ref trait_bound) => Some(&trait_bound.path),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
let path = path_opt?;
|
||||
|
||||
path.segments
|
||||
.iter()
|
||||
.last()
|
||||
.map(|segment| segment.ident.clone())
|
||||
}
|
||||
|
||||
/// Compares a path to a one-segment string value,
|
||||
/// return true if equal.
|
||||
pub fn path_eq_single(path: &syn::Path, value: &str) -> bool {
|
||||
|
|
Loading…
Reference in a new issue