Skip to content
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

Add support for GraphQL Schema Language #676

Merged
merged 17 commits into from
Jun 6, 2020
Merged
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Episode>` will be converted into
`[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be
`Option<Vec<Option<Episode>>>`.

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.
LegNeato marked this conversation as resolved.
Show resolved Hide resolved

## Integrations

### Data types
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/book/content/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
LegNeato marked this conversation as resolved.
Show resolved Hide resolved

## Installation

!FILENAME Cargo.toml
Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions docs/book/content/schema/schemas_and_mutations.md
Original file line number Diff line number Diff line change
@@ -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.
LegNeato marked this conversation as resolved.
Show resolved Hide resolved

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.

Expand Down Expand Up @@ -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
<!--TODO: Fix This URL when the EmptySubscription become available in the Documentation -->
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;
178 changes: 175 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,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<Coordinate>) -> 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());
}
}
}