【Rust】Unitテストでtype_name_of_valを使って型をチェックする

std::any::type_name::のassertの使い方がよくわからない。type_name_of_valだと、簡単にチェックできる。
https://doc.rust-lang.org/beta/std/any/fn.type_name_of_val.html

use std::any::type_name_of_val;

#[tokio::main]
async fn main() {
    let aws_credential = check_aws_credential().await;
    print_typename(aws_credential);
    assert!(type_name_of_val(&aws_credential).contains("bool"));
}

fn print_typename<T>(_: T) {
    println!("{}", std::any::type_name::<T>());
}

assert_eq!(true || false, hoge); みたいなことはできない。この場合、leftはtrueになってしまう。

【Rust】.envファイルの設定有無をチェックする

.envの設定の有無によって、処理を切り替える。is_empty()で値がセットされているか確認ができる。

#[tokio::main]
async fn main() {
    let aws_credential = check_aws_credential().await;
    println!("{}", aws_credential);
}

async fn check_aws_credential() -> bool {
    let _ = dotenv();
    let aws_access_key = env::var("AWS_ACCESS_KEY_ID").unwrap();
    let aws_secret_key = env::var("AWS_SECRET_ACCESS_KEY").unwrap();
    let aws_bucket_name = env::var("AWS_BUCKET_NAME").unwrap();
    let aws_region = env::var("AWS_REGION").unwrap();

    if aws_access_key.is_empty() || aws_secret_key.is_empty() || aws_bucket_name.is_empty() || aws_region.is_empty() {
        return false;
    }
    return true
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s
Running `target/debug/app`
false

なるほど!

【Rust】サーバ側からTera/JavaScriptにデータを渡すとき

JSに渡すときに、let value = {{ data }} と書くと宜しくない。let value = “{{ data }}” とダブルクオテーションで囲って渡してあげる必要がある。

サーバ側

    let tera = tera::Tera::new("templates/*").unwrap();

    let data = 100;

    let mut context = tera::Context::new();
    context.insert("title", "Index page");
    context.insert("data", &data);

    let output = tera.render("index.html", &context);
    return axum::response::Html(output.unwrap());

html/JS側

//
<input type="range" class="form-range" id="customRange1" min="0" max="{{data}}" v-model="amount">
//
<script>
    let value = "{{ data / 2 }}";
</script>

↓ これは動くには動くが駄目。

<script>
    let value = {{ data / 2 }};
</script>

【Rust】Vectorの要素のスライスとVecDequeのdrain(n..)

Vectorの場合は、以下のように書いて、要素のスライスができる。

    let vec: Vec<String> = vec!["A".to_string(), "B".to_string(),"C".to_string(),"D".to_string(),"E".to_string(),"F".to_string()];
    let data = &vec[..3];
    println!("{:?}", data);

Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.84s
Running `target/debug/app`
[“A”, “B”, “C”]

これをVecDequeでやろうとするとエラーになる。

    let vecdeque: VecDeque<String> = VecDeque::from(["A".to_string(), "B".to_string(),"C".to_string(),"D".to_string(),"E".to_string(),"F".to_string()]);
    let data = &vecdeque[..3];
    println!("{:?}", data);

error[E0308]: mismatched types
–> src/main.rs:37:26
|
37 | let data = &vecdeque[..3];
| ^^^ expected `usize`, found `RangeTo<{integer}>`
|
= note: expected type `usize`
found struct `RangeTo<{integer}>`

let mut vecdeque: VecDeque<String> = VecDeque::from(["A".to_string(), "B".to_string(),"C".to_string(),"D".to_string(),"E".to_string(),"F".to_string()]);
    vecdeque.drain(3..);
    println!("{:?}", vecdeque);

[“A”, “B”, “C”]

【Rust】Axumでスレッドを並列化させて、async関数を実行させたい

axumでPostされたデータの処理で重い処理があるため、スレッドで並列化させて、先にURLを表示させたい時。

### ダメな例(asyncでない関数を呼び出す)
これでも動くのは動く(先にレンダリングされる)が、spawnの中の関数は、asyncは使えない。

async fn handle_index() -> axum::response::Html<String> {

    let handle = std::thread::spawn(move||{
        let _ = delay();
    });
    println!("main thread");
    
    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");

    let output = tera.render("index.html", &context);
    return axum::response::Html(output.unwrap())
   handle.join().unwrap();
}

fn delay() {
    thread::sleep(Duration::from_secs(2));
    println!("sub thread");
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.65s
Running `target/debug/axum`
main thread
sub thread

### asyncで並列化
tokio::spawn で、別スレッドでasync関数を実行することができる。handle.joinの記述も不要。

async fn handle_index() -> axum::response::Html<String> {

    let handle = tokio::spawn(async move {
        let _ = delay().await;
    });
    println!("main thread");
    
    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");

    let output = tera.render("index.html", &context);
    return axum::response::Html(output.unwrap());
}

async fn delay() {
    thread::sleep(Duration::from_secs(2));
    println!("sub thread");
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.65s
Running `target/debug/axum`
main thread
sub thread

うおおおおおおおおお、中々良い^^

【Rust】ECDSA署名のverifyのエラーハンドリング

署名のverifyはtrue or falseで返したいが、、、

pub async fn verify_signature(signedtransaction: &kernel::SignedTransaction) -> bool {
    // 省略
    return verifying_key.verify(posted_serialized.as_bytes(), &signature).is_ok()
}

偽のpublic keyを送ってきた可能性もあるため、Result型で返却しないといけない。

pub async fn verify_signature(signedtransaction: &SignedTransaction) -> Result<bool, Box<dyn std::error::Error>>{
    // 省略
    Ok(verifying_key.verify(posted_serialized.as_bytes(), &signature).is_ok())
}

なるほど、こういうのは、UnitTestを書いてテストしないと気づかない…

【Rust】文字列が全て数字か調べる(unit test)

pub fn create_session_token() -> String {
    let mut random = ChaCha8Rng::seed_from_u64(OsRng.next_u64());
    let mut u128_pool = [0u8; 16];
    random.fill_bytes(&mut u128_pool);
    let session_token = u128::from_le_bytes(u128_pool);
    return session_token.to_string();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_create_session_token() {
        let token = create_session_token();
        let token_filter = token.clone().chars().filter(|&c| matches!(c, '0' ..= '9')).collect::<String>();
        assert_eq!(token_filter.len(), token.len());
    }
}

warning: `app` (bin “app” test) generated 7 warnings (run `cargo fix –bin “app” –tests` to apply 7 suggestions)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.10s
Running unittests src/main.rs (target/debug/deps/app-22ae7a27958c3b49)

running 1 test
test tests::test_create_session_token … ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

うーん、渋い

【Rust】async関数のunitテスト

asyncの場合は、#[test]ではなく、#[tokio::test] とする。

    #[tokio::test]
    async fn test_get_node_list() {
        let ips:Vec<String> = get_node_list().await.unwrap();
        assert!(ips[0].parse::<Ipv4Addr>().is_ok());
    }

test tests::test_get_node_list … ok

なんか時間がかかるような気がするが、気のせいだろうか…

【Rust】iter(), into_iter()を使って、JR東日本のどこかにビューーン!をRustで実装する!

ここでは東北、秋田、山形新幹線の駅からそれぞれ一つづつランダムに駅を抽出する組み合わせを出力するという簡易的な実装例とする。
実際のどこかにビューーン!は、更に上越、北陸新幹線もあり、候補47駅、5つの新幹線の中から4つの駅を出力しているので、若干ロジックが異なるが、考え方はおおよそ同じだ。※[東北、秋田、山形、上越、北陸]から、4つをランダムに抽出して、そこから更にランダムに抽出しているのかもしれない。

サーバ側では、各駅がどの新幹線駅かというデータを持っておき、iteratorからfilterでそれぞれの新幹線ごとにvectorに入れて、そこからランダムに抽出する。下のサンプルでは、最後に往復料金でソートしてprintln!している。

use rand::prelude::IndexedRandom;

#[derive(Debug, Clone)]
struct Destination {
    bullet_train: String,
    station: String,
    ticket_price: u32
}

fn main() {
    let data = [
        Destination{ bullet_train: "東北".to_string(), station: "那須塩原".to_string(), ticket_price: 12040},
        Destination{ bullet_train: "東北".to_string(), station: "新白河".to_string(), ticket_price: 13580},
        Destination{ bullet_train: "東北".to_string(), station: "郡山".to_string(), ticket_price: 16680},
        Destination{ bullet_train: "東北".to_string(), station: "福島".to_string(), ticket_price: 18220},
        Destination{ bullet_train: "東北".to_string(), station: "白石蔵王".to_string(), ticket_price: 21080},
        Destination{ bullet_train: "東北".to_string(), station: "仙台".to_string(), ticket_price: 22180},
        Destination{ bullet_train: "東北".to_string(), station: "古川".to_string(), ticket_price: 23280},
        Destination{ bullet_train: "秋田".to_string(), station: "雫石".to_string(), ticket_price: 32200},
        Destination{ bullet_train: "秋田".to_string(), station: "田沢湖".to_string(), ticket_price: 32640},
        Destination{ bullet_train: "秋田".to_string(), station: "角館".to_string(), ticket_price: 34040},
        Destination{ bullet_train: "秋田".to_string(), station: "大曲".to_string(), ticket_price: 34700},
        Destination{ bullet_train: "秋田".to_string(), station: "秋田".to_string(), ticket_price: 36040},
        Destination{ bullet_train: "山形".to_string(), station: "米沢".to_string(), ticket_price: 21060},
        Destination{ bullet_train: "山形".to_string(), station: "高畠".to_string(), ticket_price: 21500},
        Destination{ bullet_train: "山形".to_string(), station: "赤湯".to_string(), ticket_price: 22240},
        Destination{ bullet_train: "山形".to_string(), station: "かみのやま温泉".to_string(), ticket_price: 22900},
        Destination{ bullet_train: "山形".to_string(), station: "さくらんぼ東根".to_string(), ticket_price: 24900},
        Destination{ bullet_train: "山形".to_string(), station: "村山".to_string(), ticket_price: 24900},
        Destination{ bullet_train: "山形".to_string(), station: "大石田".to_string(), ticket_price: 24900},
        Destination{ bullet_train: "山形".to_string(), station: "新庄".to_string(), ticket_price: 26000},
    ];

    let mut JR_Voom = Vec::new();

    let tohoku: Vec<Destination> = data.clone().into_iter()
        .filter(|d|d.bullet_train == "東北")
        .collect();
    JR_Voom.push(tohoku.choose(&mut rand::thread_rng()).unwrap());

    let akita: Vec<Destination> = data.clone().into_iter()
        .filter(|d|d.bullet_train == "秋田")
        .collect();
    JR_Voom.push(akita.choose(&mut rand::thread_rng()).unwrap());

    let yamagata: Vec<Destination> = data.clone().into_iter()
        .filter(|d|d.bullet_train == "山形")
        .collect();
    JR_Voom.push(yamagata.choose(&mut rand::thread_rng()).unwrap());
    JR_Voom.sort_by(|a, b| a.ticket_price.cmp(&b.ticket_price));

    for (i, destination) in JR_Voom.iter().enumerate() {
        println!("{:}: {}駅 (往復{}円)", i + 1, destination.station, destination.ticket_price);
    }
    
}

何度か実行すると以下のようになる。うん、見たことある光景である。
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.55s
Running `target/debug/app`
1: 郡山駅 (往復16680円)
2: 新庄駅 (往復26000円)
3: 秋田駅 (往復36040円)

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/app`
1: 米沢駅 (往復21060円)
2: 古川駅 (往復23280円)
3: 雫石駅 (往復32200円)

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
Running `target/debug/app`
1: 福島駅 (往復18220円)
2: 米沢駅 (往復21060円)
3: 田沢湖駅 (往復32640円)

プログラムでランダムに抽出しているだけだが、これに一喜一憂しているのだからしょうがない。多分、●●駅の方が△△駅よりも出やすくするといった駅によっての重み付とかはないと思うが、実際のところは分かりません。。

【Rust】Actixでsqlxを利用する

use actix_web::{get, post, web, App, HttpServer, HttpResponse};
use askama::Template;
use askama_actix::TemplateToResponse;
use sqlx::{Row, SqlitePool};

#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate {
    name: String,
}

#[get("/hello/{name}")]
async fn hello(name: web::Path<String>) -> HttpResponse {
    let hello = HelloTemplate {
        name: name.into_inner(),
    };
    hello.to_response()
}

#[derive(Template)]
#[template(path = "todo.html")]
struct TodoTemplate {
    tasks: Vec<String>,
}

#[derive(serde::Deserialize)]
struct Task {
    id: String,
}

#[post("/update")]
async fn update(pool: web::Data<SqlitePool>, form: web::Form<Task>) -> HttpResponse {
    let task = form.into_inner();
    sqlx::query("DELETE FROM tasks WHERE task = ?")
        .bind(&task.id)
        .execute(pool.as_ref())
        .await
        .unwrap();
    HttpResponse::Ok().finish()
}

#[get("/")]
async fn todo(pool: web::Data<SqlitePool>) -> HttpResponse {
    let rows = sqlx::query("SELECT task FROM tasks;")
        .fetch_all(pool.as_ref())
        .await
        .unwrap();
    let tasks: Vec<String> = rows
        .iter()
        .map(|row| row.get::<String, _>("task"))
        .collect();
    let todo = TodoTemplate { tasks };
    todo.to_response()
}

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

    let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
    sqlx::query("CREATE TABLE tasks (task TEXT)")
        .execute(&pool)
        .await
        .unwrap();

    sqlx::query("INSERT INTO tasks(task) VALUES('task1')")
        .execute(&pool)
        .await
        .unwrap();
    sqlx::query("INSERT INTO tasks(task) VALUES('task2')")
        .execute(&pool)
        .await
        .unwrap();
    sqlx::query("INSERT INTO tasks(task) VALUES('task3')")
        .execute(&pool)
        .await
        .unwrap();

    HttpServer::new(move|| {
        App::new()
        .service(hello)
        .service(todo)
        .app_data(web::Data::new(pool.clone()))
        })
        .bind(("0.0.0.0", 8080))?
        .run()
        .await
}