From 301853ab8acb169a7b4da5999c785e3bee8a4ce2 Mon Sep 17 00:00:00 2001 From: Alexander Lyon Date: Fri, 17 Apr 2020 17:44:41 +0100 Subject: [PATCH 01/16] Add support for GraphQL Schema Language --- README.md | 4 +- juniper/Cargo.toml | 4 + juniper/src/schema/meta.rs | 32 ++ juniper/src/schema/mod.rs | 1 + juniper/src/schema/model.rs | 126 +++++++- .../src/schema/translate/graphql_parser.rs | 306 ++++++++++++++++++ juniper/src/schema/translate/mod.rs | 8 + 7 files changed, 476 insertions(+), 5 deletions(-) create mode 100644 juniper/src/schema/translate/graphql_parser.rs create mode 100644 juniper/src/schema/translate/mod.rs diff --git a/README.md b/README.md index 24a661d2c..68a987943 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,7 @@ see the [actix][actix_examples], [hyper][hyper_examples], [rocket][rocket_exampl Juniper supports the full GraphQL query language according to the [specification][graphql_spec], including interfaces, unions, schema -introspection, and validations. -It does not, however, support the schema language. Consider using [juniper-from-schema][] for generating code from a schema file. +introspection, and validations. It can also output the schema in the [GraphQL Schema Language][schema_language]. As an exception to other GraphQL libraries for other languages, Juniper builds non-null types by default. A field of type `Vec` will be converted into @@ -91,6 +90,7 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected. [playground]: https://github.com/prisma/graphql-playground [iron]: http://ironframework.io [graphql_spec]: http://facebook.github.io/graphql +[schema_language]: https://graphql.org/learn/schema/#type-language [test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs [tokio]: https://github.com/tokio-rs/tokio [actix_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_actix/examples diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index d41af35cc..1cfd23899 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -25,11 +25,14 @@ path = "benches/bench.rs" [features] expose-test-schema = ["serde_json"] +schema-language = ["graphql-parser-integration"] +graphql-parser-integration = ["graphql-parser"] default = [ "bson", "chrono", "url", "uuid", + "schema-language", ] scalar-naivetime = [] @@ -46,6 +49,7 @@ serde_json = { version="1.0.2", optional = true } static_assertions = "1.1" url = { version = "2", optional = true } uuid = { version = "0.8", optional = true } +graphql-parser = {version = "0.3.0", optional = true } [dev-dependencies] bencher = "0.1.2" diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index b024ed05e..777e3b1ee 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -169,6 +169,14 @@ pub struct Field<'a, S> { pub deprecation_status: DeprecationStatus, } +impl<'a, S> Field<'a, S> { + /// Returns true if the type is built-in to GraphQL. + pub fn is_builtin(&self) -> bool { + // "used exclusively by GraphQL’s introspection system" + self.name.starts_with("__") + } +} + /// Metadata for an argument to a field #[derive(Debug, Clone)] pub struct Argument<'a, S> { @@ -182,6 +190,14 @@ pub struct Argument<'a, S> { pub default_value: Option>, } +impl<'a, S> Argument<'a, S> { + /// Returns true if the type is built-in to GraphQL. + pub fn is_builtin(&self) -> bool { + // "used exclusively by GraphQL’s introspection system" + self.name.starts_with("__") + } +} + /// Metadata for a single value in an enum #[derive(Debug, Clone)] pub struct EnumValue { @@ -368,6 +384,22 @@ impl<'a, S> MetaType<'a, S> { } } + /// Returns true if the type is built-in to GraphQL. + pub fn is_builtin(&self) -> bool { + if let Some(name) = self.name() { + // "used exclusively by GraphQL’s introspection system" + { + name.starts_with("__") || + // + name == "Boolean" || name == "String" || name == "Int" || name == "Float" || name == "ID" || + // Our custom empty mutation marker + name == "_EmptyMutation" || name == "_EmptySubscription" + } + } else { + false + } + } + pub(crate) fn fields<'b>(&self, schema: &'b SchemaType) -> Option>> { schema .lookup_type(&self.as_type()) diff --git a/juniper/src/schema/mod.rs b/juniper/src/schema/mod.rs index 7f1658a93..d174e938b 100644 --- a/juniper/src/schema/mod.rs +++ b/juniper/src/schema/mod.rs @@ -3,3 +3,4 @@ pub mod meta; pub mod model; pub mod schema; +pub mod translate; diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index 9eb747670..a72ce507f 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -2,12 +2,14 @@ use std::fmt; use fnv::FnvHashMap; +use graphql_parser::schema::Document; use juniper_codegen::GraphQLEnumInternal as GraphQLEnum; use crate::{ ast::Type, executor::{Context, Registry}, schema::meta::{Argument, InterfaceMeta, MetaType, ObjectMeta, PlaceholderMeta, UnionMeta}, + schema::translate::{graphql_parser::GraphQLParserTranslator, SchemaTranslator}, types::{base::GraphQLType, name::Name}, value::{DefaultScalarValue, ScalarValue}, }; @@ -46,9 +48,9 @@ pub struct RootNode< #[derive(Debug)] pub struct SchemaType<'a, S> { pub(crate) types: FnvHashMap>, - query_type_name: String, - mutation_type_name: Option, - subscription_type_name: Option, + pub(crate) query_type_name: String, + pub(crate) mutation_type_name: Option, + pub(crate) subscription_type_name: Option, directives: FnvHashMap>, } @@ -102,6 +104,22 @@ where ) -> Self { RootNode::new_with_info(query_obj, mutation_obj, subscription_obj, (), (), ()) } + + #[cfg(feature = "schema-language")] + /// The schema definition as a `String` in the + /// [GraphQL Schema Language](https://graphql.org/learn/schema/#type-language) + /// format. + pub fn as_schema_language(&self) -> String { + let doc = self.as_parser_document(); + format!("{}", doc) + } + + #[cfg(feature = "graphql-parser-integration")] + /// The schema definition as a [`graphql_parser`](https://crates.io/crates/graphql-parser) + /// [`Document`](https://docs.rs/graphql-parser/latest/graphql_parser/schema/struct.Document.html). + pub fn as_parser_document(&'a self) -> Document<'a, &'a str> { + GraphQLParserTranslator::translate_schema(&self.schema) + } } impl<'a, S, QueryT, MutationT, SubscriptionT> RootNode<'a, QueryT, MutationT, SubscriptionT, S> @@ -534,3 +552,105 @@ impl<'a, S> fmt::Display for TypeType<'a, S> { } } } + +#[cfg(test)] +mod test { + + #[cfg(feature = "graphql-parser-integration")] + mod graphql_parser_integration { + use crate as juniper; + use crate::{EmptyMutation, EmptySubscription}; + + #[test] + fn graphql_parser_doc() { + struct Query; + #[juniper::graphql_object] + impl Query { + fn blah() -> bool { + true + } + }; + let schema = crate::RootNode::new( + Query, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + let ast = graphql_parser::parse_schema::<&str>( + r#" + type Query { + blah: Boolean! + } + + schema { + query: Query + } + "#, + ) + .unwrap(); + assert_eq!( + format!("{}", ast), + format!("{}", schema.as_parser_document()), + ); + } + } + + #[cfg(feature = "schema-language")] + mod schema_language { + use crate as juniper; + use crate::{EmptyMutation, EmptySubscription}; + + #[test] + fn schema_language() { + struct Query; + #[juniper::graphql_object] + impl Query { + fn blah() -> bool { + true + } + /// This is whatever's description. + fn whatever() -> String { + "foo".to_string() + } + fn fizz(buzz: String) -> Option<&str> { + None + } + fn arr(stuff: Vec) -> Option<&str> { + None + } + #[deprecated] + fn old() -> i32 { + 42 + } + #[deprecated(note = "This field is deprecated, use another.")] + fn really_old() -> f64 { + 42.0 + } + }; + + let schema = crate::RootNode::new( + Query, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + let ast = graphql_parser::parse_schema::<&str>( + r#" + type Query { + blah: Boolean! + "This is whatever's description." + whatever: String! + fizz(buzz: String!): String + arr(stuff: [String!]!): String + old: Int! @deprecated + reallyOld: Float! @deprecated(reason: "This field is deprecated, use another.") + } + + schema { + query: Query + } + "#, + ) + .unwrap(); + assert_eq!(format!("{}", ast), schema.as_schema_language()); + } + } +} diff --git a/juniper/src/schema/translate/graphql_parser.rs b/juniper/src/schema/translate/graphql_parser.rs new file mode 100644 index 000000000..de7da1d9f --- /dev/null +++ b/juniper/src/schema/translate/graphql_parser.rs @@ -0,0 +1,306 @@ +use std::boxed::Box; +use std::collections::BTreeMap; + +use graphql_parser::query::{ + Directive as ExternalDirective, Number as ExternalNumber, Type as ExternalType, +}; +use graphql_parser::schema::{Definition, Document, SchemaDefinition, Text}; +use graphql_parser::schema::{ + EnumType as ExternalEnum, EnumValue as ExternalEnumValue, Field as ExternalField, + InputObjectType as ExternalInputObjectType, InputValue as ExternalInputValue, + InterfaceType as ExternalInterfaceType, ObjectType as ExternalObjectType, + ScalarType as ExternalScalarType, TypeDefinition as ExternalTypeDefinition, + UnionType as ExternalUnionType, Value as ExternalValue, +}; +use graphql_parser::Pos; + +use crate::ast::{InputValue, Type}; +use crate::schema::meta::DeprecationStatus; +use crate::schema::meta::{Argument, EnumValue, Field, MetaType}; +use crate::schema::model::SchemaType; +use crate::schema::translate::SchemaTranslator; +use crate::value::ScalarValue; + +pub struct GraphQLParserTranslator; + +impl<'a, S: 'a, T> From<&'a SchemaType<'a, S>> for Document<'a, T> +where + S: ScalarValue, + T: Text<'a> + Default, +{ + fn from(input: &'a SchemaType<'a, S>) -> Document<'a, T> { + GraphQLParserTranslator::translate_schema(input) + } +} + +impl<'a, T> SchemaTranslator<'a, graphql_parser::schema::Document<'a, T>> + for GraphQLParserTranslator +where + T: Text<'a> + Default, +{ + fn translate_schema(input: &'a SchemaType) -> graphql_parser::schema::Document<'a, T> + where + S: ScalarValue, + { + let mut doc = Document::default(); + + // Translate type defs. + let mut types = input + .types + .iter() + .filter(|(_, meta)| !meta.is_builtin()) + .map(|(_, meta)| GraphQLParserTranslator::translate_meta(meta)) + .map(Definition::TypeDefinition) + .collect(); + doc.definitions.append(&mut types); + + doc.definitions + .push(Definition::SchemaDefinition(SchemaDefinition { + position: Pos::default(), + directives: vec![], + query: Some(From::from(input.query_type_name.as_str())), + mutation: input + .mutation_type_name + .as_ref() + .map(|s| From::from(s.as_str())), + subscription: input + .subscription_type_name + .as_ref() + .map(|s| From::from(s.as_str())), + })); + + doc + } +} + +impl GraphQLParserTranslator { + fn translate_argument<'a, S, T>(input: &'a Argument) -> ExternalInputValue<'a, T> + where + S: ScalarValue, + T: Text<'a>, + { + ExternalInputValue { + position: Pos::default(), + description: input.description.as_ref().map(From::from), + name: From::from(input.name.as_str()), + value_type: GraphQLParserTranslator::translate_type(&input.arg_type), + default_value: input + .default_value + .as_ref() + .map(|x| GraphQLParserTranslator::translate_value(x)), + directives: vec![], + } + } + + fn translate_value<'a, S: 'a, T>(input: &'a InputValue) -> ExternalValue<'a, T> + where + S: ScalarValue, + T: Text<'a>, + { + match input { + InputValue::Null => ExternalValue::Null, + InputValue::Scalar(x) => { + if let Some(v) = x.as_string() { + ExternalValue::String(v) + } else if let Some(v) = x.as_int() { + ExternalValue::Int(ExternalNumber::from(v)) + } else if let Some(v) = x.as_float() { + ExternalValue::Float(v) + } else if let Some(v) = x.as_boolean() { + ExternalValue::Boolean(v) + } else { + panic!("unknown argument type") + } + } + InputValue::Enum(x) => ExternalValue::Enum(From::from(x.as_str())), + InputValue::Variable(x) => ExternalValue::Variable(From::from(x.as_str())), + InputValue::List(x) => ExternalValue::List( + x.iter() + .map(|s| GraphQLParserTranslator::translate_value(&s.item)) + .collect(), + ), + InputValue::Object(x) => { + let mut fields = BTreeMap::new(); + x.iter().for_each(|(name_span, value_span)| { + fields.insert( + From::from(name_span.item.as_str()), + GraphQLParserTranslator::translate_value(&value_span.item), + ); + }); + ExternalValue::Object(fields) + } + } + } + + fn translate_type<'a, T>(input: &'a Type<'a>) -> ExternalType<'a, T> + where + T: Text<'a>, + { + match input { + Type::Named(x) => ExternalType::NamedType(From::from(x.as_ref())), + Type::List(x) => { + ExternalType::ListType(GraphQLParserTranslator::translate_type(x).into()) + } + Type::NonNullNamed(x) => { + ExternalType::NonNullType(Box::new(ExternalType::NamedType(From::from(x.as_ref())))) + } + Type::NonNullList(x) => ExternalType::NonNullType(Box::new(ExternalType::ListType( + Box::new(GraphQLParserTranslator::translate_type(x)), + ))), + } + } + + fn translate_meta<'a, S, T>(input: &'a MetaType) -> ExternalTypeDefinition<'a, T> + where + S: ScalarValue, + T: Text<'a>, + { + match input { + MetaType::Scalar(x) => ExternalTypeDefinition::Scalar(ExternalScalarType { + position: Pos::default(), + description: x.description.as_ref().map(From::from), + name: From::from(x.name.as_ref()), + directives: vec![], + }), + MetaType::Enum(x) => ExternalTypeDefinition::Enum(ExternalEnum { + position: Pos::default(), + description: x.description.as_ref().map(|s| From::from(s.as_str())), + name: From::from(x.name.as_ref()), + directives: vec![], + values: x + .values + .iter() + .map(GraphQLParserTranslator::translate_enum_value) + .collect(), + }), + MetaType::Union(x) => ExternalTypeDefinition::Union(ExternalUnionType { + position: Pos::default(), + description: x.description.as_ref().map(|s| From::from(s.as_str())), + name: From::from(x.name.as_ref()), + directives: vec![], + types: x + .of_type_names + .iter() + .map(|s| From::from(s.as_str())) + .collect(), + }), + MetaType::Interface(x) => ExternalTypeDefinition::Interface(ExternalInterfaceType { + position: Pos::default(), + description: x.description.as_ref().map(|s| From::from(s.as_str())), + name: From::from(x.name.as_ref()), + directives: vec![], + fields: x + .fields + .iter() + .filter(|x| !x.is_builtin()) + .map(GraphQLParserTranslator::translate_field) + .collect(), + }), + MetaType::InputObject(x) => { + ExternalTypeDefinition::InputObject(ExternalInputObjectType { + position: Pos::default(), + description: x.description.as_ref().map(|s| From::from(s.as_str())), + name: From::from(x.name.as_ref()), + directives: vec![], + fields: x + .input_fields + .iter() + .filter(|x| !x.is_builtin()) + .map(GraphQLParserTranslator::translate_argument) + .collect(), + }) + } + MetaType::Object(x) => ExternalTypeDefinition::Object(ExternalObjectType { + position: Pos::default(), + description: x.description.as_ref().map(|s| From::from(s.as_str())), + name: From::from(x.name.as_ref()), + directives: vec![], + fields: x + .fields + .iter() + .filter(|x| !x.is_builtin()) + .map(GraphQLParserTranslator::translate_field) + .collect(), + implements_interfaces: x + .interface_names + .iter() + .map(|s| From::from(s.as_str())) + .collect(), + }), + _ => panic!("unknown meta type when translating"), + } + } + + fn translate_enum_value<'a, T>(input: &'a EnumValue) -> ExternalEnumValue<'a, T> + where + T: Text<'a>, + { + ExternalEnumValue { + position: Pos::default(), + name: From::from(input.name.as_ref()), + description: input.description.as_ref().map(|s| From::from(s.as_str())), + directives: generate_directives(&input.deprecation_status), + } + } + + fn translate_field<'a, S: 'a, T>(input: &'a Field) -> ExternalField<'a, T> + where + S: ScalarValue, + T: Text<'a>, + { + let arguments = input + .arguments + .as_ref() + .map(|a| { + a.iter() + .filter(|x| !x.is_builtin()) + .map(|x| GraphQLParserTranslator::translate_argument(&x)) + .collect() + }) + .unwrap_or_else(|| Vec::new()); + + ExternalField { + position: Pos::default(), + name: From::from(input.name.as_str()), + description: input.description.as_ref().map(|s| From::from(s.as_str())), + directives: generate_directives(&input.deprecation_status), + field_type: GraphQLParserTranslator::translate_type(&input.field_type), + arguments, + } + } +} + +fn deprecation_to_directive<'a, T>(status: &DeprecationStatus) -> Option> +where + T: Text<'a>, +{ + match status { + DeprecationStatus::Current => None, + DeprecationStatus::Deprecated(reason) => Some(ExternalDirective { + position: Pos::default(), + name: From::from("deprecated"), + arguments: if let Some(reason) = reason { + vec![( + From::from("reason"), + ExternalValue::String(reason.to_string()), + )] + } else { + vec![] + }, + }), + } +} + +// Right now the only directive supported is `@deprecated`. `@skip` and `@include` +// are dealt with elsewhere. +// +fn generate_directives<'a, T>(status: &DeprecationStatus) -> Vec> +where + T: Text<'a>, +{ + if let Some(d) = deprecation_to_directive(&status) { + vec![d] + } else { + vec![] + } +} diff --git a/juniper/src/schema/translate/mod.rs b/juniper/src/schema/translate/mod.rs new file mode 100644 index 000000000..2408af630 --- /dev/null +++ b/juniper/src/schema/translate/mod.rs @@ -0,0 +1,8 @@ +use crate::{ScalarValue, SchemaType}; + +pub trait SchemaTranslator<'a, T> { + fn translate_schema(s: &'a SchemaType) -> T; +} + +#[cfg(feature = "graphql-parser-integration")] +pub mod graphql_parser; From 4fcbcac6e323b8f3318ee1c4bbe1c5d60f64cf60 Mon Sep 17 00:00:00 2001 From: Alexander Lyon Date: Fri, 17 Apr 2020 17:00:17 +0100 Subject: [PATCH 02/16] Add an enum to schema language test --- juniper/src/schema/model.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index a72ce507f..9f7f8ec5f 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -597,10 +597,15 @@ mod test { #[cfg(feature = "schema-language")] mod schema_language { use crate as juniper; - use crate::{EmptyMutation, EmptySubscription}; + use crate::{EmptyMutation, EmptySubscription, GraphQLEnum}; #[test] fn schema_language() { + #[derive(GraphQLEnum)] + enum Fruit { + Apple, + Orange, + } struct Query; #[juniper::graphql_object] impl Query { @@ -617,6 +622,9 @@ mod test { fn arr(stuff: Vec) -> Option<&str> { None } + fn fruit() -> Fruit { + Fruit::Apple + } #[deprecated] fn old() -> i32 { 42 @@ -634,12 +642,18 @@ mod test { ); let ast = graphql_parser::parse_schema::<&str>( r#" + enum Fruit { + APPLE + ORANGE + } + type Query { blah: Boolean! "This is whatever's description." whatever: String! fizz(buzz: String!): String arr(stuff: [String!]!): String + fruit: Fruit! old: Int! @deprecated reallyOld: Float! @deprecated(reason: "This field is deprecated, use another.") } From 8031339be406333b68538a84dce2c37972aa193c Mon Sep 17 00:00:00 2001 From: Alexander Lyon Date: Fri, 17 Apr 2020 17:13:37 +0100 Subject: [PATCH 03/16] Add an interface to schema language test --- juniper/src/schema/model.rs | 52 +++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index 9f7f8ec5f..823b9205d 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -597,15 +597,38 @@ mod test { #[cfg(feature = "schema-language")] mod schema_language { use crate as juniper; - use crate::{EmptyMutation, EmptySubscription, GraphQLEnum}; + use crate::{EmptyMutation, EmptySubscription, GraphQLEnum, GraphQLObject, GraphQLInputObject}; #[test] fn schema_language() { + #[derive(GraphQLObject, Default)] + struct Cake { + fresh: bool, + }; + #[derive(GraphQLObject, Default)] + struct IceCream{ + cold: bool, + }; + enum Sweet { + Cake(Cake), + IceCream(IceCream), + } + juniper::graphql_interface!(Sweet: () where Scalar = |&self| { + instance_resolvers: |_| { + &Cake => match *self { Sweet::Cake(ref x) => Some(x), _ => None }, + &IceCream => match *self { Sweet::IceCream(ref x) => Some(x), _ => None }, + } + }); #[derive(GraphQLEnum)] enum Fruit { Apple, Orange, } + #[derive(GraphQLInputObject)] + struct Coordinate { + latitude: f64, + longitude: f64 + } struct Query; #[juniper::graphql_object] impl Query { @@ -616,10 +639,14 @@ mod test { fn whatever() -> String { "foo".to_string() } - fn fizz(buzz: String) -> Option<&str> { - None + fn fizz(buzz: String) -> Option { + if buzz == "whatever" { + Some(Sweet::Cake(Cake::default())) + } else { + Some(Sweet::IceCream(IceCream::default())) + } } - fn arr(stuff: Vec) -> Option<&str> { + fn arr(stuff: Vec) -> Option<&str> { None } fn fruit() -> Fruit { @@ -646,18 +673,27 @@ mod test { APPLE ORANGE } - + interface Sweet + type Cake { + fresh: Boolean! + } + type IceCream { + cold: Boolean! + } type Query { blah: Boolean! "This is whatever's description." whatever: String! - fizz(buzz: String!): String - arr(stuff: [String!]!): String + fizz(buzz: String!): Sweet + arr(stuff: [Coordinate!]!): String fruit: Fruit! old: Int! @deprecated reallyOld: Float! @deprecated(reason: "This field is deprecated, use another.") } - + input Coordinate { + latitude: Float! + longitude: Float! + } schema { query: Query } From a538a9716b9cfcb43cf840ca85afacd664184cc8 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Fri, 8 Mar 2019 16:48:27 -0800 Subject: [PATCH 04/16] Fix formatting --- juniper/src/schema/model.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index 823b9205d..9a68eafe7 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -606,7 +606,7 @@ mod test { fresh: bool, }; #[derive(GraphQLObject, Default)] - struct IceCream{ + struct IceCream { cold: bool, }; enum Sweet { @@ -627,7 +627,7 @@ mod test { #[derive(GraphQLInputObject)] struct Coordinate { latitude: f64, - longitude: f64 + longitude: f64, } struct Query; #[juniper::graphql_object] From 63b8d8f4af3d7148e93d20285308db9f01753a46 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Fri, 8 Mar 2019 16:58:05 -0800 Subject: [PATCH 05/16] Add a field to example interface --- juniper/src/schema/model.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index 9a68eafe7..a6223f0c9 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -614,6 +614,7 @@ mod test { IceCream(IceCream), } juniper::graphql_interface!(Sweet: () where Scalar = |&self| { + field is_brownie() -> bool { false } instance_resolvers: |_| { &Cake => match *self { Sweet::Cake(ref x) => Some(x), _ => None }, &IceCream => match *self { Sweet::IceCream(ref x) => Some(x), _ => None }, @@ -673,7 +674,9 @@ mod test { APPLE ORANGE } - interface Sweet + interface Sweet { + isBrownie: Boolean! + } type Cake { fresh: Boolean! } From b73cb89b0c25ef3d024742077b6c884627ec4e8a Mon Sep 17 00:00:00 2001 From: Alexander Lyon Date: Fri, 17 Apr 2020 17:50:07 +0100 Subject: [PATCH 06/16] Add a union to schema language test --- juniper/src/schema/model.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index a6223f0c9..f64b36ebd 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -597,7 +597,10 @@ mod test { #[cfg(feature = "schema-language")] mod schema_language { use crate as juniper; - use crate::{EmptyMutation, EmptySubscription, GraphQLEnum, GraphQLObject, GraphQLInputObject}; + use crate::{ + EmptyMutation, EmptySubscription, GraphQLEnum, GraphQLInputObject, GraphQLObject, + GraphQLUnion, + }; #[test] fn schema_language() { @@ -620,6 +623,11 @@ mod test { &IceCream => match *self { Sweet::IceCream(ref x) => Some(x), _ => None }, } }); + #[derive(GraphQLUnion)] + enum GlutenFree { + Cake(Cake), + IceCream(IceCream), + } #[derive(GraphQLEnum)] enum Fruit { Apple, @@ -653,6 +661,13 @@ mod test { fn fruit() -> Fruit { Fruit::Apple } + fn gluten_free(flavor: String) -> GlutenFree { + if flavor == "savory" { + GlutenFree::Cake(Cake::default()) + } else { + GlutenFree::IceCream(IceCream::default()) + } + } #[deprecated] fn old() -> i32 { 42 @@ -670,6 +685,7 @@ mod test { ); let ast = graphql_parser::parse_schema::<&str>( r#" + union GlutenFree = Cake | IceCream enum Fruit { APPLE ORANGE @@ -690,6 +706,7 @@ mod test { fizz(buzz: String!): Sweet arr(stuff: [Coordinate!]!): String fruit: Fruit! + glutenFree(flavor: String!): GlutenFree! old: Int! @deprecated reallyOld: Float! @deprecated(reason: "This field is deprecated, use another.") } From 5835557faa807fae8790439268ff9181bbb928d2 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Thu, 4 Jun 2020 20:25:05 -1000 Subject: [PATCH 07/16] Remove interfaces This can come back when https://github.com/graphql-rust/juniper/issues/605 exists. --- juniper/src/schema/model.rs | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index f64b36ebd..80f401e75 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -599,7 +599,7 @@ mod test { use crate as juniper; use crate::{ EmptyMutation, EmptySubscription, GraphQLEnum, GraphQLInputObject, GraphQLObject, - GraphQLUnion, + GraphQLUnionInternal as GraphQLUnion, }; #[test] @@ -612,17 +612,6 @@ mod test { struct IceCream { cold: bool, }; - enum Sweet { - Cake(Cake), - IceCream(IceCream), - } - juniper::graphql_interface!(Sweet: () where Scalar = |&self| { - field is_brownie() -> bool { false } - instance_resolvers: |_| { - &Cake => match *self { Sweet::Cake(ref x) => Some(x), _ => None }, - &IceCream => match *self { Sweet::IceCream(ref x) => Some(x), _ => None }, - } - }); #[derive(GraphQLUnion)] enum GlutenFree { Cake(Cake), @@ -648,16 +637,13 @@ mod test { fn whatever() -> String { "foo".to_string() } - fn fizz(buzz: String) -> Option { - if buzz == "whatever" { - Some(Sweet::Cake(Cake::default())) + fn arr(stuff: Vec) -> Option<&str> { + if stuff.is_empty() { + None } else { - Some(Sweet::IceCream(IceCream::default())) + Some("stuff") } } - fn arr(stuff: Vec) -> Option<&str> { - None - } fn fruit() -> Fruit { Fruit::Apple } @@ -690,9 +676,6 @@ mod test { APPLE ORANGE } - interface Sweet { - isBrownie: Boolean! - } type Cake { fresh: Boolean! } @@ -703,7 +686,6 @@ mod test { blah: Boolean! "This is whatever's description." whatever: String! - fizz(buzz: String!): Sweet arr(stuff: [Coordinate!]!): String fruit: Fruit! glutenFree(flavor: String!): GlutenFree! From 66de4078a3fce1a246e0c0bebff4f791c9c53c98 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Thu, 4 Jun 2020 21:27:08 -1000 Subject: [PATCH 08/16] Add docs --- README.md | 3 ++ docs/book/content/quickstart.md | 4 ++ .../content/schema/schemas_and_mutations.md | 51 +++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/README.md b/README.md index 68a987943..ad507984e 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ non-null types by default. A field of type `Vec` will be converted into `[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be `Option>>`. +Juniper follows a [code-first approach][schema_approach] to defining GraphQL Schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file. + ## Integrations ### Data types @@ -91,6 +93,7 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected. [iron]: http://ironframework.io [graphql_spec]: http://facebook.github.io/graphql [schema_language]: https://graphql.org/learn/schema/#type-language +[schema_approach]: https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/ [test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs [tokio]: https://github.com/tokio-rs/tokio [actix_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_actix/examples diff --git a/docs/book/content/quickstart.md b/docs/book/content/quickstart.md index ab2f02c00..4918c784c 100644 --- a/docs/book/content/quickstart.md +++ b/docs/book/content/quickstart.md @@ -2,6 +2,8 @@ This page will give you a short introduction to the concepts in Juniper. +Juniper follows a [code-first approach][schema_approach] to defining GraphQL Schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file. + ## Installation !FILENAME Cargo.toml @@ -193,6 +195,8 @@ fn main() { } ``` +[juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema +[schema_approach]: https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/ [hyper]: servers/hyper.md [warp]: servers/warp.md [rocket]: servers/rocket.md diff --git a/docs/book/content/schema/schemas_and_mutations.md b/docs/book/content/schema/schemas_and_mutations.md index 124b7a89e..eed648eb5 100644 --- a/docs/book/content/schema/schemas_and_mutations.md +++ b/docs/book/content/schema/schemas_and_mutations.md @@ -1,5 +1,7 @@ # Schemas +Juniper follows a [code-first approach][schema_approach] to defining GraphQL Schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file. + A schema consists of three types: a query object, a mutation object, and a subscription object. These three define the root query fields, mutations and subscriptions of the schema, respectively. @@ -60,6 +62,55 @@ impl Mutations { # fn main() { } ``` +# Outputting schemas in the [GraphQL Schema Language][schema_language] + +Many tools in the GraphQL ecosystem require the schema to be defined in the [GraphQL Schema Language][schema_language]. You can generate a [GraphQL Schema Language][schema_language] representation of your schema defined in Rust using the `schema-language` feature (on by default): + +```rust +# // Only needed due to 2018 edition because the macro is not accessible. +# #[macro_use] extern crate juniper; +use juniper::{FieldResult, EmptyMutation, EmptySubscription, RootNode}; + +struct Query; + +#[juniper::graphql_object] +impl Query { + fn hello(&self) -> FieldResult<&str> { + Ok("hello world") + } +} + +fn main() { + // Define our schema in Rust. + let schema = RootNode::new( + Query, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + + // Convert the Rust schema into the GraphQL Schema Language. + let result = schema.as_schema_language(); + + let expected = + r#" + type Query { + hello: String! + } + schema { + query: Query + } + "#; + assert_eq!(result, expected); +} +``` + +Note the `schema-language` feature may be turned off if you do not need this functionality to reduce dependencies and speed up +compile times. + + +[schema_language]: https://graphql.org/learn/schema/#type-language +[juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema +[schema_approach]: https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/ [section]: ../advanced/subscriptions.md [EmptyMutation]: https://docs.rs/juniper/0.14.2/juniper/struct.EmptyMutation.html From 34d40e88f0675b9954276d3e4e3fd42f0be7438f Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Thu, 4 Jun 2020 21:36:41 -1000 Subject: [PATCH 09/16] Apply suggestions from code review --- README.md | 2 +- docs/book/content/quickstart.md | 2 +- docs/book/content/schema/schemas_and_mutations.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ad507984e..646ecc58f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ non-null types by default. A field of type `Vec` will be converted into `[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be `Option>>`. -Juniper follows a [code-first approach][schema_approach] to defining GraphQL Schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file. +Juniper follows a [code-first approach][schema_approach] to defining GraphQL schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file. ## Integrations diff --git a/docs/book/content/quickstart.md b/docs/book/content/quickstart.md index 4918c784c..dc663ecbc 100644 --- a/docs/book/content/quickstart.md +++ b/docs/book/content/quickstart.md @@ -2,7 +2,7 @@ This page will give you a short introduction to the concepts in Juniper. -Juniper follows a [code-first approach][schema_approach] to defining GraphQL Schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file. +Juniper follows a [code-first approach][schema_approach] to defining GraphQL schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file. ## Installation diff --git a/docs/book/content/schema/schemas_and_mutations.md b/docs/book/content/schema/schemas_and_mutations.md index eed648eb5..f16426e04 100644 --- a/docs/book/content/schema/schemas_and_mutations.md +++ b/docs/book/content/schema/schemas_and_mutations.md @@ -1,6 +1,6 @@ # Schemas -Juniper follows a [code-first approach][schema_approach] to defining GraphQL Schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file. +Juniper follows a [code-first approach][schema_approach] to defining GraphQL schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file. A schema consists of three types: a query object, a mutation object, and a subscription object. These three define the root query fields, mutations and subscriptions of the schema, respectively. From 81ea7cc10e4db4a7abb8d9292b0e24ea29539676 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Thu, 4 Jun 2020 21:44:38 -1000 Subject: [PATCH 10/16] Fix formatting in assert --- .../content/schema/schemas_and_mutations.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/book/content/schema/schemas_and_mutations.md b/docs/book/content/schema/schemas_and_mutations.md index eed648eb5..658283019 100644 --- a/docs/book/content/schema/schemas_and_mutations.md +++ b/docs/book/content/schema/schemas_and_mutations.md @@ -91,15 +91,15 @@ fn main() { // Convert the Rust schema into the GraphQL Schema Language. let result = schema.as_schema_language(); - let expected = - r#" - type Query { - hello: String! - } - schema { - query: Query - } - "#; + let expected = r#" +type Query { + hello: String! +} + +schema { + query: Query +} +"#; assert_eq!(result, expected); } ``` From f66d96eb2a2d1386526aabf72a298b1272167c52 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Thu, 4 Jun 2020 21:50:15 -1000 Subject: [PATCH 11/16] Add entry to changelog --- juniper/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 1584f9251..02ce87588 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -2,6 +2,9 @@ ## Features +- Added support for outputting the Rust schema in the [GraphQL Schema Language](https://graphql.org/learn/schema/#type-language) ([#676]) + - Controlled by the `schema-language` feature and is on by default. It may be turned off if you do not need this functionality to reduce dependencies and speed up compile times. + - Normalization for the subscriptions_endpoint_url in the `graphiql_source`. (See [#628](https://github.com/graphql-rust/juniper/pull/628) for more details) From 88387db735d487c3a0e219f61deb764b73f97065 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Thu, 4 Jun 2020 21:52:11 -1000 Subject: [PATCH 12/16] Fix changelog --- juniper/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 02ce87588..e96483b22 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -2,8 +2,8 @@ ## Features -- Added support for outputting the Rust schema in the [GraphQL Schema Language](https://graphql.org/learn/schema/#type-language) ([#676]) - - Controlled by the `schema-language` feature and is on by default. It may be turned off if you do not need this functionality to reduce dependencies and speed up compile times. +- Added support for outputting the Rust schema in the [GraphQL Schema Language](https://graphql.org/learn/schema/#type-language). ([#676](https://github.com/graphql-rust/juniper/pull/676)) + - This is controlled by the `schema-language` feature and is on by default. It may be turned off if you do not need this functionality to reduce dependencies and speed up compile times. - Normalization for the subscriptions_endpoint_url in the `graphiql_source`. (See [#628](https://github.com/graphql-rust/juniper/pull/628) for more details) From b6bd4b380d0e913aa4120152462e2fe83bec0ca6 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Thu, 4 Jun 2020 21:56:05 -1000 Subject: [PATCH 13/16] fix formatting --- docs/book/content/schema/schemas_and_mutations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/book/content/schema/schemas_and_mutations.md b/docs/book/content/schema/schemas_and_mutations.md index dea1a6a66..df3417972 100644 --- a/docs/book/content/schema/schemas_and_mutations.md +++ b/docs/book/content/schema/schemas_and_mutations.md @@ -91,7 +91,7 @@ fn main() { // Convert the Rust schema into the GraphQL Schema Language. let result = schema.as_schema_language(); - let expected = r#" + let expected = "\ type Query { hello: String! } @@ -99,7 +99,7 @@ type Query { schema { query: Query } -"#; +"; assert_eq!(result, expected); } ``` From 539a4789c8dfb11fa40a28b3eac302d4d042af6f Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Fri, 5 Jun 2020 16:51:08 -1000 Subject: [PATCH 14/16] Fix build with features --- juniper/src/schema/model.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index 80f401e75..ea6be39d0 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -1,19 +1,22 @@ use std::fmt; use fnv::FnvHashMap; - +#[cfg(feature = "graphql-parser-integration")] use graphql_parser::schema::Document; + use juniper_codegen::GraphQLEnumInternal as GraphQLEnum; use crate::{ ast::Type, executor::{Context, Registry}, schema::meta::{Argument, InterfaceMeta, MetaType, ObjectMeta, PlaceholderMeta, UnionMeta}, - schema::translate::{graphql_parser::GraphQLParserTranslator, SchemaTranslator}, types::{base::GraphQLType, name::Name}, value::{DefaultScalarValue, ScalarValue}, }; +#[cfg(feature = "graphql-parser-integration")] +use crate::schema::translate::{graphql_parser::GraphQLParserTranslator, SchemaTranslator}; + /// Root query node of a schema /// /// This brings the mutation, subscription and query types together, @@ -644,7 +647,7 @@ mod test { Some("stuff") } } - fn fruit() -> Fruit { + fndm fruit() -> Fruit { Fruit::Apple } fn gluten_free(flavor: String) -> GlutenFree { From 1a102628220ec26257721bd3e8cb0b4ffffc6c63 Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Fri, 5 Jun 2020 17:02:58 -1000 Subject: [PATCH 15/16] Fix typo --- juniper/src/schema/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index ea6be39d0..2a0d5c021 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -647,7 +647,7 @@ mod test { Some("stuff") } } - fndm fruit() -> Fruit { + fn fruit() -> Fruit { Fruit::Apple } fn gluten_free(flavor: String) -> GlutenFree { From a41d234077e36d36bd01cdff8b80243e88e67e6f Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Fri, 5 Jun 2020 17:40:25 -1000 Subject: [PATCH 16/16] Fix comment --- juniper/src/schema/meta.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index 777e3b1ee..1f610ed6f 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -392,7 +392,7 @@ impl<'a, S> MetaType<'a, S> { name.starts_with("__") || // name == "Boolean" || name == "String" || name == "Int" || name == "Float" || name == "ID" || - // Our custom empty mutation marker + // Our custom empty markers name == "_EmptyMutation" || name == "_EmptySubscription" } } else {