MENU

RustでGraphQLやってみるその1(導入編)

こんにちは、とらラボの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については以下を参照してください。

rustup.rs

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

f:id:toranoana-lab:20200325151209p:plain
表示される画面

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を合体します。
以下サンプルがあるので参考にしました。

github.com

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 にアクセスすると以下のような画面が表示されます。

f:id:toranoana-lab:20200325182422p:plain

これは、GraphiQLと呼ばれる、ブラウザ上でGraphQLのクエリを試せるツールです。
juniper独自のものではなく、GraphQL Foundationが提供する公式ツールになります。
このエディタの左側に以下のようなクエリを書き込むと、更に次の様に結果が確認できます。

(クエリ)

query {
  allPhotos {
    id
    name
    url
    description
  }
}

(結果) f:id:toranoana-lab:20200325183538p:plain

まとめ

*.graphql ファイルから、構造体などを自動生成するクレートなどもあるのですが、依存を増やしたくないのも有り、今回は使用していません。
全体としては、データの取得までを実施しました。残りの話題としては、データ更新系のMutationや、各GraphQLの型(Enumなど)をRustとしてどう表現するかなどあります。
また、RDBとの連携なども今回は触れていません。
前回のWebAssemblyの記事のように連載していきたいと思いますので、ぜひご期待ください。

P.S

残念ながら、COVID-19の感染被害の防止の為中止となった技術書典ですが、虎の穴ラボでは用意していた同人誌を技術書典 応援祭にて0円にて頒布しております。 ぜひ御覧ください。 techbookfest.org

加えて今回は、有償版の同人誌も作成しております。Go、Kotlin、Rustに関連する内容をまとめた、内容の濃い薄い本になります。 こちらも、ぜひ入手してお楽しみください。 techbookfest.org

また、「まず業務について聞いてみたい」という方は、カジュアル面談も実施していますので、こちらもフォームから申込みをお願いします。
虎の穴ラボのエンジニアが、ざっくばらんに業務についての質問から「今季何見ました?」というお話まで何でもお応えします。

WantedlyLAPLASでの採用も行っております)

yumenosora.co.jp

news.toranoana.jp