Open8

Actix WebでAPI作る

水無瀬水無瀬

CRUDのリクエストを受け付ける

対応するリクエストメソッドを指定すれば良い。
カッコ内にパスを書く。(今回は全部ルート)

use actix_web::{get, post, put, delete, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn get() -> impl Responder {
    HttpResponse::Ok().body("get ok")
}
#[post("/")]
async fn post() -> impl Responder {
    HttpResponse::Ok().body("post ok")
}
#[put("/")]
async fn put() -> impl Responder {
    HttpResponse::Ok().body("put ok")
}
#[delete("/")]
async fn delete() -> impl Responder {
    HttpResponse::Ok().body("delete ok")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
        .service(get)
        .service(post)
        .service(put)
        .service(delete)
    })
    // ローカルホストのport8080で起動
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

水無瀬水無瀬

パスから値を取る

use actix_web::{get, web, HttpResponse, Responder};

#[get("/users/{user_id}")]
async fn get(web::Path(user_id): web::Path<String>) -> impl Responder {
    HttpResponse::Ok().body(format!("user_id is {}", user_id))
}

// mainはさっきと同じ
$ curl http://localhost:8080/users/hoge
> user_id is hoge

複数の場合

#[get("/users/{user_id}/articles/{article_id}")]
async fn get_article(web::Path((user_id, article_id)): web::Path<(String, i32)>) -> impl Responder {
    HttpResponse::Ok().body(format!("user_id is {}. article_id is {}.", user_id, article_id))
}
$ curl http://localhost:8080/users/hoge/articles/1000
> user_id is hoge. article_id is 1000.
水無瀬水無瀬

Query Parameterを取得する

#[derive(Deserialize)]
struct ArticleQuery {
    start: i32,
    results: i32
}

#[get("/articles")]
async fn get_articles(query: web::Query<ArticleQuery>) -> impl Responder {
    HttpResponse::Ok().body(format!("start is {}. results is {}.", query.start, query.results))
}
$ curl http://localhost:8080/articles?start=1&results=100
> start is 1. results is 100.
水無瀬水無瀬

bodyを取る

#[derive(Deserialize)]
struct User {
    user_id: String,
    name: String,
    age: i32
}

#[post("/user")]
async fn post(user_data: web::Json<User>) -> impl Responder {
    HttpResponse::Ok().body(format!("user_id is {}. name is {}. age is {}.", user_data.user_id, user_data.name, user_data.age))
}
$ curl -X POST -H "Content-Type: application/json" -d '{"user_id": "user", "name": "user", "age": 20}' localhost:8080/user
> user_id is user. name is user. age is 20.
水無瀬水無瀬

JSON形式のレスポンス

#[derive(Deserialize)]
struct User {
    user_id: String,
    name: String,
    age: i32
}
#[derive(Serialize)]
struct UserResponse {
    user_id: String,
    name: String,
    age: i32
}
impl Responder for UserResponse {
    type Error = Error;
    type Future = Ready<Result<HttpResponse, Error>>;
    fn respond_to(self, _req: &HttpRequest) -> Self::Future {
        let body = serde_json::to_string(&self).unwrap();
        ready(Ok(HttpResponse::Ok().content_type("application/json").body(body)))
    }
}

#[post("/user")]
async fn post(user_data: web::Json<User>) -> impl Responder {
    UserResponse{ user_id: user_data.user_id.clone(), name: user_data.name.clone(), age: user_data.age }
}
水無瀬水無瀬

httpリクエストを送る

actixではデフォでHttp Client Libraryが入っているのでそれを使い、httpリクエストが送れるところまで試す。

Cargo.toml更新

デフォで使えるので更新する必要は無いが、httpsに対応していないのでそれだけできるようにする

Cargo.toml
[dependencies]
actix-web = {version = "3.3.0", features = ["openssl"]}
openssl = "0.10.28"

httpリクエストを送るとこ作成

ファイル分割してみる

http_client.rs
pub mod http_client_module {
  use actix_web::client::{Client, Connector};
  use openssl::ssl::{SslConnector, SslMethod};

  pub async fn get() -> String {
    let builder = SslConnector::builder(SslMethod::tls()).unwrap();
    let client = Client::builder()
        .connector(Connector::new().ssl(builder.build()).finish())
        .finish();
    let result = client
      // 適当なjsonを返すサーバー
      .get("https://sample-domain/api/v1/json")
      .send()
      .await
      .unwrap()
      .body()
      .limit(20_000_000) // 最大payloadサイズ
      .await
      .unwrap();
   
    result.iter().map(|&s| s as char).collect::<String>()
  }
}

main.rs修正

main.rs
mod http_client;

#[get("/json")]
async fn get_json() -> impl Responder {
    let result = http_client::http_client_module::get().await;
    HttpResponse::Ok()
    .content_type("application/json")
    .body(result)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(get)
            .service(get_article)
            .service(get_articles)
            // ここを追加
            .service(get_json)
            .service(post)
            .service(put)
            .service(delete)
    })
    // ローカルホストのport8080で起動
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

参考

水無瀬水無瀬

DB用意する

dockerでmysql用意

# 必要なファイルの準備
$ touch docker-compose.yaml
$ mkdir db
# mysql用Dockerfile
$ touch db/Dockerfile
# 初期設定用SQL
$ touch db/init.sql
# mysql設定用
$ touch db/my.cnf

ファイルの中身

docker-compose.yaml
version: '3'

services: 
  db:
    build: ./db/
    volumes: 
      - ./db:/docker-entrypoint-initdb.d
    environment:
      - MYSQL_ROOT_PASSWORD=hogehoge
Dockerfile
FROM mysql
EXPOSE 3306
COPY ./my.cnf /etc/mysql/conf.d/my.cnf
CMD ["mysqld"]
init.sql
CREATE DATABASE sample_db;
use sample_db;

CREATE TABLE users (
  id int(10) unsigned not null auto_increment,
  name varchar(255) not null,
  created_time datetime not null default current_timestamp,
  updated_time datetime not null default current_timestamp on update current_timestamp,
  primary key (id)
);
my.cnf
[mysqld]
character-set-server=utf8

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8

参考

水無瀬水無瀬

アプリケーション側のdockerfile

FROM rust AS builder
WORKDIR /actix-api-sample
COPY Cargo.toml Cargo.toml
COPY Cargo.lock Cargo.lock

RUN mkdir src &&\
    echo "fn main(){}" > src/main.rs &&\
    cargo build --release
COPY ./src ./src

RUN rm -f target/release/deps/actix-api-sample* &&\
    cargo build --release 

FROM debian

COPY --from=builder /actix-api-sample/target/release/actix-api-sample /usr/local/bin/actix-api-sample
CMD ["actix-api-sample"]