rusty-gql

rusty-gql is a Schema First GraphQL library for Rust.

It is designed to make it easier to create a GraphQL server.

Features

  • Schema First approach
  • Code Generate from GraphQL schema
  • Convention Over Configuration

Status

rusty-gql is still an experimental project. APIs and the architecture are subject to change.

It is not yet recommended for use in production.

Getting Started

Install rusty-gql-cli

cargo install rusty-gql-cli

Run new command

rusty-gql new gql-example cd gql-example

Start the GraphQL Server

cargo run

Creating a GraphQL Schema

rusty-gql is designed for schema first development. It reads any graphql files under schema/**.

schema/schema.graphql

type Query { todos(first: Int): [Todo!]! } type Todo { title: String! content: String done: Boolean! }

Implement Resolvers

Let's edit src/graphql/query/todos.rs.

#![allow(unused)] fn main() { pub async fn todos(ctx: &Context<'_>, first: Option<i32>) -> Vec<Todo> { let all_todos = vec![ Todo { title: "Programming".to_string(), content: Some("Learn Rust".to_string()), done: false, }, Todo { title: "Shopping".to_string(), content: None, done: true, }, ]; match first { Some(first) => all_todos.into_iter().take(first as usize).collect(), None => all_todos, } } }

Generate Rust code

Edit schema.graphql.

type Query { todos(first: Int): [Todo!]! # added todo(id: ID!): Todo } type Todo { title: String! description: String done: Boolean! }

rusty-gql generates rust code from graphql schema files.

rusty-gql generate // or rusty-gql g

Directory Structure

src ┣ graphql ┃ ┣ directive ┃ ┃ ┗ mod.rs ┃ ┣ input ┃ ┃ ┗ mod.rs ┃ ┣ mutation ┃ ┃ ┗ mod.rs ┃ ┣ query ┃ ┃ ┣ mod.rs ┃ ┃ ┣ todo.rs ┃ ┃ ┗ todos.rs ┃ ┣ resolver ┃ ┃ ┣ mod.rs ┃ ┃ ┗ todo.rs ┃ ┣ scalar ┃ ┃ ┗ mod.rs ┃ ┗ mod.rs ┗ main.rs

GraphQL Playground

rusty-gql supports GraphiQL playground. Open a browser to http://localhost:3000/graphiql.

Directory Structure

A rusty-gql project has the following directory structure.

rusty-gql-project ┣ schema ┃ ┗ schema.graphql ┣ src ┃ ┣ graphql ┃ ┃ ┣ directive ┃ ┃ ┃ ┗ mod.rs ┃ ┃ ┣ input ┃ ┃ ┃ ┗ mod.rs ┃ ┃ ┣ mutation ┃ ┃ ┃ ┗ mod.rs ┃ ┃ ┣ query ┃ ┃ ┃ ┣ mod.rs ┃ ┃ ┣ resolver ┃ ┃ ┃ ┣ mod.rs ┃ ┃ ┣ scalar ┃ ┃ ┃ ┗ mod.rs ┃ ┃ ┗ mod.rs ┃ ┗ main.rs ┗ Cargo.toml

schema

GraphQL schema files are located under schema/**.

We can also place multiple GraphQL files.

For example, like this.

schema ┣ post ┃ ┗ post.graphql ┣ user ┃ ┗ user.graphql ┗ index.graphql

src/graphql/query

Query resolvers.

Query

src/graphql/mutation

Mutation resolvers.

Mutation

src/graphql/resolver

GraphQL Object, Enum, Union, Interface types.

src/graphql/input

GraphQL InputObject.

InputObject

src/graphql/scalar

Custom scalars.

Scalar

src/graphql/directive

Custom directives.

Directive

Types

rusty-gql generates GraphQL types as Rust codes from schemas.

Object

rusty-gql defines GraphQL Object as Rust struct and #[GqlType] like the following.

src/graphql/resolver/todo.rs

#![allow(unused)] fn main() { #[derive(Clone)] pub struct Todo { pub title: String, pub content: Option<String>, pub done: bool, } #[GqlType] impl Todo { pub async fn title(&self, ctx: &Context<'_>) -> String { self.title.clone() } pub async fn content(&self, ctx: &Context<'_>) -> Option<String> { self.content.clone() } pub async fn done(&self, ctx: &Context<'_>) -> bool { self.done } } }

schema.graphql

type Todo { title: String! content: String done: Boolean! }

We'll implement async fn for each fields with #[GqlType].

If we want to execute only when the field is included in a operation, implement async fn without the struct field.

src/graphql/resolver/todo.rs

#![allow(unused)] fn main() { #[derive(Clone)] pub struct Todo { pub title: String, pub content: Option<String>, pub done: bool, } #[GqlType] impl Todo { ... pub async fn user(&self, ctx: &Context<'_>) -> User { todo!() } } }
type Todo { title: String! content: String done: Boolean! user: User! } type User { name }

Interface

GraphQL Interface is represented as Rust enum with different types and #[derive(GqlInterface), #[GqlType(interface)].

Each variants is possible types of interface.

src/graphql/resolver/pet.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; #[derive(GqlInterface, Clone)] pub enum Pet { Cat(Cat), Dog(Dog), } #[GqlType(interface)] impl Pet { async fn name(&self, ctx: &Context<'_>) -> Result<String, Error> { match self { Pet::Cat(obj) => obj.name(&ctx).await, Pet::Dog(obj) => obj.name(&ctx).await, } } } }

schema.graphql

interface Pet { name: String } type Cat implements Pet { name: String meows: Boolean } type Dog implements Pet { name: String woofs: Boolean }

Union

rusty-gql defines GraphQL Union as Rust enum with different types and #[derive(GqlUnion)].

src/graphql/resolver/search_result.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; #[derive(GqlUnion)] pub enum SearchResult { Human(Human), Droid(Droid), } }

schema.graphql

type Query { search(text: String): [SearchResult!]! } union SearchResult = Human | Droid type Human { id: ID! name: String! homePlanet: String } type Droid { id: ID! name: String! primaryFunction: String }

Enum

rusty-gql defines GraphQL Enum as Rust enum with #[derive(GqlEnum)].

src/graphql/resolver/episode.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; #[derive(GqlEnum, Debug, Copy, Clone, Eq, PartialEq)] pub enum Episode { NEWHOPE, EMPIRE, JEDI, } }

schema.graphql

enum Episode { NEWHOPE EMPIRE JEDI }

InputObject

rusty-gql defines GraphQL InputObject as Rust struct with #[derive(GqlInputObject)].

src/graphql/input/review_input.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; #[derive(GqlInputObject)] pub struct ReviewInput { pub stars: i32, pub commentary: Option<String>, } }

schema.graphql

type Mutation { createReview(episode: Episode, review: ReviewInput!): Review } input ReviewInput { stars: Int! commentary: String } type Review { episode: Episode stars: Int! commentary: String }

Scalar

We can define custom scalars.

rusty-gql represents custom scalar by using #[derive(GqlScalar)] and GqlInputType trait.

src/graphql/scalar/base64.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; #[derive(GqlScalar)] pub struct Base64(String); impl GqlInputType for Base64 { fn from_gql_value(value: Option<GqlValue>) -> Result<Self, String> { if let Some(GqlValue::String(v)) = value { let encoded = base64::encode(v); Ok(Base64(encoded)) } else { Err(format!( "{}: is invalid type for Base64", value.unwrap_or(GqlValue::Null).to_string() )) } } fn to_gql_value(&self) -> GqlValue { GqlValue::String(self.0.to_string()) } } }

schema.graphql

scalar Base64

Directive

We can use directives as middleware.

It is useful in the following use cases.

  • Authorization
  • Validation
  • Caching
  • Logging, metrics
  • etc.

If we don't want to expose a specific field, we can define the following directive.

src/graphql/directive/hidden.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; #[derive(Clone)] struct Hidden; impl Hidden { fn new() -> Box<dyn CustomDirective> { Box::new(Hidden {}) } } #[async_trait::async_trait] impl CustomDirective for Hidden { async fn resolve_field( &self, _ctx: &Context<'_>, _directive_args: &BTreeMap<String, GqlValue>, resolve_fut: ResolveFut<'_>, ) -> ResolverResult<Option<GqlValue>> { resolve_fut.await.map(|_v| None) } } }

schema.graphql

type User { name: String! password_hash: String @hidden } directive @hidden on FIELD_DEFINITION | OBJECT

Need to pass a HashMap of directives when Container::new in main.rs.

A Key is the directive name, a value is the directive struct.

main.rs

async fn main() { ... let mut custom_directive_maps = HashMap::new(); custom_directive_maps.insert("hidden", Hidden::new()); let container = Container::new( schema_docs.as_slice(), Query, Mutation, EmptySubscription, custom_directive_maps, // path here ) .unwrap(); ... }

Schema

rusty-gql supports Query and Mutation. (Subscription is work in progress.)

These will be generated automatically when we create a rusty-gql project.

Query

rusty-gql has Query files under src/graphql/query/**.

For example,

src ┣ graphql ┃ ┣ query ┃ ┃ ┣ mod.rs ┃ ┃ ┗ todos.rs

src/graphql/query/todos.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; pub async fn todos(ctx: &Context<'_>, first: Option<i32>) -> Vec<Todo> { let all_todos = vec![ Todo { title: "Programming".to_string(), content: Some("Learn Rust".to_string()), done: false, }, Todo { title: "Shopping".to_string(), content: None, done: true, }, ]; match first { Some(first) => all_todos.into_iter().take(first as usize).collect(), None => all_todos, } } }

src/graphql/query/mod.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; mod todos; #[derive(Clone)] pub struct Query; #[GqlType] impl Query { pub async fn todos(&self, ctx: &Context<'_>, first: Option<i32>) -> Vec<Todo> { todos::todos(&ctx,first).await } } }

Files except for mod.rs implements resolvers for each Query fields.

mod.rs only bundles these files and defines Query struct.

Mutation

Mutation has a similar directory structure to Query.

rusty-gql has Mutation files under src/graphql/mutation/**.

src ┣ graphql ┃ ┣ mutation ┃ ┃ ┣ mod.rs ┃ ┃ ┗ create_todo.rs

src/graphql/mutation/create_todo.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; pub async fn createTodo(ctx: &Context<'_>, input: TodoInput) -> Todo { ... } }

src/graphql/mutation/mod.rs

#![allow(unused)] #![allow(warnings, unused)] fn main() { use crate::graphql::*; use rusty_gql::*; mod create_todo; #[derive(Clone)] pub struct Mutation; #[GqlType] impl Mutation { pub async fn todos(&self, ctx: &Context<'_>, input: TodoInput) -> Todo { create_todo::createTodo(ctx, input).await } } }

Mutation is optional, so if we don't need Mutation, use EmptyMutation struct in main.rs

main.rs

mod graphql; ... #[tokio::main] async fn main() { ... let container = Container::new( schema_docs.as_slice(), Query, EmptyMutation, // or graphql::Mutation EmptySubscription, Default::default(), ) .unwrap(); }

Error Handling

If errors occur while GraphQL operation, errors field will be included in the response.

Add errors by using add_error of Context.

A error is defined by GqlError struct.

#![allow(unused)] fn main() { pub async fn todos(ctx: &Context<'_>, first: Option<i32>) -> Vec<Todo> { let all_todos = vec![ Todo { title: "Programming".to_string(), content: Some("Learn Rust".to_string()), done: false, }, Todo { title: "Shopping".to_string(), content: None, done: true, }, ]; match first { Some(first) => { if first > 30 { // add error ctx.add_error(&GqlError::new("Up to 30 items at one time.", Some(ctx.item.position))); all_todos } else { all_todos.into_iter().take(first as usize).collect(), } } None => all_todos, } } }

When we want to add a meta info, use extensions.

#![allow(unused)] fn main() { ctx.add_error( &GqlError::new("Error happens", Some(ctx.item.position)).set_extentions( GqlTypedError { error_type: GqlErrorType::Internal, error_detail: Some("Internal Error".to_string()), origin: None, debug_info: None, debug_uri: None, }, ), ); }

The GraphQL definition of rusty-gql error is as follows. Also see GraphQL spec.

type GqlError { message: String! locations: [Location!]! path: [String!]! extensions: GqlTypedError } type GqlTypedError { errorType: GqlErrorType! errorDetail: String origin: String debugInfo: DebugInfo debugUri: String } enum GqlErrorType { BadRequest FailedPreCondition Internal NotFound PermissionDenied Unauthenticated Unavailable Unknown }

Roadmap

The following features will be implemented.

  • Subscription
  • Dataloader
  • Calculate Query complexity
  • Apollo tracing
  • Apollo Federation
  • Automatic Persisted Query
  • etc.