rustのtraitとは

traitとは構造体に共通のメソッドを実装したい時に利用する

trait Animal {
	fn get_name(&self) -> &String;

	fn get_age(&self) -> usize {
		return 0;
	}
}

struct Cat {
	name: String,
	age: usize,
}

impl Animal for Cat {
	fn get_name(&self) -> &String {
		return &self.name;
	}
}

const cat = Cat {
	name:String::from("Tama"),
	age: 20,
};
println!("{}", cat.get_name());

他の言語のインターフェイス

axum sqlx

use sqlx::postgres::PgPoolOptions;

#[async_std::main]
async fn main() -> Result<(), sqlx::Error> {

	let pool = PgPoolOptions::new()
		.max_connections(5)
		.connect("postgres://postgres:password@localhost/test").await?;

	let row: (i64,) = sqlx::query_as("SELECT $1")
		.bind(150_i64)
		.fetch_one(&pool).await?;

	assert_eq!(row.0, 150);
	Ok(())
}


pub async fn startup(modules: Arc<Modules>){
	let hc_router = Router::new()
		.route("/", get(hc))
		.route("/postgres", get(hc_postgres));

	let todo_router = Router::new()
		.route("/", get(find_todo).post(create_todo))
		.route(
			"/:id",
			get(get_todo)
				.patch(update_todo)
				.put(upsert_todo)
				.delete(delte_todo),
		);

	let app = Router::new()
		.nest("/v1/hc", hc_router)
		.nest("/v1/todos", todo_router)
		.layer(Extension(modules));

	axum::Server::bind(&&addr)
		.server(app.into_mmake_service())
		.await
		.unwrap_or_else(|_| panic!("server cannot launch."));
}

use std::sync::Arc;
use todo_adapter::modules::{RepositoriesModule, RepositoriesModuleExt};
use todo_adapber::persistence::postgres::Db;
use todo_adapter::repository::health_check::HealthCheckRepository;
use todo_app::usecase::health_check::HealthCechkUseCase;
use todo_app::usecase::todo::TodoUseCase;

pub struct Modules {
	health_check_use_case:: HealthCheckUseCase,
	todo_use_case: TodoUseCase<RepositoriesModule>,
}

pub trait ModulesExt {
	type RepositoriesModule: RepositoriesModuleExt;

	fn health_check_use_case(&self) -> &HealthCheckUseCase;
	fn todo_use_case(&self) -> &TodoUseCase<Self::RepositoriesModule>;
}

immpl ModulesExt for Modules {
	type RepositoriesModule = RepositoriesModule;

	fn health_check_use_case(&self) -> &HealthCheckUseCase {
		&self.health_check_use_case
	}

	fn todo_use_case(&self) -> &TodoUseCase<Self::RepositoriesModule> {
        &self.todo_use_case
    }
}

impl Modules {
	pub async fn new() -> Self {
		pub async fn new() -> Self {
			let db = Db::new().await;

			let repositories_module = Arc::new(RepositoriesModule::new(db.clone()));

			let health_check_use_case = HealthCheckUseCase::new(HealthCheckRepository::new(db));
			let todo_use_case = TodoUseCase::new(repositories_module.clone());

			Self {
				health_check_use_case,
				todo_use_case,
			}
		}
	}
}


#[derive(Debug, Deserialize)]
#[serde(rename_all) = "camelCase"]
pub struct TodoQuery {
	pub status: Option<String>,
}

immpl From<TodoQuery> for SearchTodoCondition {
	fn from(tq: TodoQuery) -> Self {
		Self {
			status_code: tq.status,
		}
	}
}

pub async fn update_todo(
	Path(id): Path<String>,
	ValidatedRequest(source): ValidatedRequest<JsonUpdateTodoContents>,
	Extension(modules): Extension<Arc<Modules>>,
) -> Result<impl IntoResponse, impl IntoResponse> {
	match source.validate(id) {
		Ok(todo) => {
			let resp = modules.todo_use_case().update_todo(todo).await;

			resp.map(|tv| {
				info!("updated todo {}", tv.id);
				let json: JsonTodo = tv.info();
				(StatusCode::OK, Json(json))
			})
			.map_err(|err|{
				error!("{:?}", err);

                if err.to_string() == *"`statusCode` is invalid." {
                    let json = JsonErrorResponse::new(
                        "invalid_request".to_string(),
                        vec![err.to_string()],
                    );
                    (StatusCode::BAD_REQUEST, Json(json))
                } else {
                    let json = JsonErrorResponse::new(
                        "server_error".to_string(),
                        vec!["INTERNAL SERVER ERROR".to_string()],
                    );
                    (StatusCode::INTERNAL_SERVER_ERROR, Json(json))
                }
			})
		}
		Err(errors) => {
			let json = JsonErrorResponse::new("invalid_request".to_string(), errors);
			Err((Statuscode::BAD_REQUEST, Json(json)))
		}
	}
}

use crate::context::errors::AppError;
use crate::context::validate::ValidatedRequest;
use axum::async_trait;
use axum::extract::{FromRequest, RequestParts};
use axum::{BoxError, Json};
use serde::de::DeserializeOwned;
use validator::Validate;

#[async_trait]
impl<T, B> FromRequest<B> for ValidatedRequest<T>
where
    T: DeserializeOwned + Validate,
    B: http_body::Body + Send,
    B::Data: Send,
    B::Error: Into<BoxError>,
{
    type Rejection = AppError;

    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
        let Json(value) = Json::<T>::from_request(req).await?;
        value.validate()?;
        Ok(ValidatedRequest(value))
    }
}

use crate::context::errors::AppError;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;
use tracing::log::error;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonErrorResponse {
    error_code: String,
    errors: Vec<String>,
}

impl JsonErrorResponse {
    pub(crate) fn new(error_code: String, errors: Vec<String>) -> Self {
        Self { error_code, errors }
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            AppError::Validation(validation_errors) => {
                error!("{:?}", validation_errors);

                let mut messages: Vec<String> = Vec::new();
                let errors = validation_errors.field_errors();
                for (_, v) in errors.into_iter() {
                    for validation_error in v {
                        if let Some(msg) = validation_error.clone().message {
                            messages.push(msg.to_string());
                        }
                    }
                }

                (
                    StatusCode::BAD_REQUEST,
                    Json(JsonErrorResponse::new(
                        "invalid_request".to_string(),
                        messages,
                    )),
                )
            }
            AppError::JsonRejection(rejection) => {
                error!("{:?}", rejection);

                let messages = vec![rejection.to_string()];
                (
                    StatusCode::BAD_REQUEST,
                    Json(JsonErrorResponse::new(
                        "invalid_request".to_string(),
                        messages,
                    )),
                )
            }
        }
        .into_response()
    }
}

#[derive(Deserialize, Debug, Validate)]
#[serde(rename_all = "camelCase")]
pub struct JsonCreateTodo {
    #[validate(
        length(min = 1, message = "`title` is empty."),
        required(message = "`title` is null.")
    )]
    pub title: Option<String>,
    #[validate(required(message = "`description` is null."))]
    pub description: Option<String>,
}

impl From<JsonCreateTodo> for CreateTodo {
    fn from(jc: JsonCreateTodo) -> Self {
        CreateTodo {
            title: jc.title.unwrap(),
            description: jc.description.unwrap(),
        }
    }
}

use sqlx::postgres::PgPoolOptions;
use sqlx::{Pool, Postgres};
use std::env;
use std::sync::Arc;

#[derive(Clone)]
pub struct Db(pub(crate) Arc<Pool<Postgres>>);

impl Db {
	pub async fn new() -> Db {
		let pool = PgPoolOptions::new()
			.max_connections(8)
			.connect(
				&env::var("DATABASE_URL").unwrap_or_else(|_| panic!("DATABASE_URL must be selt!")),
			)
			.await
			.unwrap_or_else(|_| {
				panic!("Cannot connect to the database. Please check your configuration.")
			});

		Db(Arc::new(pool))	
	}
}

async fn get(&self, id: &Id<Todo>) -> anyhow::Result<Option<Todo>> {
	let pool = self.db.0.clone();
	let sql = r#"
		select 
			t.id as id,
			t.title as title,
			t.description as description,
			ts.id as status_id,
			ts.code as status_code,
			ts.name as status_name,
			t.created_at as created_at,
			t.updated_at as updated_at
		from
			todos as t
			inner join
				todo statuses as ts
				on ts.id = t.status_id
		where
			t.id = $1
	"#;
	let stored_todo = query_as::<_, StoredTodo>(sql)
		.bind(id.value.to_string())
		.fetch_one(&*pool)
		.await
		.ok();

	match stored_todo {
		Some(st) => Ok(Some(st.try_into()?)),
		None => Ok(None),
	}

	"
}

Rust axumでcrudを実装する

$ cargo new axum_crud_api

Cargo.toml

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

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

[dependencies]
axum = "0.5.9"
tokio = { version="1.0", features = ["full"] }
serde = "1.0.137"
tracing = "0.1"
tracing-subscriber = { version="0.3", features = ["env-filter"] }

sqlx = { version = "0.5", features = ["runtime-tokio-tls", "json", "postgres"] }
anyhow = "1.0.58"
serde_json = "1.0.57"
tower-http = { version="0.3.4", features = ["trace"] }

main.rs

use axum::{
	routing::{get},
	Router,
};

use std::net::SocketAddr;

#[tokio::main]
async fn main() {
	let app = Router::new()
		.route("/", get(root));

	let addr = SocketAddr::from(([192,168,56,10], 8000));
	println!("listening on {}", addr);
	axum::Server::bind(&addr)
		.serve(app.into_make_service())
		.await
		.unwrap();
}

async fn root() -> &'static str {
	"Hello, World"
}

task.rs

use serde::{Deserialize, Serialize};

pub struct Task {
	pub id: i32,
	pub task: String,
}

pub struct NewTask {
	pub task: String,
}

DATABASE_URL = postgresql://user:password@locahost:host/database

$ cargo install sqlx-cli
$ cargo install sqlx-cli –no-default-features –features native-tls,postgres
$ cargo install sqlx-cli –features openssl-vendored
$ cargo install sqlx-cli –no-default-features –features rustls

sqlx database create
sqlx migrate add task

CREATE TABLE task {
id SERIAL PRIMARY KEY,
task varch(255) NOT NULL
}

sqlx migrate run

main.rs

use axum::{
	extract::{Extension},
	routing::{get, post},
	Router,
};

use sqlx::postgres::PgPoolOptions;
use std::net::SocketAddr;
use std::fs;
use anyhow::Context

#[tokio::main]
async fn main() -> anyhow::Result<()> {

	let env = fs::read_to_string(".env").unwrap();
	let (key, database_url) = env.split_once('=').unwrap();

	assert_eq!(key, "DATABASE_URL");

	tracing_subscriber::fmt::init();

	let pool = PgPoolOptions::new()
	.max_connections(50)
	.connect(&dtabase_url)
	.await
	.context("could not connect to database_url")?;

	let app = Router::new()
		.route("/hello", get(root));

	let addr = SocketAddr::from(([192,168,56,10], 8000));
	println!("listening on {}", addr);
	axum::Server::bind(&addr)
		.serve(app.into_make_service())
		.await?;

		Ok(())
}

controller/task.rs

use axum::response::IntoResponse;
use axum::http::StatusCode;

use axum::{Extension, Json}
use sqlx::PgPool;

use crate::{
	models::task
};

pub async fn all_tasks(Extension(pool): Extension<PgPool>) -> impl IntoResponse {
	let sql = "SELECT * FROM task".to_string();

	let task = sqlx::query_as::<_, task::Task>(&sql).fetch_all(&pool).await.unwrap();

	(StatusCode::OK, Json(task))
}

error.rs

use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;

pub enum CustomError {
	BadRequest,
	TaskNotFound,
	InternalServerError,
}

impl IntoResponse for CustomError {
	fn into_response(self) -> axum::response::Response {
		let (status, error_message) = match self {
			Self::InternalServerError => (
				StatusCode::INTERNAL_SERVER_ERROR,
				"Internal Server Error",
			),
			Self::BadRequest=> (StatusCode::BAD_REQUEST, "Bad Request"),
			Self::TaskNotFound => (StatusCode::NOT_FOUND, "Task Not Found"),
		};
		(status, Json(json!({"error": error_message}))).into_response()
	}
}

GET

// GET
pub async fn task(Path(id):Path<i32>,
	Extension(pool): Extension<PgPool>)-> Result<Json<task::Task>, CustomError>{

	let sql = "SELECT * FROM task where id=$1".to_string();

	let task: task::Task = sqlx::query_as(&sql)
		.bind(id)
		.fetch_one(&pool)
		.await
		.map_err(|_| {
			CustomError::TaskNotFound
		})?;

	Ok(Json(task))
}

// POST
pub async fn new_task(Json(task): Json<task::NewTask>,
	Extension(pool): Extension<PgPool>) -> Result<(StatusCode,
	Json<task::NewTask>), CustomError> {
		if task.task.is_empty() {
			return Err(CustomError::BadRequest)
		}
		let sql = "INSERT INTO task (task) values ($1)";

		let _ = sql::query(&sql)
			.bind(&task.task)
			.execute(&pool)
			.await
			.map_err(|_| {
				CustomError::InternalServerError
			})?;

		Ok((StatusCode::CREATED, Json(task)))
	}


// PUT
pub async fn update_task(Path(id): Path<i32>,
	Json(task): Json<task::UpdateTask>, Extension(pool): Extension<PgPool>)
	-> Result <(StatusCode, Json<task::UpdateTask>), CustomError> {

		let sql = "SELECT * FROM task where id=$1".to_string();

		let _find: task::Task = sqlx::query_as(&sql)
			.bind(id)
			.fetch_one(&pool)
			.await
			.map_err(|_| {
				CustomError::TaskNotFound
			})?;

		sqlx::query("UPDATE task SET task=$1 WHERE id=$2")
			.bind(&task.task)
			.bind(id)
			.execute(&pool)
			.await;

		Ok((StatusCode::OK, Json(task)))
	}

pub async fn delete_task(Path(id): Path<i32>, Extension(pool):Extension<PgPool>)
	-> Result<(StatusCode, Json<Value>), CustomError> {

		let _find: task::Task = sqlx::query_as("SELECT * FROM task where id=$1")
			.bind(id)
			.fetch_one(&pool)
			.await
			.map_err(|_| {
				CustomError::TaskNotFound
			})?;

		sqlx::query("DELETE FROM task WHERE id=$1")
			.bind(id)
			.execute(&pool)
			.await
			.map_err(|_|{
				CustomError::TaskNotFound
			})?;

			Ok((StatusCode::OK, Json(json!({"msg": "TaskDeleted"}))))
	}

main.rs

use axum::{
	extract::{Extension},
	routing::{get, post, put, delete},
	Router,
};

use sqlx::postgres::PgPoolOptions;
use std::net::SocketAddr;
use std::fs;
use anyhow::Context
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SuscriberInitExt};

#[tokio::main]
async fn main() -> anyhow::Result<()> {

	let env = fs::read_to_string(".env").unwrap();
	let (key, database_url) = env.split_once('=').unwrap();

	assert_eq!(key, "DATABASE_URL");

	tracing_subscriber::registery()
		.with(tracing_subscriber::EnvFilter::new(
			std::env::var("tower_http=trace")
				.unwrap_or_else(|_| "example_tracing_aka_logging=debug,tower_http=debug".into()),
				))
				.with(tracing_subscriber::fmt::layer())
				.init();

	let pool = PgPoolOptions::new()
	.max_connections(50)
	.connect(&dtabase_url)
	.await
	.context("could not connect to database_url")?;

	let app = Router::new()
		.route("/hello", get(root))
		.route("/tasks", get(controllers::task::all_tasks))
		.route("/task", post(controllers::task::new_task))
		.route("/task/:id", get(controllers::task::task))
		.route("/task/:id", put(controllers::task::update_task))
		.route("/task/:id", delete(controllers::task::delete_task))
		.layer(Extension(pool))
		.layer(TraceLayer::new_for_http());

	let addr = SocketAddr::from(([192,168,56,10], 8000));
	println!("listening on {}", addr);
	axum::Server::bind(&addr)
		.serve(app.into_make_service())
		.await?;

		Ok(())
}

async fn root() -> &'static str {
	"Hello, World"
}

Rust axum サンプル

Rust github: https://github.com/tokio-rs/axum

use axum:: {
	routing::{get, post},
	http::StatusCode,
	response::IntoResponse,
	Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
	tracing_subscriber::fmt::init();

	let app = Router::new()
		.route("/", get(root))
		.route("/users", post(create_user));

	let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
	axum::server(listener, app).await.unwrap();
}

async fn root() -> &'static str {
	"Hello, World"
}

async fn create_user(
	Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>){
	let user = User {
		id: 1337,
		username: payload.username,
	};

	(StatusCode::CREATED, Json(user))
}

#[derive(Deserialize)]
struct CreateUser {
	username: String,
}

#[derive(Serialize)]
struct User {
	id: u64,
	username: String,
}

axum project
https://github.com/tokio-rs/axum/blob/main/ECOSYSTEM.md

Cargo.toml

[dependencies]
axum="0.5"
tokio = { version="1", features-["full"] }

main.rs

use axum::{routing::get, Router};

#[tokio::main]
async fn main(){
	let app = Router::new().route("/",  get(|| async {"hello world!"}));

	axum::Server::bind(&&"0.0.0.0:3000".parse().unwrap())
		.serve(app.into_make_service())
		.await
		.unwrap();
}

workspace Cargo.toml

[workspace]
members = [
	"hello-world"
]

$ cargo new hello-world

[dependencies]
axum = "0.5"
tokio = { version = "1", features = ["full"] }
use axum::{routing::get, Router};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
	let app = Router::new().route("/", get(handler));

	let addr = SocketAddr::from(([192,168,56,10], 3000));

	axum::Server::bind(&addr)
		.serve(app.into_make_service())
		.await
		.unwrap();
}

async fn handler() -> &'static str {
	"hello world!"
}

/workspace/generate-random-number

[dependencies]
axum = "0.5"
rand = "0.8"
serde = { version = "1", features = ["derive"]}
tokio = { version = "1", features = ["full"]}
use axum::{extract::Query, response::Html, routing::get, Router};
use rand::{thread_rng, Rng};
use serde::Deserialize;
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
	let app = Router::new().route("/", get(handler));

	let addr = SocketAddr::from(([192,168,56,10], 3000));
	println!("listening on {}", addr);
	axum::Server::bind(&addr)
		.serve(app.into_make_service())
		.await
		.unwrap();
}

#[derive(Deserialize)]
struct RangeParameters {
	start: usize,
	end: usize,
}

async fn handler(Query(range): Query<RangeParameters>)-> Html<String> {

	let random_number = thread_rng().gen_range(range.start..range.end);

	Html(format!("<h1>Random Number: {}</h1>", random_number))
}

http://192.168.56.10:3000/?start=50&end=100

use axum::{response::Html, routing::get, Router};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
	let app = Router::new().route("/", get(handler));

	let addr = SocketAddr::from(([192,168,56,10], 3000));

	axum::Server::bind(&addr)
		.serve(app.into_make_service())
		.await
		.unwrap();
}

async fn handler() -> HTML<&'static str> {
	Html(include_str!("../index.html"))
}

Rust axumでhandlebarsの利用

Cargo.toml

[dependencies]
axum-template="0.14.0"
handlebars="4.3.6"

テンプレートファイルの用意
template/index.hbs

<body class="container">
	<h1 class="display-6 my-2">{{title}}</h1>

	<p class="my-2">{{ message }}</p>
</body>

main.rs

async fn handle_index() -> axum::response::Html<String> {
	let mut params = std::collections::HashMap::new();
	params.insert("title", "Index page");
	params.insert("message", "This is sample page message!");

	let mut handlebars = handlebars::Handlebars::new();
	handlebars
		.register_template_string("hello", include_str!("template/index.hbs"));

	let template = handlebars.render("hello", &params).unwrap();
	axum::response::Html(template)
}

Rust axum Teraを使いこなす

### {% %}の利用
テンプレート内での計算などができる

<body class="container">
	<h1 class="display-6 my-2">{{title}}</h1>
	<div class="border border-primary p-3 my-3">
		{% set v1 = value * 1.08 %}
		{% set v2 = value * 1.1 %}
		<p class="my-2">value: {{ value }}</p>
		<p class="my-2">value1: {{ v1 }}</p>
		<p class="my-2">value2: {{ v2 }}</p>
	</div>
</body>

main.rs

#[tokio::main]
async fn main() {
	let app = axum::Router::new()
		.route("/:value", axum::routing::get(handle_index));

    axum::Server::bind(&"192.168.56.10:8000".parse().unwrap())
    	.serve(app.into_make_service())
    	.await
    	.unwrap();
}

async fn handle_index(axum::extract::Path(value):
		axum::extract::Path<usize>
		)-> axum::response::Html<String> {
	let tera = tera::Tera::new("template/*").unwrap();

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

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

{% raw %}で生の文を出力する
L HTMLタグはそのまま表示される

	<div class="border border-primary p-3 my-3">
		{% raw %}
		{% set v1 = value * 1.08 %}
		{% set v2 = value * 1.1 %}
		<p class="my-2">value: {{ value }}</p>
		<p class="my-2">value1: {{ v1 }}</p>
		<p class="my-2">value2: {{ v2 }}</p>
		{% endraw %}
	</div>

テキストで繋げる

	<div class="border border-primary p-3 my-3">
		{% set v1 = value * 1.08 %}
		{% set v2 = value * 1.1 %}
		<p class="my-2">value: {{ value }}</p>
		<p class="my-2">value1: {{ value ~ " * 1.08 = " ~ v1  }}</p>
		<p class="my-2">value2: {{ value ~ " * 1.1 = " ~ v2 }}</p>
	</div>

### ifを使う

	{% if value % 2 == 0 %}
	<div class="border border-primary p-3 my-3">
		<p class="my-2">value: {{ value ~ "は偶数です。" }}</p>
	</div>
	{% else %}
	<div class="border border-primary p-3 my-3">
		<p class="my-2">value: {{ value ~ "は奇数です。" }}</p>
	</div>
	{% endif %}

更に分岐したい場合

	{% if value % 3 == 0 %}
	<div class="border border-primary p-3 my-3">
		<p class="my-2">value: {{ value ~ "はグーです。" }}</p>
	</div>
	{% elif value % 3 == 1 %}
	<div class="border border-primary p-3 my-3">
		<p class="my-2">value: {{ value ~ "はチョキです。" }}</p>
	</div> 
	{% else %}
	<div class="border border-primary p-3 my-3">
		<p class="my-2">value: {{ value ~ "はパーです。" }}</p>
	</div> 
	{% endif %}

### 繰り返し表示
main.rs

async fn handle_index()-> axum::response::Html<String> {
	let data = [
		Myform {name:"taro".to_string(), mail:"taro@yamada".to_string()},
		Myform {name:"hanako".to_string(), mail:"hanako@flower".to_string()},
		Myform {name:"sachiko".to_string(), mail:"sachiko@happy".to_string()},
		Myform {name:"jiro".to_string(), mail:"jiro@change".to_string()},
	];
	let tera = tera::Tera::new("template/*").unwrap();

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

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

view

	<ul class="list-group">
		{% for item in data %}
		<li class="list-group-item">
			{{item.name}} &lt;{{item.mail}}&gt;
		</li>
		{% endfor %}
	</ul>

### キー&バリューのコレクション
マップにHashMapインスタンスを代入。このHashMapをテンプレート側で処理する

async fn handle_index()-> axum::response::Html<String> {
	let mut map = std::collections::HashMap::new();
	map.insert("taro", ("taro@yamada", 39));
	map.insert("hanako", ("hanako@flower", 28));
	map.insert("sachiko", ("sachiko@happy", 17));

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

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

	let output = tera.render("index.html", &context);
	axum::response::Html(output.unwrap())
}
	<ul class="list-group">
		{% for key, value in data %}
			<li class="list-group-item">
				[{{loop.index}}] {{ key }}({{value.1}}) &lt;{{value.0}}&gt;
			</li>
		{% endfor %}
	</ul>

loop.indexは繰り返し情報がまとめられたオブジェクト

### フィルターの利用
フィルターは、あらかじめRust側で定義した関数を使ってテンプレートの表示を変換する機能
フィルター用の関数は形が決まっている
fn 関数(引数1: &value, 引数2: &HashMap)-> Result{…}

作成したフィルター関数はTeraのインスタンスに登録する

main.rs

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

	let mut tera = tera::Tera::new("template/*").unwrap();
	tera.register_filter("hello", hello_filter);

	let mut context = tera::Context::new();
	context.insert("title", "Index page");
	context.insert("name", "山田タロー");

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

fn hello_filter(value: &tera::Value,
	_: &std::collections::HashMap<String, tera::Value>)
	-> tera::Result<tera::Value> {
		let s = tera::try_get_value!("hello_filter", "value", String, value);
		Ok(tera::Value::String(format!("こんにちは、{}さん!", s)))
	}

テンプレート

	<div class="alert alert-primary">
		<p class="my-2">{{ name | hello}}</p>
	</div>

### フィルターでオブジェクトを扱う場合

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

	let mut tera = tera::Tera::new("template/*").unwrap();
	tera.register_filter("sample", sample_filter);

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

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

fn sample_filter(value: &tera::Value,
	_: &std::collections::HashMap<String, tera::Value>)
	-> tera::Result<tera::Value> {
		let data = [
			("taro", "taro@yamada", 39, "male"),
			("hanako", "hanako@flower", 28, "female"),
			("sachiko", "sachiko@happy", 17, "female"),
			("jiro", "jiro@change", 6, "male")
		];
		let n = tera::try_get_value!("sample_filter", "value", usize, value);
		let item = data[n];
		Ok(tera::Value::String(format!("{}({},{})<{}>", item.0, item.3, item.2, item.1)))
	}

### フィルターの属性を利用する

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

	let mut tera = tera::Tera::new("template/*").unwrap();
	tera.register_filter("calc", calc_filter);

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

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

fn calc_filter(_: &tera::Value,
	map: &std::collections::HashMap<String, tera::Value>)
	-> tera::Result<tera::Value> {
		let price = map.get("price").unwrap().as_f64().unwrap();
		let tax = map.get("tax").unwrap().as_f64().unwrap();
		let res = price * tax;
		Ok(tera::Value::String(format!("price:{} * tax:{} = {}", price, tax, res)))
}
	<div class="alert alert-primary">
		<p class="my-2">{{ false | calc(price=1234, tax=1.1) }}</p>
	</div>

Rust axumテンプレートエンジンTera

Rustのaxumで利用できるテンプレートエンジンは幾つかある

handlerbars: JavaScriptのHandler.jsにインスパイアされて開発されたRust用のテンプレートエンジン
minijinja: PythonのJinja2の文法を採用して作られている。
Tera: Djangoのテンプレートエンジンなどの影響を強く受けたもので、フィルターやマクロなど非常に豊富な機能を持っている

### Teraの準備
Cargo.tomlのdependenciesに以下を追加

axum-template="0.14.0"
tera="1.17.1"

### テンプレート作成
template/index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>{{title}}</title>
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.css" rel="stylesheet" crossorigin="anonymous">
</head>
<body class="container">
	<h1 class="display-6 my-2">{{title}}</h1>
	<p class="my-2">{{message}}</p>
</body>
</html>

### axumからTeraを利用
Teraのテンプレートパスを指定してTeraのインスタンス作成
テンプレートエンジンとの間でやりとりするデータの管理はContextを使用する
レンダリングの実行はtera.render, htmlインスタンスの返却はaxum::response::Html

#[tokio::main]
async fn main() {
	let app = axum::Router::new()
		.route("/", axum::routing::get(handle_index));

    axum::Server::bind(&"192.168.56.10:8000".parse().unwrap())
    	.serve(app.into_make_service())
    	.await
    	.unwrap();
}

async fn handle_index()-> axum::response::Html<String> {
	let tera = tera::Tera::new("template/*").unwrap();

	let mut context = tera::Context::new();
	context.insert("title", "Index page");
	context.insert("message", "これはサンプルです。");

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

### フォームの送信

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>{{title}}</title>
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.css" rel="stylesheet" crossorigin="anonymous">
</head>
<body class="container">
	<h1 class="display-6 my-2">{{title}}</h1>
	<div class="alert alert-primary">
		<p class="my-2">{{message}}</p>
	</div>
	<form method="post" action="/post">
		<div class="mb-3">
			<label for="name" class="form-label">
			Your name:</label>
			<input type="text" class="form-control" name="name" id="name">
		</div>
		<div class="mb-3">
			<label for="mail" class="form-label">
			Email address:</label>
			<input type="text" class="form-control" name="mail" id="mail">
		</div>
		<input type="submit" class="btn btn-primary" value="Submit">
	</form>
</body>
</html>

/postのルーティングを用意する

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Myform {
	name: String,
	mail: String,
}

#[tokio::main]
async fn main() {
	let app = axum::Router::new()
		.route("/", axum::routing::get(handle_index))
		.route("/post", axum::routing::post(handle_post));

    axum::Server::bind(&"192.168.56.10:8000".parse().unwrap())
    	.serve(app.into_make_service())
    	.await
    	.unwrap();
}

// 省略

async fn handle_post(axum::Form(myform): axum::Form<Myform>)
	-> axum::response::Html<String> {
		let msg = format!("I am {}<{}>.", myform.name, myform.mail);
		let tera = tera::Tera::new("template/*").unwrap();

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

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

Rust axumの基本

RustのWebアプリケーションフレームワーク
– active-web: 高速なパフォーマンス
– Rocket: シンプルで直感的なAPIを持つフレームワーク、わかりやすいコーディングスタイル
– axum: シンプルなフレームワークでモジュラ方式でカスタマイズが容易
– warp: 関数型プログラミングの考え方に基づいたWebフレームワーク

Cargo.toml

[dependencies]
axum="0.6.9"
hyper = { version = "0.14", features = ["full"]}
tokio = { version = "1.25", features = ["full"]}
tower = { version = "0.4", features = ["full"]}

hyper: httpライブラリ、tokio:非同期処理ライブラリ、tower:抽象化レイヤーライブラリ

### axumの基本コード
RouterとServerを用意する必要がある

#[tokio::main]
async fn main() {
	let app = axum::Router::new()
		.route("/", axum::routing::get(|| async{ "Hello, World!"}));

    axum::Server::bind(&"192.168.56.10:8000".parse().unwrap())
    	.serve(app.into_make_service())
    	.await
    	.unwrap();
}

axum::Router::newでインスタンスを作成し、routeでルーティングを追加
|| async {表示するテキスト}
await.unwrap()を忘れると処理が終了してしまう

### 複数のルーティング
getで実行する関数は別に定義しておき、それをgetの引数に指定するようにする

#[tokio::main]
async fn main() {
	let app = axum::Router::new()
		.route("/", axum::routing::get(handler_top))
		.route("/other", axum::routing::get(handler_other));

    axum::Server::bind(&"192.168.56.10:8000".parse().unwrap())
    	.serve(app.into_make_service())
    	.await
    	.unwrap();
}

async fn handler_top() -> String {
	"Hello, World!".to_string()
}

async fn handler_other() -> String {
	"This is otehr page...".to_string()
}

### パラメータを利用

#[tokio::main]
async fn main() {
	let app = axum::Router::new()
		.route("/", axum::routing::get(handler_top))
		.route("/usr/:user_id", axum::routing::get(handler_param));

    axum::Server::bind(&"192.168.56.10:8000".parse().unwrap())
    	.serve(app.into_make_service())
    	.await
    	.unwrap();
}

async fn handler_top() -> String {
	"Hello, World!".to_string()
}

async fn handler_param(axum::extract::Path(user_id):
	axum::extract::Path<String>) -> String {
	format!("User ID: {}", user_id)
}

### 複数の値を渡す

async fn main() {
	let app = axum::Router::new()
		.route("/", axum::routing::get(handler_top))
		.route("/usr/:id/:user", axum::routing::get(handler_param));

// 省略

async fn handler_param(axum::extract::Path((id, user)):
	axum::extract::Path<(usize,String)>) -> String {
	format!("User ID: {}. name:{}", id, user)
}

### クエリパラメータの利用

async fn main() {
	let app = axum::Router::new()
		.route("/", axum::routing::get(handler_top))
		.route("/qry", axum::routing::get(handler_param));
// 省略
async fn handler_param(axum::extract::Query(params):
	axum::extract::Query<std::collections::HashMap<String, String>>) -> String {
	format!("id:{}, name:{}.", params["id"], params["name"])
}

### JSONデータの出力
「serde」というパッケージを使用する。serdeは、JSONなど各種データをRustの構造体に変換するための機能を提供する
Cargo.tomlに以下を追記する

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

serdeのSerializeとDeserializeを指定することで、構造体のシリアライズ/デシリアライズが可能になる。
jsonを返すときは、async fn 関数() -> axum::Json{…}

### json形式で指定IDのデータを出力する

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Mydata {
	name:String,
	mail:String,
	age:u32,
}


#[tokio::main]
async fn main() {
	let app = axum::Router::new()
		.route("/", axum::routing::get(handler_top))
		.route("/json/:id", axum::routing::get(handler_json));

    axum::Server::bind(&"192.168.56.10:8000".parse().unwrap())
    	.serve(app.into_make_service())
    	.await
    	.unwrap();
}

async fn handler_top() -> String {
	"Hello, World!".to_string()
}

async fn handler_json(axum::extract::Path(id):
	axum::extract::Path<usize>) -> axum::Json<serde_json::Value>{
	let data:[Mydata;3] = [
		Mydata {name:String::from("Taro"),
		mail:String::from("taro@yamada"), age:39},
		Mydata {name:String::from("Hanako"),
		mail:String::from("hanako@flower"), age:28},
		Mydata {name:String::from("Sachiko"),
		mail:String::from("sachiko@happy"), age:17},
	];
	let item = &data[id];
	let data = serde_json::json!(item);
	axum::Json(data)
}

Rust eguiでグラフィックの利用

eguiのPainterという構造体を利用する

fn main() {
    let native_options = eframe::NativeOptions::default();
    native_options.default_theme = eframe::Theme::Light;
    native_options.initial_window_size = Some(egui::Vec2 {x:400.0, y:300.0});

    let _ = eframe::run_native("My egui App", native_options,
    	Box::new(|cc| Box::new(MyEguiApp::new(cc))));
}


#[derive(Default)]
struct MyEguiApp {
	pub value:bool,
}

impl MyEguiApp {
	fn new(_cc: &eframe::CreationContext<'_>)-> Self {
		Self::default()
	}
}

impl eframe::App for MyEguiApp {
	fn update(&mut self, _ctx: &egui::Context, _frame: &mut eframe::Frame){
		egui::CentralPanel::default().show(ctx, |ui|{
			ui.heading("Hello World!");
			plot(ui);
		});
	}
}

fn plot(ui: &mut egui::Ui){
	
}
fn plot(ui: &mut egui::Ui){
	ui.painter().rect_filled(
		egui::Rect::from_min_max(
			egui::Pos2::new(50.0, 50.0),
			egui::Pos2::new(150.0, 150.0)
		),
		egui::Rounding::same(20.0),
		egui::Color32::RED
	);
	ui.painter().rect_stroke(
		egui::Rect::from_min_max(
			egui::Pos2::new(100.0, 100.0),
			egui::Pos2::new(200.0, 200.0)
		),
		egui::Rounding::none(),
		egui::Stroke::new(10.0, egui::Color32::GREEN)
	);
}

円を表示する

fn plot(ui: &mut egui::Ui){
	let pos_1 = egui::Pos2::new(100.0, 100.0);
	ui.painter().circle_filled(pos_1, 50.0, egui::Color32::RED);
	let pos_2 = egui::Pos2::new(150.0, 150.0);
	let stroke_2 = egui::Stroke::from((10.0, egui::Color32::GREEN));
	ui.painter().circle_stroke(pos_2, 50.0, stroke_2);
}

直線を描く

fn plot(ui: &mut egui::Ui){
	let pos_1 = egui::Pos2::new(50.0, 50.0);
	let pos_2 = egui::Pos2::new(200.0, 200.0);
	let stroke_1 = egui::Stroke::new(5.0, egui::Color32::RED);
	let stroke_2 = egui::Stroke::new(5.0, egui::Color32::GREEN);
	ui.painter().vline(50.0, std::ops::RangeInclusive::new(50.0, 200.0), stroke_1);
	ui.painter().hline(std::ops::RangeInclusive::new(50.0, 200.0), 50.0, stroke_1);
	ui.painter().line_segment([pos_1, pos_2], stroke_2);
}

テキストを表示

fn plot(ui: &mut egui::Ui){
	ui.painter().text(
		egui::Pos2 {x:50.0, y:50.0},
		egui::Align2::LEFT_CENTER,
		"Hello!",
		egui::FontId::proportional(24.0),
		egui::Color32::RED
	);
	ui.painter().text(
		egui::Pos2 {x:50.0, y:100.0},
		egui::Align2::LEFT_CENTER,
		"sample message.",
		egui::FontId::proportional(36.0),
		egui::Color32::BLUE
	);
}

シェイプの利用

fn plot(ui: &mut egui::Ui){
	let data = vec![
		egui::Pos2::new(50.0, 100.0),
		egui::Pos2::new(250.0, 100.0),
		egui::Pos2::new(75.0, 225.0),
		egui::Pos2::new(150.0, 50.0),
		egui::Pos2::new(225.0, 225.0)
	];
	let stroke_1 = egui::Stroke::new(5.0, egui::Color32::RED);

	let mut shape_1 = eframe::epaint::PathShape::line(data, stroke_1);
	shape_1.closed = true;
	ui.painter().add(shape_1);
}

クリックした位置に描く

impl eframe::App for MyEguiApp {
	fn update(&mut self, _ctx: &egui::Context, _frame: &mut eframe::Frame){
		egui::CentralPanel::default().show(ctx, |ui|{
			ui.heading("Hello World!");
			let resp = ui.allocate_response(egui::vec2(400.0, 300.0), egui::Sense::click());
			if resp.clicked(){
				let p = resp.interact_pointer_pos().unwrap();
				self.click_pos.push(p);
			}
			plot(ui, &self.click_pos);
		});
	}
}

fn plot(ui: &mut egui::Ui, pos: &Vec<egui::Pos2>){
	for p in pos {
		ui.painter().circle_filled(*p, 25.0,
			egui::Color32::from_rgba_premultiplied(255, 0, 0, 100));
	}
}

Rust eguiで主なeguiを利用

fn main() {
    let native_options = eframe::NativeOptions::default();
    native_options.default_theme = eframe::Theme::Light;
    native_options.initial_window_size = Some(egui::Vec2 {x:400.0, y:200.0});

    let _ = eframe::run_native("My egui App", native_options,
    	Box::new(|cc| Box::new(MyEguiApp::new(cc))));
}

impl Default for MyEguiApp {
	fn default() -> MyEguiApp {
		MyEguiApp {
			value:0,
		}
	}
}

#[derive(Default)]
struct MyEguiApp {
	pub value:usize,
}

impl MyEguiApp {
	fn new(_cc: &eframe::CreationContext<'_>)-> Self {
		Self::default()
	}
}

impl eframe::App for MyEguiApp {
	fn update(&mut self, _ctx: &egui::Context, _frame: &mut eframe::Frame){
		egui::CentralPanel::default().show(ctx, |ui|{
			ui.heading("Hello World!");

			ui.spacing();

			let msg = format!("click {} times.", self.value);
			let label_txt = egui::RichText::new(msg)
				.size(32.0);
			let label = egui::Label::new(label_txt);
			ui.add(label);

			ui.separator();

			let btn_txt = egui::RichText::new("Click!")
				.font(egui::FontId::proportional(24.0));
			let btn = egui::Button::new(btn_txt);
			let resp = ui.add_sized(egui::Vec2{x:150.0, y:40.0}, btn);
			if resp.clicked() {
				self.value += 1;
			}
		});
	}
}

チェックボックス

impl eframe::App for MyEguiApp {
	fn update(&mut self, _ctx: &egui::Context, _frame: &mut eframe::Frame){
		egui::CentralPanel::default().show(ctx, |ui|{
			ui.heading("Hello World!");

			ui.spacing();

			let msg = format!("checked = {}.", self.value);
			let label_txt = egui::RichText::new(msg)
				.size(32.0);
			let label = egui::Label::new(label_txt);
			ui.add(label);

			ui.separator();

			let check_txt = egui::RichText::new("checkbox")
				.size(24.0);
			let check = egui::Checkbox::new(&mut self.value, check_txt);
			let _resp = ui.add(check);
		});
	}
}

ラジオボタン、スライダー、ドラッグバリュー、選択ラベル、テキストエディット、パスワードなども