Skip to content

Add support for GraphQL Schema Language #624

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Episode>` will be converted into
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions juniper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []

Expand All @@ -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"
Expand Down
32 changes: 32 additions & 0 deletions juniper/src/schema/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -182,6 +190,14 @@ pub struct Argument<'a, S> {
pub default_value: Option<InputValue<S>>,
}

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 {
Expand Down Expand Up @@ -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("__") ||
// <https://facebook.github.io/graphql/draft/#sec-Scalars>
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<S>) -> Option<Vec<&'b Field<'b, S>>> {
schema
.lookup_type(&self.as_type())
Expand Down
1 change: 1 addition & 0 deletions juniper/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
pub mod meta;
pub mod model;
pub mod schema;
pub mod translate;
196 changes: 193 additions & 3 deletions juniper/src/schema/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -46,9 +48,9 @@ pub struct RootNode<
#[derive(Debug)]
pub struct SchemaType<'a, S> {
pub(crate) types: FnvHashMap<Name, MetaType<'a, S>>,
query_type_name: String,
mutation_type_name: Option<String>,
subscription_type_name: Option<String>,
pub(crate) query_type_name: String,
pub(crate) mutation_type_name: Option<String>,
pub(crate) subscription_type_name: Option<String>,
directives: FnvHashMap<String, DirectiveType<'a, S>>,
}

Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -534,3 +552,175 @@ 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,
GraphQLUnion,
};

#[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 = <S> |&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),
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 fizz(buzz: String) -> Option<Sweet> {
if buzz == "whatever" {
Some(Sweet::Cake(Cake::default()))
} else {
Some(Sweet::IceCream(IceCream::default()))
}
}
fn arr(stuff: Vec<Coordinate>) -> Option<&str> {
None
}
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
}
interface Sweet {
isBrownie: Boolean!
}
type Cake {
fresh: Boolean!
}
type IceCream {
cold: Boolean!
}
type Query {
blah: Boolean!
"This is whatever's description."
whatever: String!
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.")
}
input Coordinate {
latitude: Float!
longitude: Float!
}
schema {
query: Query
}
"#,
)
.unwrap();
assert_eq!(format!("{}", ast), schema.as_schema_language());
}
}
}
Loading