From 9167654a731ece63fab3f1bbdd988a80ed255c2c Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Fri, 5 Jun 2020 17:43:11 -1000 Subject: [PATCH] Add support for GraphQL Schema Language (#676) Co-authored-by: Alexander Lyon --- README.md | 7 +- docs/book/content/quickstart.md | 4 + .../content/schema/schemas_and_mutations.md | 51 +++ juniper/CHANGELOG.md | 3 + juniper/Cargo.toml | 4 + juniper/src/schema/meta.rs | 32 ++ juniper/src/schema/mod.rs | 1 + juniper/src/schema/model.rs | 181 ++++++++++- .../src/schema/translate/graphql_parser.rs | 306 ++++++++++++++++++ juniper/src/schema/translate/mod.rs | 8 + 10 files changed, 592 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..646ecc58f 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,15 @@ 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 `[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 +92,8 @@ 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 +[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..dc663ecbc 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..df3417972 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 = "\ +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 diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 1584f9251..e96483b22 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](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) 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..1f610ed6f 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 markers + 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..2a0d5c021 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -1,6 +1,8 @@ use std::fmt; use fnv::FnvHashMap; +#[cfg(feature = "graphql-parser-integration")] +use graphql_parser::schema::Document; use juniper_codegen::GraphQLEnumInternal as GraphQLEnum; @@ -12,6 +14,9 @@ use crate::{ 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, @@ -46,9 +51,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 +107,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 +555,157 @@ 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, GraphQLEnum, GraphQLInputObject, GraphQLObject, + GraphQLUnionInternal as GraphQLUnion, + }; + + #[test] + fn schema_language() { + #[derive(GraphQLObject, Default)] + struct Cake { + fresh: bool, + }; + #[derive(GraphQLObject, Default)] + struct IceCream { + cold: bool, + }; + #[derive(GraphQLUnion)] + enum GlutenFree { + Cake(Cake), + IceCream(IceCream), + } + #[derive(GraphQLEnum)] + enum Fruit { + Apple, + Orange, + } + #[derive(GraphQLInputObject)] + struct Coordinate { + latitude: f64, + longitude: f64, + } + struct Query; + #[juniper::graphql_object] + impl Query { + fn blah() -> bool { + true + } + /// This is whatever's description. + fn whatever() -> String { + "foo".to_string() + } + fn arr(stuff: Vec) -> Option<&str> { + if stuff.is_empty() { + None + } else { + Some("stuff") + } + } + 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 + } + #[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#" + union GlutenFree = Cake | IceCream + enum Fruit { + APPLE + ORANGE + } + type Cake { + fresh: Boolean! + } + type IceCream { + cold: Boolean! + } + type Query { + blah: Boolean! + "This is whatever's description." + whatever: String! + arr(stuff: [Coordinate!]!): String + fruit: Fruit! + glutenFree(flavor: String!): GlutenFree! + old: Int! @deprecated + reallyOld: Float! @deprecated(reason: "This field is deprecated, use another.") + } + input Coordinate { + latitude: Float! + longitude: Float! + } + 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;