RustのSchema First GraphQLライブラリrusty-gqlを作りました
rusty-gql
Schema FirstのRust製GraphQLライブラリであるrusty-gqlを作りました。この記事ではその紹介(宣伝)をさせていただきます。
なぜ作ったのか
これまでRustのGraphQLライブラリはjuniperとasync-graphqlがありました。
これら2つはCode Firstで設計されており、GraphQLのスキーマ定義をRustのマクロを使用して定義します。
以下はasync-graphqlの例です。
use async_graphql::*;
struct MyObject {
value: i32,
}
#[Object]
impl MyObject {
async fn value(&self) -> String {
self.value.to_string()
}
async fn value_from_db(
&self,
ctx: &Context<'_>,
#[graphql(desc = "Id of object")] id: i64
) -> Result<String> {
let conn = ctx.data::<DbPool>()?.take();
Ok(conn.query_something(id)?.name)
}
}
#[derive(Interface)]
#[graphql(
field(name = "area", type = "f32"),
field(name = "scale", type = "Shape", arg(name = "s", type = "f32")),
field(name = "short_description", method = "short_description", type = "String")
)]
enum Shape {
Circle(Circle),
Square(Square),
}
私個人の意見ですが、GraphQLのサーバー実装はSchema Firstの方が適していると考えています。
特にそれがクライアントサイドのエンジニアにとってあまり馴染みがない言語である場合において。
GraphQLはUIに応じたtree構造でデータを返却できることが特徴です。つまり、クライアントサイドエンジニアのためのものなので、フロントエンドやモバイルエンジニアの方もスキーマ定義を簡単に把握できることが重要と考えています。
Code Firstである場合、GraphQLのスキーマ定義を使用せずにサーバー実装の言語でスキーマを定義します。そのため、サーバサイドの実装言語に馴染みのない方にとっては把握しづらいものとなります。
Schema Firstの場合は最初に.graphql
ファイルでスキーマを定義するため、クライアントサイド、バックエンドのエンジニア双方が理解したフォーマットで仕様を決めることができます。
開発プロセスによってはUIを作るエンジニアが.graphql
ファイルを編集してPRを作成し、それに応じてサーバーサイドのエンジニアが実装するということも可能になります。
ちなみに余談ではありますが、昨年発表されたNetflixのGraphQLライブラリであるDGSもSchema Firstを推奨しています。
Open Sourcing the Netflix Domain Graph Service Framework: GraphQL for Spring Boot
Getting Started
プロジェクトの作成
rusty-gqlのプロジェクトを作成するためにrusty-gql-cli
をインストールします。
cargo install rusty-gql-cli
rusty-gql
コマンドを使用してプロジェクトを作成します。
rusty-gql new gql-example
cd gql-example
あとはcargo run
でサーバーを起動するだけです。
GraphQLのスキーマ定義
最初に述べたようにrusty-gqlはSchema Firstで開発することを前提に作成しているので、まずはスキーマを定義します。
schema/**
配下のGraphQLファイルをスキーマとして読み込みます。
プロジェクトを作成した際は次のスキーマがデフォルトで定義されています。
type Query {
todos(first: Int): [Todo!]!
}
type Todo {
title: String!
content: String
done: Boolean!
}
Resolverの実装
Queryのtodosを実装します。
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,
}
}
コード生成
schema.graphql
を編集して、Queryにtodo
を追加してみます。
type Query {
todos(first: Int): [Todo!]!
# added
todo(id: ID!): Todo
}
次のコマンドを実行することでGraphQLスキーマからRustのコードを生成できます。
rusty-gql generate // or rusty-gql g
あとは同様にsrc/graphql/query/todo.rs
を実装するだけです。
Playground
rusty-gqlはGraphiQLのplaygroundをサポートしています。
ブラウザでhttp://localhost:3000/graphiql
にアクセスしてください。
ディレクトリ構造
rusty-gqlはRailsやNext.jsと同じように設定より規約のポリシーで設計しています。
つまり、次のような規約に従ったディレクトリ構造でコードを生成します。
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
ファイル
このディレクトリ配下であれば複数のGraphQLファイルをサポートしています。
src/graphql/query
Query
のresolver実装
src/graphql/mutation
Mutation
のresolver実装
src/graphql/resolver
GraphQLのObject
,Enum
,Union
,Interface
のresolver実装
src/graphql/input
GraphQLのInputObject
の定義
src/graphql/scalar
src/graphql/directive
その他詳しい情報はドキュメントを参照してください。
今後について
まだまだ足りない機能がたくさんあるので、実装予定です。
- Subscription
- Dataloader
- Calculate Query complexity
- Apollo tracing
- Apollo Federation
- Automatic Persisted Query
などなど...
ディレクトリの規約やコード生成もより良いものにしていきたいので、改善・変更する可能性があります。
また、内部実装の話ではあるのですが、GraphQLのparserなどはapollo-rsに変更するかもしれません。
Apolloが最近リリースしたApollo RouterもRust製ですし、今後GraphQL関連でRustのエコシステムがどのように発展していくのか楽しみです。
最後に
rusty-gqlの紹介でした。
まだまだラフな実装で改善点はたくさんあるのですが、今後も引き続き開発していきます。
また、starも押してもらえると開発の励みになるので、ぜひよろしくお願いします!
Discussion
Youtubeも拝見しました。すごくわくわくしています。ありがとうございます。
よかったら教えてください。Prismaはスキーマファーストに対する一般的な問題点を5つ挙げてくれていますが、( https://www.prisma.io/blog/the-problems-of-schema-first-graphql-development-x1mn4cb0tyl3 )
rusty-gqlで解決できている部分はありますか?
あるいは今後スキーマファーストのスタンスでも解決していけるだろうとお考えの点はあったりしますか?
可能なら、返信お待ちしております。