【Rust】Actixでaskama(template)の使い方

axumの方が優勢な気がしますが、actixでも問題なくルーティングとテンプレートを使った開発ができそうです。

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

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

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

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

#[get("/")]
async fn todo() -> HttpResponse {
    let tasks = vec!["task1".to_string(), "task2".to_string(), "task3".to_string()];
    let todo = TodoTemplate { tasks };
    todo.to_response()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(hello).service(todo))
        .bind(("0.0.0.0", 8080))?
        .run()
        .await
}
<html>
<head>
    <title>ToDo</title>
</head>
<body>
    <ul>
        {% for task in tasks %}
        <li>{{ task }}</li>
        {% endfor %}
    </ul>
</body>
</html>

【Rust】エラーハンドリングの基本

実行した時にエラーになりうる関数はResultを返す。
Ok(File), Err(Error)など

use std::fs::File;

fn main() {
    let result = File::open("file.txt");
    match result {
        Ok(file) => {
            println!("ファイルのオープンに成功しました!");
        }
        Err(error) => {
            println!("ファイルのオープンに失敗しました!");
        }
    }
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.70s
Running `target/debug/app`
ファイルのオープンに失敗しました!

関数化すると

use std::fs::File;

#[derive(Debug)]
struct MyError {
    message: String,
}

fn main() {
    let result = open_file();
    println!("{:?}", result);
}

fn open_file() -> Result<File, MyError> {
    let result = File::open("file.txt");
    match result {
        Ok(file) => {
            return Ok(file);
        }
        Err(error) => {
            let error = MyError{ message: "ファイルのオープンに失敗しました!".to_string()};
            return Err(error);
        }
    }
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/app`
Err(MyError { message: “ファイルのオープンに失敗しました!” })

### エラーが発生した時、発生したエラーをそのまま返す

fn main() {
    let result = open_file();
    println!("{:?}", result);
}

fn open_file() -> Result<File, std::io::Error> {
    let result = File::open("file.txt");
    match result {
        Ok(file) => {
            return Ok(file);
        }
        Err(error) => {
            return Err(error);
        }
    }
}

“?”とすれば、その時点でエラーを返す。下記の方が簡略して書ける。

fn main() {
    let result = open_file();
    println!("{:?}", result);
}

fn open_file() -> Result<(), std::io::Error> {
    let result = File::open("file.txt");
    let file = result?;
    Ok(())
}

File::open(SCHEDULE_FILE).unwrap()のunwrap()はエラーだたった時、エラー内容を表示してそのまま終了してしまう。
Result<(), std::io::Error> と書いた場合は、エラーだったときに、終了せずにエラーを返す。

【Rust】Unit Testの実用的な書き方

use std::{fs::File, io::{BufReader, BufWriter}};
use serde::{Serialize, Deserialize};
use clap::{Parser, Subcommand};
use chrono::NaiveDateTime;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Schedule {
    id: u64,
    subject: String,
    start: NaiveDateTime,
    end: NaiveDateTime,
}
impl Schedule {
    fn intersects(&self, other: &Schedule) -> bool {
        self.start < other.end && other.start < self.end
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Calendar {
    schedules: Vec<Schedule>,
}

const SCHEDULE_FILE : &str = "schedule.json";

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    List,
    Add {
        subject: String,
        start: NaiveDateTime,
        end: NaiveDateTime,
    }
}

fn main() {
    let options = Cli::parse();
    match options.command {
        Commands::List => show_list(),
        Commands::Add { subject, start, end }
            => add_schedule(subject, start, end),
    }
}

fn show_list() {
    let calendar : Calendar = {
        let file = File::open(SCHEDULE_FILE).unwrap();
        let reader = BufReader::new(file);
        serde_json::from_reader(reader).unwrap()
    };

    println!("ID\tSTART\tEND\tSUBJECT");
    for schedule in calendar.schedules {
        println!(
            "{}\t{}\t{}\t{}",
            schedule.id, schedule.start, schedule.end, schedule.subject
        );
    }
}

fn add_schedule(
    subject: String,
    start: NaiveDateTime,
    end: NaiveDateTime,
) {
    let mut calendar : Calendar = {
        let file = File::open(SCHEDULE_FILE).unwrap();
        let reader = BufReader::new(file);
        serde_json::from_reader(reader).unwrap()
    };

    let id = calendar.schedules.len() as u64;
    let new_schedule = Schedule {
        id,
        subject,
        start,
        end,
    };

    for schedule in &calendar.schedules {
        if schedule.intersects(&new_schedule) {
            println!("エラー: 予定が重複しています");
            return;
        }
    }
    calendar.schedules.push(new_schedule);

    {
        let file = File::create(SCHEDULE_FILE).unwrap();
        let writer = BufWriter::new(file);
        serde_json::to_writer(writer, &calendar).unwrap();
    }
    println!("予定を追加しました。");
}

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

    fn naive_date_time(
        year: i32,
        month: u32,
        day: u32,
        hour: u32,
        minute: u32,
        second: u32,
    ) -> NaiveDateTime {
        chrono::NaiveDate::from_ymd_opt(year, month, day)
            .unwrap()
            .and_hms_opt(hour, minute, second)
            .unwrap()
    }

    fn test_schedule_intersects(
        h0: u32,
        m0: u32,
        h1: u32,
        m1: u32,
        should_intersects: bool
    ) {
        let schedule = Schedule {
            id: 0,
            subject: "既存予定1".to_string(),
            start: naive_date_time(2024, 1, 1, h0, m0, 0),
            end: naive_date_time(2024, 1, 1, h1, m1, 0),
        };
        let new_schedule = Schedule {
            id: 999,
            subject: "新規予定".to_string(),
            start: naive_date_time(2024, 1, 1, 19, 0, 0),
            end: naive_date_time(2024, 1, 1, 20, 0, 0),
        };
        assert_eq!(should_intersects, schedule.intersects(&new_schedule));
    }
    

    #[test]
    fn test_schedule_intersects_1(){
        test_schedule_intersects(18, 15, 19, 15, true)
    }

    #[test]
    fn test_schedule_intersects_2(){
        test_schedule_intersects(19, 45, 20, 45, true)
    }
}

$ cargo test
Compiling calendar v0.1.0 (/home/vagrant/dev/work/rusty/calendar)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.40s
Running unittests src/main.rs (target/debug/deps/calendar-867e0c10670d3913)

running 2 tests
test tests::test_schedule_intersects_2 … ok
test tests::test_schedule_intersects_1 … ok

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

なるほど、これはかなり参考になる。

【Rust】CLIプログラムの書き方

これはかなり面白い。というか、参考にさせていただきます!

use std::{collections::HashMap, fs::OpenOptions};
use chrono::NaiveDate;
use clap::{Args, Parser, Subcommand};
use csv::{Writer, Reader, WriterBuilder};
use serde::{Deserialize, Serialize};

#[derive(Parser)]
#[clap(version = "1.0")]
struct App {
    #[clap(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// 新しい口座を作る
    New(NewArgs),
    /// 口座に入金する
    Deposit(DepositArgs),
    /// 口座から出金する
    Withdraw(WithdrawArgs),
    /// CSVからインポートする
    Import(ImportArgs),
    /// レポートを出力する
    Report(ReportArgs),
}

#[derive(Args)]
struct NewArgs {
    account_name: String,
}
impl NewArgs {
    fn run(&self) {
        let file_name = format!("{}.csv", self.account_name);
        let mut writer = Writer::from_path(file_name).unwrap();
        writer
            .write_record(["日付", "用途", "金額"])
            .unwrap();
        writer.flush().unwrap();
    }
}
#[derive(Args)]
struct DepositArgs {
    account_name: String,
    date: NaiveDate,
    usage: String,
    amount: u32,
}
impl DepositArgs {
    fn run(&self) {
        let open_option = OpenOptions::new()
            .write(true)
            .append(true)
            .open(format!("{}.csv", self.account_name))
            .unwrap();
        let mut writer = Writer::from_writer(open_option);
        writer
            .write_record([
                self.date.format("%Y-%m-%d").to_string(),
                self.usage.to_string(), 
                self.amount.to_string()])
            .unwrap();
        writer.flush().unwrap();
    }
}

#[derive(Args)]
struct WithdrawArgs {
    account_name: String,
    date: NaiveDate,
    usage: String,
    amount: u32,
}
impl WithdrawArgs {
    fn run(&self) {
        let open_option = OpenOptions::new()
            .write(true)
            .append(true)
            .open(format!("{}.csv", self.account_name))
            .unwrap();
        let mut writer = Writer::from_writer(open_option);
        writer
            .write_record([
                self.date.format("%Y-%m-%d").to_string(),
                self.usage.to_string(), 
                format!("-{}", self.amount),
            ])
            .unwrap();
        writer.flush().unwrap();
    }
}
#[derive(Args)]
struct ImportArgs {
    src_file_name: String,
    dst_account_name: String,
}
impl ImportArgs {
    fn run(&self) {
        let open_option = OpenOptions::new()
            .write(true)
            .append(true)
            .open(format!("{}.csv", self.dst_account_name))
            .unwrap();
        let mut writer = WriterBuilder::new()
            .has_headers(false)
            .from_writer(open_option);
        let mut reader = Reader::from_path(&self.src_file_name).unwrap();
        for result in reader.deserialize() {
            let record: Record = result.unwrap();
            writer.serialize(record).unwrap();
        }
    }
}
#[derive(Serialize, Deserialize)]
struct Record {
    日付: NaiveDate,
    用途: String,
    金額: i32,
}
#[derive(Args)]
struct ReportArgs {
    files: Vec<String>,
}
impl ReportArgs {
    fn run(&self) {
        let mut map = HashMap::new();
        for file in &self.files {
            let mut reader = Reader::from_path(file).unwrap();
            for result in reader.records() {
                let record = result.unwrap();
                let amount : i32 = record[2].parse().unwrap();
                let date: NaiveDate = record[0].parse().unwrap();
                let sum = map.entry(date.format("%Y-%m").to_string())
                    .or_insert(0);
                *sum += amount;
            }
        }
        println!("{:?}", map);
    }
}
fn main() {
    let args = App::parse();
    match args.command {
        Command::New(args) => args.run(),
        Command::Deposit(args) => args.run(),
        Command::Withdraw(args) => args.run(),
        Command::Import(args) => args.run(),
        Command::Report(args) => args.run(),
    }
}

【Rust】コマンド入力とargs

fn main() {

    let command_name = std::env::args().nth(0).unwrap_or("CLI".to_string());
    let name = std::env::args().nth(1).unwrap_or("WORLD".to_string());
    println!("Hello {} via {}!", name, command_name);
}

Running `target/debug/kakeibo arg1 arg2`
Hello arg1 via target/debug/kakeibo!

use clap::Parser;

#[derive(Parser)]
#[clap(version = "1.0")]
struct Args {
    arg1: String,
    arg2: String
}

fn main() {
    let _args = Args::parse();
}
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[clap(version = "1.0")]
struct App {
    #[clap(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// 新しい口座を作る
    New,
    /// 口座に入金する
    Deposit,
    /// 口座から出金する
    Withdraw,
    /// CSVからインポートする
    Import,
    /// レポートを出力する
    Report,
}

fn main() {
    let _args = App::parse();
}

$ ./target/debug/kakeibo -h
Usage: kakeibo

Commands:
new 新しい口座を作る
deposit 口座に入金する
withdraw 口座から出金する
import CSVからインポートする
report レポートを出力する
help Print this message or the help of the given subcommand(s)

Options:
-h, –help Print help
-V, –version Print version

【Rust】字句解析(Lexical analysis)の初級

use std::io::stdin;

fn main() {

    let mut memory = Memory {
        slots: vec![0.0; 10],
    }
    let mut prev_result: f64 = 0.0;
    for line in stdin().lines() {
        let line = line.unwrap();
        if line.is_empty() {
            break;
        }

        let tokens: Vec<&str> = line.split(char::is_whitespace).collect();

        let is_memory = tokens[0].starts_with("mem");
        if is_memory && tokens[0].ends_with('+') {
            add_and_print_memory(&mut memory, tokens[0], prev_result);
            continue;
        } else if is_memory && tokens[0].ends_with('-') {
            add_and_print_memory(&mut memory, tokens[0], -prev_result);
            continue;
        }

        let left = eval_token(tokens[0], &memory);
        let right = eval_token(tokens[2], &memory);
        let result = eval_expression(left, tokens[1], right);
        print_output(result);

        prev_result = result;
    }
}

struct Memory {
    slots: Vec<f64>,
}

fn add_and_print_memory(memory: &mut Memory, token: &str, prev_result: f64) {
    let slot_index: usize = token[3..token.len() - 1].parse().unwrap();
    memory.slots[slot_index] += prev_result;
    print_output(memory.slots[slot_index]);
}
fn eval_token(token: &str, memory: &Memory) -> f64 {
    if token.starts_with("mem") {
        let slot_index: usize = token[3..].parse().unwrap();
        memory.slots[slot_index]
    } else {
        token.parse().unwrap()
    }
}
fn eval_expression(left: f64, operator: &str, right: f64) -> f64 {
    match operator {
        "+" => left + right,
        "-" => left - right,
        "*" => left * right,
        "/" => left / right,
        _ => {
            unreachable!()
        }
    }
}
fn print_output(value: f64) {
    println!(" => {}", value);
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.49s
Running `target/debug/app`
1 + 2
=> 3
mem1+
=> 3
3 + 4
=> 7
mem2-
=> -7
mem1 * mem2
=> -21