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 }
【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】パッケージとモジュール
$ cargo new –lib my_package
src/lib.rs
pub fn hello(name: &str) { println!("Hello, {}", name); }
src/bin/bin.rs
use my_package::hello; fn main() { hello("bin_1"); }
$ cargo run –bin bin
【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
【Rust】ポーカー
このソース見た時、凄い感慨したんよね。絵柄と1~13の数字でdeckを作るところ。
pocker chaseの裏側ってどうやってんだろうずっと思ってたんだけど、これを見ると、カードの配り方や、最後の役完成時の判定をどうやってるかがわかる。素晴らしい!
use rand::seq::SliceRandom; #[derive(Debug, Clone, Copy, PartialEq)] enum Suit { Club, Diamond, Heart, Spade, } #[derive(Debug, Clone, Copy, PartialEq)] struct Card { suit: Suit, rank: i32, } fn main() { let mut deck: Vec<Card> = Vec::new(); let suits = [Suit::Club, Suit::Diamond, Suit::Heart, Suit::Spade]; for suit in suits { for rank in 1..=13 { deck.push(Card {suit, rank}); } } let mut rng = rand::thread_rng(); deck.shuffle(&mut rng); let mut hand: Vec<Card> = Vec::new(); for _ in 0..5 { hand.push(deck.pop().unwrap()); } hand.sort_by(|a, b| a.rank.cmp(&b.rank)); for (i, card) in hand.iter().enumerate() { println!("{:}: {:?} {:}", i +1, card.suit, card.rank); } println!("入れ替えたいカードの番号を入力してください(例: 1 2 3)"); let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); let numbers : Vec<usize> = input.split_whitespace() .map(|x| x.parse().unwrap()) .collect::<Vec<usize>>(); for number in numbers { hand[number - 1] = deck.pop().unwrap(); } hand.sort_by(|a, b| a.rank.cmp(&b.rank)); for (i, card) in hand.iter().enumerate() { println!("{:}: {:?} {:}", i +1, card.suit, card.rank); } let suit = hand.first().unwrap().suit; let flash = hand.iter().all(|c| c.suit == suit); let mut count = 0; for i in 0..hand.len() - 1 { for j in i + 1..hand.len() { if hand[i].rank == hand[j].rank { count += 1; } } } if flash { println!("flash!"); } else if count >= 3 { println!("スリーカード!"); } else if count == 2 { println!("2ペア"); } else if count == 1 { println!("1ペア"); } else { println!("役なし..."); } }
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.62s
Running `target/debug/app`
1: Spade 3
2: Spade 4
3: Club 5
4: Diamond 8
5: Spade 13
入れ替えたいカードの番号を入力してください(例: 1 2 3)
1 2 3 4
1: Diamond 10
2: Club 10
3: Diamond 12
4: Heart 12
5: Spade 13
2ペア
【Rust】dbg!によるデバッグ
fn main() { println!("1 + 1 = ??"); println!("??の値を入力してください。"); let mut ans_input = String::new(); std::io::stdin().read_line(&mut ans_input).unwrap(); dbg!(ans_input); }
Compiling app v0.1.0 (/home/vagrant/dev/rust/app)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/app`
1 + 1 = ??
??の値を入力してください。
2
[src/main.rs:6:5] ans_input = “2\n”