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.
src/graphql/mutation
Mutation resolvers.
src/graphql/resolver
GraphQL Object
, Enum
, Union
, Interface
types.
src/graphql/input
GraphQL InputObject.
src/graphql/scalar
Custom scalars.
src/graphql/directive
Custom directives.
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.