2023-1-13

rust

actix

Posted by

applemango

今回はactix-webの基本的な機能を解説します

actixとは

actix-webはrustの中ではgithubで一番starを獲得しているフレームワークです

ドキュメントも比較的多くあります、また数多くの機能がありやりたい事を簡単に実装出来ます

またスピードが早くTechEmpower Performance Ratingでは100以上のフレームワークがある中ランクが5位と高い順位を記録してます

今回はそんなactixを独断と偏見で必要ないと思った機能と高度な機能をそぎ落としたり追加したりした解説です

プロジェクトを作成する

まずは適当な名前の新しいcargoプロジェクトを作成します

例ではactix-testにしています

$ cargo new actix-test
$ cd actix-test 

コマンドを実行したら以下のようなファイルが作成されます

actix-test

.git

src

main.rs

.gitignore

Cargo.toml

後はCargo.tomlを以下のように変更します

1

2

3

4

5

6

7

8

9

+

[package]
name = "actix-test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "4"

これで後はプログラムを書いて実行するだけです

実行はcargo runで、できます

初回は実行時にライブラリがインストールされるので少し時間がかかります

$ cargo run

まだプログラムを書いてないので

Hello, world!

と表示されるだけですけど

Hello, world!を実行する

main.rsはまだデフォルトの状態ですけど変更しましょう

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

これが最小のアプリです

info

macの場合は14行目を下のように変えてください

14

-

15

+

    .bind(("127.0.0.1", 8080))?.run().await
    .bind("0.0.0.0:8080")?.run().await

#[get("/")]で関数helloはgetでパスが/の時に応答すると定義し

.service(hello)で登録しています

HttpResponseでレスポンスを返しています

#[actix_web::main]マクロはasync main関数をいじくってactixランタイム内で非同期に実行してくれたりする便利なやつです

実行したら

$ cargo run
   Compiling actix-test v0.1.0 (A:\abc\osaka\actix-test)
    Finished dev [unoptimized + debuginfo] target(s) in 32.43s
     Running `target\debug\actix-test.exe`

このようになりhttp://127.0.0.1:8080/にアクセスしたら

response

Hello, world!

response header

HTTP/1.1 200 OK
content-length: 13
date: ___, __ Jan 2023 __:__:__ GMT

このようなレスポンスがかえって来るはずです

ロギング

今のままでも十分良いのですがアクセスしてもログが出力されません

ログが出力されないとちゃんとアクセス出来ているのか不安になるのでログを出力するようにしましょう

まずは出力するためのライブラリを追加します

Cargo.toml

1

2

3

4

5

6

7

8

9

10

+

11

+

12

+

[package]
name = "actix-test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "4"

log = "0.4.0"
env_logger = "0.9.0"

そしてmain.rsにログを出力するミドルウェアを追加しましょう

今回は作成せずにデフォルトで入っているミドルウェアを使います

main.rs

1

-

2

+

3

+

4

5

6

7

8

9

10

11

12

+

13

14

15

16

+

17

18

19

use actix_web::{App, HttpServer, Responder, HttpResponse, get};
use actix_web::{App, HttpServer, Responder, HttpResponse, get, middleware::Logger};
use env_logger::Env;

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(Env::default().default_filter_or("info"));
    HttpServer::new(|| {
        App::new()
            .service(hello)
            .wrap(Logger::default())
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

12行目でロガーを初期化しています

16行目の.warpでミドルウェアを登録しています

今回は全体に適応されますがScope

  (ルーティングで出てくるが/user/list,/user/{id}などを/userのグループに入れて/list/{id}に出来る)

などの限定的な範囲に適応させる事も可能です

これでログを出力するようになりました

実行してみるとこのようになります

$ cargo run
   Compiling actix-test v0.1.0 (A:\abc\osaka\actix-test)
    Finished dev [unoptimized + debuginfo] target(s) in 7.44s
     Running `target\debug\actix-test.exe`
[2023-01-17___:__:___ INFO  actix_server::builder] Starting 8 workers
[2023-01-17___:__:___ INFO  actix_server::server] Actix runtime found; starting in Actix runtime
[2023-01-17___:__:___ INFO  actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 13 "-" "Mozilla/5.0 (__________) AppleWebKit/___._ (KHTML, like Gecko) Chrome/___._._._ Safari/___._" 0.000245

今回はLogger::default()を使用していますがカスタマイズする事も可能です

.wrap(Logger::new("%r %s %D"))

%rなどのフォーマットは色々な種類がありますがここでは一部を紹介します

名前説明結果
%%%%
%aリモートIPアドレス127.0.0.1
%tリクエストの処理が開始された時刻2023-01-17T__:__:__._______Z
%rリクエストの最初の行GET/HTTP/1.1
%sレスポンスのステータスコード200
%bレスポンスの、HTTPヘッダーを含むバイト単位のサイズ13
%Tリクエストの処理にかかった時間(秒)0.000299
%Dリクエストの処理にかかった時間(ミリ秒)0.318400
%{Foo}iリクエストのヘッダー情報%{Accept-Language}iの場合はja
%{Bar}oレスポンスのヘッダー情報
%{FooBar}e環境変数

ルーティング

ルーティングには幾つかの方法があります

属性のようなマクロを使用した方法

これはここまでに登場した方法です

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

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

一番簡単な方法かもしれません

またこの方法でも色々な事が出来ます

現在このようなマクロですが

#[get("/")]

少し方法を変えてみたり

#[route("/", method = "GET", method = "POST")]

あるいは複数のマクロを使ってみたり

#[routes]
#[get("/")]
#[get("/index")]
#[post("/")]

色々な方法があるので試してみるといいかもしれません

routeを使用した方法

こちらは少し複雑かもしれませんが保守性は高いかもしれません

use actix_web::{App, HttpServer, Responder, HttpResponse, web};

async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(hello))
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

デフォルト

デフォルトでは存在しない場所にアクセスされたとき404 Not Foundだけを返しますが

response header

HTTP/1.1 404 Not Found
content-length: 0
date: ___, __ Jan 2023 __:__:__ GMT

その他にも色々返したい時は.default_serviceを使えば解決できます

HttpServer::new(|| {
    App::new()
        .service(hello)
        .default_service(web::to(|| HttpResponse::NotFound()))
})

勿論関数を使う事も出来ます

async fn not_found() -> impl Responder {
    HttpResponse::NotFound().body("Not found!")
}
HttpServer::new(|| {
    App::new()
        .service(hello)
        .default_service(web::to(not_found))
})

そもままでは全体に適応されてしまいますがスコープに適応することで範囲を一部に絞ることもできます

スケープ

スケープは例えば以下のようなページがあったとします

get /
post /user/{id}
get /user/{id}
get /user/{id}/profile
get /user/{id}/follower
get /user/{id}/post
get /user/{id}/すべてのパス

その時愚直に実装したら

HttpServer::new(|| {
    App::new()
        .route("/", web::get().to(hello))
        .route("/user/{id}", web::post().to(hello))
        .route("/user/{id}", web::get().to(hello))
        .route("/user/{id}/profile", web::get().to(hello))
        .route("/user/{id}/follower", web::get().to(hello))
        .route("/user/{id}/post", web::get().to(hello))
})
.bind(("127.0.0.1", 8080))?.run().await

となります、最後のが無いと思われるかもしれませんが最後のはスコープを使わないと無理です

そしてスコープを使ったコードがこちらです

HttpServer::new(|| {
    App::new()
        .route("/", web::get().to(hello))
        .service(
            web::scope("/user/{id}")
                .service(web::resource("")
                    .route(web::post().to(hello))
                    .route(web::get().to(hello))
                )
                .service(web::resource("/profile").route(web::get().to(hello)))
                .route("/follower",web::get().to(hello))
                .route("/post",web::get().to(hello))
                .default_service(web::to(not_found))
        )
})
.bind(("127.0.0.1", 8080))?.run().await

info

上の例では諸事情により"/profile"も.serviceとweb::resourceを使用していますが.route("/profile",web::get().to(hello))でも同じ結果になります(多分)

複雑化したと思うかもしれませんが、階層が分かりやすくなったような気がします

一番最初のweb::resourceの引数が""なのは"/"だと/user/{id}/からしかアクセスできない為です、逆に""だと/user/{id}/からはアクセスできません

地味に初めて(多分)でてきたweb::resourceは同じパターンに応答する時に便利です、例では""にはpostgetの二つが同じ""に応答するのでweb::resourceを使っています

またweb::resource("/profile")などは一つだけですが本家のscopeの例で最初に出てきたので使用しています

勿論.route("/post",web::get().to(hello))みたいな感じも良いと思いますよ?

マクロでスコープを使用する

忘れていましたがマクロでもscopeを使用することができます

#[get("/test")]
async fn test() -> impl Responder {
    HttpResponse::Ok().body("test, world!")
}
HttpServer::new(|| {
    App::new()
        .route("/", web::get().to(hello))
        .service(
            web::scope("/user/{id}")
                .service(test)
        )
})
.bind(("127.0.0.1", 8080))?.run().await

メソッド

これまでgetばかり使ってきましたがほかのメソッドが無いわけではありません

#[get("/")]
#[post("/")]
#[put("/")]
#[delete("/")]
#[head("/")]
#[connect("/")]
#[options("/")]
#[trace("/")]
#[patch("/")]
#[route("/", method = "GET", method = "POST", method = "PUT", method = "DELETE", method = "HEAD", method = "CONNECT", method = "OPTIONS", method = "TRACE", method = "PATCH")]

web::get()なども複数あります

web::get()
web::post()
web::delete()
web::head()
web::connect()
web::options()
web::trace()
web::patch()

CORS

Access to XMLHttpRequest at 'http://127.0.0.1:8080/' from origin 'http://127.0.0.1:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

などとエラーが出た場合CORSが原因である場合が多いです

それにエラーが出なくてもCORSを変えたい時があります

そんな時actix-corsを使えば簡単に解決できます

Cargo.toml

1

2

3

4

5

6

7

8

9

10

+

11

12

13

[package]
name = "actix-test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "4"
actix-cors = "0.6.4"

log = "0.4.0"
env_logger = "0.9.0"

使い方は簡単でミドルウェアのように追加できます

use actix_web::{App, HttpServer, Responder, HttpResponse, get};
use actix_cors::Cors;

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        let cors = Cors::default()
            .send_wildcard()
            .allow_any_origin()
            .allow_any_method()
            .allow_any_header();
        App::new()
            .wrap(cors)
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

今回は特定のoriginだけ許可をするなどがめんどくさいので.allow_any_originなどを使っていますが、本番環境などでは使わない事をお勧めします

その場合.allowed_originなどがあります

今回はcorsメインでは無いので本当にさわりだけ書きましたが詳しくは本家を参照してください

リクエスト

リクエストされたデータにアクセスするには関数の引数を使います

関数ごとに最大12個の引数を追加できます、引数の位置や名前、順番などは関係ありません

パスの値を取り出す

/user/{id}などのpathからデータを得るにはweb::Path<()>を使います、web::Path<()>の中には得たいデータの型を入れてください/user/{id}の場合はweb::Path<(u32)>です

取り出すときはpath.into_inner()を使います、順番はpathと同じです

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

#[get("/user/{id}/post/{title}")]
async fn hello(path: web::Path<(u32, String)>) -> impl Responder {
    let (id, title) = path.into_inner();
    HttpResponse::Ok().body(format!("user id: {}, title: {}", id, title))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

/user/{id}idに文字列などを入れた場合は変換できずエラーメッセージ(can not parse "hello" to a u32(idhelloを入れたため))とともに404が返されます

response header

HTTP/1.1 404 Not Found
content-length: 30
content-type: text/plain; charset=utf-8
date: ___, __ Jan 2023 __:__:__ GMT

またトレイトが必要ですがpathと同じ名前にすれば構造体として取得する事もできます

use actix_web::{App, HttpServer, Responder, HttpResponse, get, web};
use serde::Deserialize;

#[derive(Deserialize)]
struct Paths {
    id: u32,
    title: String,
}

#[get("/user/{id}/post/{title}")]
async fn hello(path: web::Path<Paths>) -> impl Responder {
    HttpResponse::Ok().body(format!("user id: {}, title: {}", path.id, path.title))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

serde::Deserializeを使用するにはserdeが必要です

Cargo.toml

1

2

3

4

5

6

7

8

9

10

11

12

13

14

+

[package]
name = "actix-test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "4"

log = "0.4.0"
env_logger = "0.9.0"

serde = { version = "1.0", features = ["derive"] }

クエリ文字列を取り出す

クエリ文字列はhttps://abc.osaka?ref=google.com&theme=darkref=google.com&theme=dark部分の事ですが取得するにはweb::Queryを使います

またこちらもトレイトが必要になります

use actix_web::{App, HttpServer, Responder, HttpResponse, get, web};
use serde::Deserialize;

#[derive(Deserialize)]
struct Queries {
    name: String,
}

#[get("/")]
async fn hello(info: web::Query<Queries>) -> impl Responder {
    HttpResponse::Ok().body(format!("name: {}", info.name))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

もし指定したクエリ文字列が見つからない場合は400,Query deserialize error: missing field 'name'エラーが出るので注意が必要です

クエリ文字列にoptionを使用する

上の方法でも良いのですがクエリ文字列が見つからない場合に400を返してしまいます、解決策としてはoption型を使う事です

#[derive(Deserialize)]
struct Queries {
    name: Option<String>,
}

これでクエリ文字列にnameが含まれない場合も400を返すことが無くなりました

後は普通に使えます

#[get("/")]
async fn hello(info: web::Query<Queries>) -> impl Responder {
    if let Some(name) = &info.name {
        return HttpResponse::Ok().body(format!("hey {}!", name))
    }
    HttpResponse::Ok().body("what is your name?")
}

任意のクエリ文字列を取得する

また任意のクエリ文字列を取得する方法もあります、この場合クエリ文字列をHashMapで取得できます

#[get("/")]
async fn hello(info: web::Query<HashMap<String, String>>) -> impl Responder {
    HttpResponse::Ok().body(format!("name: {:#?}", info))
}

もちろん型を変える事も出来ます例えば数字にしたりweb::Query<HashMap<String, u32>>

bodyの値を取り出す

値を取り出すにはweb::Json<>を使います

基本的にはweb::Queryと同じように取得できます

optionHashMap<String, String>なども使えます

#[derive(Deserialize)]
struct Bodies {
    name: Option<String>,
}

#[post("/")]
async fn hello(info: web::Json<Bodies>) -> impl Responder {
    if let Some(name) = &info.name {
        return HttpResponse::Ok().body(format!("hey {}!", name))
    }
    HttpResponse::Ok().body("what is your name?")
}

平文で取り出す

あまりお勧めはしませんがもしあなたが物好きなら平文で取り出す事も可能です

async fn hello(body: String) -> impl Responder {
    println!("{}", body);
    HttpResponse::Ok().body("Hello, world!")
}

ヘッダーの値を取り出す

こちらは扱いが少し不便ですがよく使うことになります

web::Headerを使う方法

この方法では簡単に取得することはできますが自由度が低いです

取得するデータを変更するにはheader::ContentTypeを変える必要があります

#[get("/")]
async fn hello(info: web::Header<header::ContentType>) -> impl Responder {
    HttpResponse::Ok().body(format!("{}", info.to_string()))
}

HttpRequestを使う方法

この方法では少し複雑になりますが好きな値を取得することができます

認証ヘッダーなどを取得するにはこの方法が良いでしょう

#[get("/")]
async fn hello(req: HttpRequest) -> impl Responder {
    if let Some(x) = req.headers().get("User-Agent") {
        let x = x.to_str().unwrap();
        return HttpResponse::Ok().body(format!("{}", x))
    }
    HttpResponse::Ok().body("User-Agent not found")
}

画像やファイルを受け取る

画像やファイルを取り出す場合は次のようにします

async fn save_file(req: HttpRequest, mut payload: web::Payload) -> HttpResponse {
    let mut file = File::create("test.png").unwrap();

    while let Some(chunk) = payload.try_next().await.unwrap() {
        file.write_all(&chunk).unwrap();
    }

    HttpResponse::Ok().body("File saved successfully")
}

HttpRequest

あまり使う事は無いかもしれませんが、色々な値にアクセスできます

上のヘッダーの値を取得する時に使う事が多い気がします

async fn hello(req: HttpRequest) -> impl Responder {
    println!("req : {:#?}", req);
    println!("uri : {:#?}", req.uri());
    println!("method : {:#?}", req.method());
    println!("version : {:#?}", req.version());
    println!("headers : {:#?}", req.headers());
    println!("path : {:#?}", req.path());
    println!("query_string : {:#?}", req.query_string());
    println!("match_info : {:#?}", req.match_info());
    println!("match_pattern : {:#?}", req.match_pattern());
    println!("match_name : {:#?}", req.match_name());
    println!("cookies : {:#?}", req.cookies());
    HttpResponse::Ok()
}
req :
HttpRequest HTTP/1.1 POST:/
  headers:
    "content-length": "13"
    "user-agent": "HTTPie"
    "connection": "close"
    "content-type": "application/json"
    "host": "127.0.0.1:8080"

uri : /
method : POST
version : HTTP/1.1
headers : HeaderMap {
    inner: {
        "content-length": Value {
            inner: [
                "13",
            ],
        },
        "user-agent": Value {
            inner: [
                "HTTPie",
            ],
        },
        "connection": Value {
            inner: [
                "close",
            ],
        },
        "content-type": Value {
            inner: [
                "application/json",
            ],
        },
        "host": Value {
            inner: [
                "127.0.0.1:8080",
            ],
        },
    },
}
path : "/"
query_string : ""
match_info : Path {
    path: Url {
        uri: /,
        path: None,
    },
    skip: 0,
    segments: [],
}
match_pattern : None
match_name : None
cookies : Ok(
    [],
)

レスポンス

HttpResponse

レスポンスには基本的にHttpResponseを使います

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok()
}

色々な種類がありますがここではよく使ういくつかを紹介しましょう

HttpResponse::Ok()
HttpResponse::Created()
HttpResponse::BadRequest()
HttpResponse::MethodNotAllowed()
HttpResponse::Unauthorized()
HttpResponse::NotFound()
HttpResponse::TooManyRequests()

これだけでもいいですがこれではただステータスコードを送信するだけです

.insert_header.bodyを付ける事でbodyやheaderをカスタマイズできます

HttpResponse::Ok()
    .insert_header(("Content-Type", "text/plain"))
    .body("hello")

Jsonを返す

jsonを返すにはSerializeのトレイトのついた構造体を使う必要があります

#[derive(Serialize)]
pub struct User {
    pub id: i32,
    pub name: String,
}

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok()
        .json(User {
            id: -1,
            name: "example".to_string()
        })
}

複数ある場合はVecが使えます

#[get("/")]
async fn test() -> impl Responder {
    let mut all = Vec::new();
    all.push(User {
        id: 1,
        name: "apple".to_string()
    });
    all.push(User {
        id: 2,
        name: "osaka".to_string()
    });
    HttpResponse::Ok()
        .json(all)
}

因みにSerializeserdeからインポートできます

use serde::{Deserialize, Serialize};

serde自体はCargo.toml[dependencies]のしたに一行追加すれば使えます

1

2

+

3

[dependencies]
serde = { version = "1.0", featurs = ["derive"] }
actix-web = "4"

画像を返す

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

#[get("/image/{path}")]
async fn image(path: web::Path<String>) -> impl Responder {
    let name = path.into_inner();
    let image = web::block(move || std::fs::read(format!("./static/{}", name)).unwrap()).await.unwrap();
    HttpResponse::Ok()
        .content_type("image/jpeg")
        .body(image)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(image)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

少し複雑なのでコードの解説をしましょう

まず、web::blockは時間がかかる処理をしている時にアクセスが来た時、通常二つ目のアクセスは一つ目の処理が終わるのを待つ必要があるのですが、web::blockを使う事で二つ目のアクセスを処理しながら、一つ目のアクセスの処理が終わるのを待つことで二つ目のアクセスが待つ必要が無くなることで全体的な速度が向上します

そしてstd::fs::readでファイルを読み込んでいます

こうして無事に読み込まれたファイルをbodyに入れてレスポンスを返しています

.content_typeはレスポンスを受け取ったブラウザなどに画像である事を伝えています、最近のブラウザは賢いので画像である事を認識しますがあまり賢くない時もあるので一応つけています

上のプログラムはもし画像が見つからない場合場合サーバー側でエラーが発生して何も返しませんがもしサーバーが応答しないのが嫌なのならmatchなどでエラー処理をするべきです

#[get("/image/{path}")]
async fn image(path: web::Path<String>) -> impl Responder {
    let name = path.into_inner();
    let image = match web::block(move || std::fs::read(format!("./static/{}", name))).await {
        Ok(image) => match image {
            Ok(image) => image,
            Err(__) => return HttpResponse::NotFound().body("File not found")
        },
        Err(__) => return HttpResponse::NotFound().body("Blocking Error")
    };
    HttpResponse::Ok()
        .content_type("image/jpeg")
        .body(image)
}

言い忘れていましたがこのプログラムは以下のようなディレクトリ構成である事を前提に書いているので、formt!("./static/{}")の場所は環境に合わせて変更してください

actix-test

.git

scr

main.rs

static

img.png

img2.png

img3.jpg

.gitignore

Cargo.lock

Cargo.toml

データベース

本当は分けようと思ったのですが既に(/rust/db)解説しているのでさわりだけ解説します、そのため少しスピードをあげます

今回はsqlite3rusqliteを使って実装します

まずいつものようにライブラリをプロジェクトに追加します

Cargo.toml

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

+

17

+

18

+

[package]
name = "rusqlite"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1.0", features = ["derive"] }

actix-web = "4"

log = "0.4.0"
env_logger = "0.9.0"

[dependencies.rusqlite]
version = "*"
features = ["bundled"]

そしていつものHello, world!を書きます

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

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

そこにライブラリをインポートし

1

-

2

+

3

+

4

+

use actix_web::{App, HttpServer, Responder, HttpResponse, get};
use actix_web::{web, App, HttpServer, Responder, HttpResponse, get, post};
use rusqlite::Connection;
use serde::{Serialize, Deserialize};

dbと接続し、ない場合は自動的に作成されるプログラムを書きます

11

12

13

+

14

15

16

17

18

19

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let conn = Connection::open("app.db").unwrap();
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

そしてテーブルを作成するプログラミングを書き

11

12

13

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

24

25

26

27

28

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let conn = Connection::open("app.db").unwrap();
    conn.execute(
        "CREATE TABLE IF NOT EXISTS post (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            title       STRING,
            body        STRING
        )",
        ()
    ).unwrap();

    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?.run().await
}

ハンドラーと色々するプログラムを書いたら完成です

3

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

54

use serde::{Serialize, Deserialize};

#[derive(Serialize)]
struct Post {
    id: i32,
    title: String,
    body: String,
}

#[derive(Deserialize)]
struct CreatePostRequest {
    title: String,
    body: String,
}

#[post("/create")]
async fn create_post(body: web::Json<CreatePostRequest>) -> impl Responder {
    let conn = Connection::open("app.db").unwrap();
    let mut stmt = conn.prepare("INSERT INTO post ( title, body ) values ( ?1, ?2 )").unwrap();
    let _ = stmt.execute([&body.title, &body.body]).unwrap();
    let post = conn.query_row("SELECT id, title, body FROM post WHERE id = last_insert_rowid()", [], |row| {
        Ok(Post {
            id: row.get(0)?,
            title: row.get(1)?,
            body: row.get(2)?
        })
    }).unwrap();
    HttpResponse::Ok().json(post)
}

#[get("/")]
async fn get_post() -> impl Responder {
    let conn = Connection::open("app.db").unwrap();
    let mut stmt = conn.prepare("SELECT id, title, body FROM post").unwrap();
    let posts = stmt.query_map([], |row| {
        Ok(Post {
            id: row.get(0)?,
            title: row.get(1)?,
            body: row.get(2)?
        })
    }).unwrap();
    let mut posts_vec = Vec::new();
    for item in posts {
        if let Ok(item) = item {
            posts_vec.push(item)
        }
    }
    HttpResponse::Ok().json(posts_vec)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {

終わり

これで一通り出来ました、多分

リンク

actixのgithub

actixのdocs.rs

このドキュメントどう?

emoji
emoji
emoji
emoji