はじめに
前回の記事でRustでSQLサーバに接続することができたので、次はRocketを使って簡単なapiサーバーを作って見たいと思います。
(2021/06/15 公式サンプルを参考に少し修正しました)
(2021/06/21 CORS対応のための記事リンクを追記しました)
環境
- Ubuntu-20.04 on WSL2
- rustc 1.54.0-nghtly(2021-05-28)
- rocket 0.5.0-rc.1
- sqlx 0.5.5
- dotenv 0.15
MySQLサーバーは構築済みで、サンプルとしてuserテーブルにid(INT)とname(VARCHAR)カラムがあるものとします。
リクエストにはx-api-key:123456というヘッダを必要とし、http://localhost:8000/allで全件を取得してjsonで返すサーバーをつくります。(公式のサンプルコードをほぼそのまま使っています)
サンプルコード
[package]
name = "api-server"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = { version = "0.5.0-rc.1", features = ["json"] }
rocket_sync_db_pools = "0.1.0-rc.1"
sqlx = { version = "0.5",default-features = true, features = [ "runtime-async-std-native-tls", "mysql"] }
dotenv = "0.15"
今回はdotenvを使ってDBの接続情報等を管理してみます。ついでに簡単なセキュリティー対策としてリクエストにはAPIキーを必須とするため、API_KEYも設定しておきます。
MYSQL_HOST="サーバー名"
MYSQL_DB="DB名"
MYSQL_USER="ユーザー名"
MYSQL_PASSWORD="パスワード"
API_KEY="123456"
見通しを良くするためルーターとモデルをサブモジュールとして作成します。
use crate::models::*; // main.rcで読み込みます。
use rocket::http::Status;
use rocket::request::{Outcome,Request,FromRequest};
use rocket::serde:::{Serialize,Deserialize,json::Json};
//APIキーの構造体を用意します
pub struct ApiKey<'r>(&'r str);
//APIキーのエラー時に返すステータスをenumで用意します。
#[derive(Debug)]
pub enum ApiKeyError {
Missing,
Invalid,
}
// SQLの成否に関わらずJSONで結果を返すため、成功、失敗のenumを定義します。
#[derive(Debug,Serialize,Deserialize)]
#[serde(crate="rocket::serde")]
enum ResBody<T:Serialize> {
OK(T),
NG(String)
}
// レスポンスの型を定義します
#[derive(Debug,Serialize,Deserialize)]
#[serde(crate="rocket::serde")]
struct ApiResponse<T:Serialize> {
success: bool,
data:ResBody<T>
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for ApiKey<'r>{
type Error = ApiKeyError;
async fn from_request(req: &'r Request<'_> ) -> Outcome<Self,Self::Error> {
// .envから読み込んだAPI_KEYの値とリクエストヘッダの値をチェックします。
fn is_valid(key:&str) -> bool {
key == std::env::var("API_KEY").unwrap()
}
// リクエストヘッダーがなければMissing,リクエストヘッダの値が異なる場合などその他のエラーはInvalidステータスを返します。
match req.headers().get_one("x-api-key") {
None => Outcome::Failure((Status::BadRequest, ApiKeyError::Missing)),
Some(key) if is_valid(key) => Outcome::Success(ApiKey(key)),
Some(_) => Outcome::Failure((Status::BadRequest, ApiKeyError::Invalid)),
}
}
}
#[get("/")]
pub fn index() -> &'static str {
"リクエストにはAPIキーが必要です。"
}
#[get("/all")]
pub async fn all(_key:ApiKey<'_>,db: &State<sqlx::MySqlPool>) -> Json<ApiResponse<Vec<Userdata>>> {
let r= fetch_all_data().await;
match r {
Ok(val) => {Json(ApiResponse {success:true,data:ResBody::OK(val)})},
Err(e) => {Json(ApiResponse {success:false,data:ResBody::NG(e.to_string())}}
}
}
//ちなみに下記のようにするとhttp:localhost:8000/api?id=123&name=fooのようなクエリ文字列が使えます
#[get("/search?<id>&<name>")]
pub async fn search(_key:ApiKey<'_>,db: &State<sqlx::MySqlPool,id: u32, name: String) -> Json<ApiResponse<Vec<Userdata>> {
match r {
Ok(val) => {Json(ApiResponse {success:true,data:ResBody::OK(val)})},
Err(e) => {Json(ApiResponse {success:false,data:ResBody::NG(e.to_string())}}
};
}
use rocket::serde::{Deserialize,Serialize};
//DBのカラム名と方にあわせて構造体を用意します
#[derive(Debug,Serialize,Deserialize,sqlx::FromRow)]
pub struct Userdata {
pub id:i64,
pub name: String
}
//DBに接続してデータを読み込みます
pub async fn fetch_data(db:&rocket::State<sqlx::MySqlPool>)->sqlx::Result<Vec<Userdata>> {
let query = format!("SELECT id , name from users;");
sqlx::query_as::<_,Userdata>(&query).fetch_all(&**db).await
}
pub async fn search(db:&rocket::State<sqlx::MySqlPool>,id:i64,name:String)->sqlx::Result<Vec<Userdata>>{
let query = format!("SELECT id, name FROM users WHERE id ={} AND name = {}",id,name);
sqlx::query_as::<_,Userdata>(&query).fetch_all(&**db).await
}
#![feature(proc_macro_hygiene,decl_macro)]
#[macro_use] extern crate rocket;
//上のファイルをサブモジュールとして読み込みます。
mod routes;
mod models;
use rocket::fairing::{self, AdHoc};
use rocket::{Build,Rocket};
use dotenv::dotenv;
use routes::{api,search};
#[launch]
fn rocket() -> _ {
dotenv().ok();
rocket::build()
.attach(AdHoc::try_on_ignite("init SQLx Database",init_db ))
.mount("/",routes![index,all,search])
}
async fn init_db(rocket: Rocket<Build>) -> fairing::Result {
let opt = sqlx::mysql::MySqlConnectOptions::new()
.host(&std::env::var("MYSQL_HOST").unwrap())
.ssl_mode(sqlx::mysql::MySqlSslMode::Disabled)
.database(&std::env::var("MYSQL_DB").unwrap())
.username(&std::env::var("MYSQL_USER").unwrap())
.password(&std::env::var("MYSQL_PASSWORD").unwrap());
let db = match sqlx::MySqlPool::connect_with(opt).await {
Ok(pool) => pool,
Err(e) => {
error!("Failed to connenct to SQLx database: {}", e);
return Err(rocket);
}
};
Ok(rocket.manage(db))
}
サーバーを立ち上げます
$ cargo run
curlで確認してみます。
$ curl -i -H 'x-api-key:123456' http://localhost:8000/all
無事JSONが返ってくればひとまず完成です。
(続き)CORSに対応する
2021/06/21追記
クロスドメインに対応するためには追加の手順が必要です。
下記の記事もご確認ください
まとめ
Rocketは公式のドキュメントも英語ではありますがサンプルが豊富でわかりやすく、デフォルトでasync-awaitに対応するなど「全部入り」なので慣れれば使いやすそうです。