こんにちは、とらラボのY.Fです。
先日、こんな記事を書きました。
toranoana-lab.hatenablog.com
本を読んだだけでは寂しいので、実際に作ってみたいと思います。
今回の記事では導入編として、DBなどを利用しないGraphQLサーバーを立ててみようと思います。
ちなみに、著者はRust初心者に毛が生えたような感じなのでツッコミも歓迎です!
環境
今回メインで使うものは以下のようになります。
- rustup 1.12.1
- Rust本体とツールチェインをインストールするためのツール
- Rust 1.42.0
- 言語本体
- actix-web 2.0系
- Webフレームワーク
- juniper 0.14.2
- Rust用GraphQLライブラリ
GraphQLサーバーを作る
ということで、作って行きたいと思います。
rustupを使った言語のインストールなどは割愛します。
rustupについては以下を参照してください。
1.プロジェクト作成と依存関係のインストール
Rustに付随するCargoを使ってプロジェクトを作成します。プロジェクト名は graphql-sample
にします。
作ったらそのディレクトリに移動しておきます。
$ cargo new graphql-sample
$ cd graphql-sample
ディレクトリにある Cargo.toml
ファイルに以下依存関係を追記します。
[package] name = "graphql-sample" version = "0.1.0" authors = ["内緒だよ"] edition = "2018" [dependencies] # 以下追記 actix-web = "2" actix-rt = "1" juniper = "0.14" dotenv = "*" env_logger = "*" log = "*" serde = { version = "*", features = ["derive"] } serde_json = "*"
以下コマンドで依存解決します。
$ cargo update Updating crates.io index
2.Hello World
とりあえずactix-webでHello Worldしたいと思います。
src/main.rs
を以下のように書き換えます。
(もともとの内容)
fn main() { println!("Hello, world!"); }
(書き換え後)
use actix_web::{middleware, App, HttpServer, web, Responder}; use dotenv::{dotenv, from_filename}; /// Hello World!を出力するだけのハンドラ関数 async fn hello_world() -> impl Responder { "Hello World!" } #[actix_rt::main] async fn main() -> std::io::Result<()> { if cfg!(debug_assertions) { // debugのときは.env.localファイルを読み込み from_filename(".env.local").ok(); } else { dotenv().ok(); } env_logger::init(); let mut server = HttpServer::new(move || { App::new() .wrap(middleware::Logger::default()) .route("/", web::get().to(hello_world)) }); server = server.bind("127.0.0.1:3000").unwrap(); server.run().await }
本題では無いので、細かいソースの説明は割愛します。
hello_world
関数をURLの /
にバインドしているだけです。
また、actix-web2系を利用しているので、Rustの async/await
を利用することができます。
これで、以下コマンドを実行し、 http://127.0.0.1:3000
にアクセスすると "Hello World!" がブラウザに表示されます。
$ cargo run
3. juniper導入
juniper用の関数を作っておきます。
src/graphql/schema.rs
ファイルを作成し、以下の内容を記述します。
use juniper::{FieldResult}; /// GraphQLの型の素になるPhoto構造体 #[derive(Clone, Debug)] pub struct Photo { id: String, name: String, description: String } /// 実際にGraphQLとしての型になるのは以下アトリビュートがついているこちら #[juniper::object] #[graphql(description = "A Project returns struct")] impl Photo { pub fn id(&self) -> String { self.id.clone() } pub fn name(&self) -> String { self.name.clone() } pub fn description(&self) -> String { self.description.clone() } pub fn url(&self) -> String { format!("http://hogehoge/{}", self.id) } } pub struct Query; pub struct Mutation; /// 各GraphQLのリゾルバの引数に与えられるコンテキスト /// 今は仮実装で起動時に与えられたPhotoのベクタだけ持つ #[derive(Clone, Debug)] pub struct Context { pub photos: Vec<Photo>, } impl juniper::Context for Context {} /// GraphQLのクエリ系リゾルバ #[juniper::object(Context = Context)] impl Query { fn all_photos(&self, context: &Context) -> FieldResult<Vec<Photo>> { Ok(context.photos.clone()) } } /// GraphQLのミューテーション系(更新系)リゾルバ #[juniper::object(Context = Context)] impl Mutation {} pub type Schema = juniper::RootNode<'static, Query, Mutation>; pub fn create_schema() -> Schema { Schema::new(Query {}, Mutation {}) }
Photo
構造体をGraphQLの型として利用するようにしています。
また、メソッドを定義することで、urlフィールドを取得する場合には、構造体の値に従って自動で任意のURLを生成できるようにしています。
4. juniperとactixを合体
actix-webとjuniperを合体します。
以下サンプルがあるので参考にしました。
main.rsを以下のように追記します。
use crate::graphql::schema::{create_schema, Context, Photo, Schema}; use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer, Responder}; use dotenv::{dotenv, from_filename}; use juniper::http::graphiql::graphiql_source; use juniper::http::GraphQLRequest; use std::sync::Arc; pub mod graphql; impl Photo { fn new(id: String, name: String, description: String) -> Photo { Photo { id, name, description, } } } /// actixからGraphQLにアクセスするためのハンドラメソッド pub async fn graphql( st: web::Data<Arc<Schema>>, data: web::Json<GraphQLRequest>, ) -> Result<HttpResponse, Error> { let user = web::block(move || { let res = data.execute( &st, &Context { photos: vec![ Photo::new( "1".to_string(), "test1".to_string(), "test1 photo".to_string(), ), Photo::new( "2".to_string(), "test2".to_string(), "test2 photo".to_string(), ), Photo::new( "3".to_string(), "test3".to_string(), "test3 photo".to_string(), ), Photo::new( "4".to_string(), "test4".to_string(), "test4 photo".to_string(), ), Photo::new( "5".to_string(), "test5".to_string(), "test5 photo".to_string(), ), ], }, ); // serde_jsonで実際はエラーになる可能性があるのでOkのturbofishに指定 // 実際これでエラーになる場合はawait?によって早期returnされてErrがreturnされる Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?) }) .await?; Ok(HttpResponse::Ok() .content_type("application/json") .body(user)) } /// actixからGraphiQLにアクセスするためのハンドラメソッド pub async fn graphiql() -> HttpResponse { let html = graphiql_source("http://127.0.0.1:3000/graphql"); HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(html) } pub async fn hello_world() -> impl Responder { "Hello World!" } #[actix_rt::main] async fn main() -> std::io::Result<()> { if cfg!(debug_assertions) { // debugのときは.env.localファイルを読み込み from_filename(".env.local").ok(); } else { dotenv().ok(); } env_logger::init(); // 追加 // juniperのschemaを共有してGraphQL用のハンドラに引き渡す let schema = std::sync::Arc::new(create_schema()); let mut server = HttpServer::new(move || { App::new() .data(schema.clone()) .wrap(middleware::Logger::default()) .route("/", web::get().to(hello_world)) .route("/graphiql", web::get().to(graphiql)) // 追加 .route("/graphql", web::post().to(graphql)) // 追加 }); server = server.bind("127.0.0.1:3000").unwrap(); server.run().await }
今回はDBを利用してないので、アプリケーション起動時に5個のPhoto構造体を作るようにしました。
この状態で、 http://http://127.0.0.1:3000/graphiql
にアクセスすると以下のような画面が表示されます。
これは、GraphiQLと呼ばれる、ブラウザ上でGraphQLのクエリを試せるツールです。
juniper独自のものではなく、GraphQL Foundationが提供する公式ツールになります。
このエディタの左側に以下のようなクエリを書き込むと、更に次の様に結果が確認できます。
(クエリ)
query { allPhotos { id name url description } }
(結果)
まとめ
*.graphql
ファイルから、構造体などを自動生成するクレートなどもあるのですが、依存を増やしたくないのも有り、今回は使用していません。
全体としては、データの取得までを実施しました。残りの話題としては、データ更新系のMutationや、各GraphQLの型(Enumなど)をRustとしてどう表現するかなどあります。
また、RDBとの連携なども今回は触れていません。
前回のWebAssemblyの記事のように連載していきたいと思いますので、ぜひご期待ください。
P.S
残念ながら、COVID-19の感染被害の防止の為中止となった技術書典ですが、虎の穴ラボでは用意していた同人誌を技術書典 応援祭にて0円にて頒布しております。 ぜひ御覧ください。 techbookfest.org
加えて今回は、有償版の同人誌も作成しております。Go、Kotlin、Rustに関連する内容をまとめた、内容の濃い薄い本になります。 こちらも、ぜひ入手してお楽しみください。 techbookfest.org
また、「まず業務について聞いてみたい」という方は、カジュアル面談も実施していますので、こちらもフォームから申込みをお願いします。
虎の穴ラボのエンジニアが、ざっくばらんに業務についての質問から「今季何見ました?」というお話まで何でもお応えします。