class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
final title = 'Flutterサンプル';
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(
title: this.title,
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
title ='Flutter Demo'
}): super();
@override
_MyHomePageState createState() => _MyHomePageState();
}
class Data {
int _price;
String _name;
Data(this._name, this._price): super();
@override
String toString() {
return _name + ':' + _price.toString() + '円';
}
}
class _MyHomePageState extends State<MyHomePage> {
static final _data = [
Data('Apple',200),
Data('Orange',150),
Data('Peach',300)
];
Data _item = _data[0];
void _setData(){
setState((){
_item = (_data..shuffle()).first;
});
}
@override
Widget build(BuildContext context) {
return Scaffold (
appBar: AppBar(
title: Text('Set data'),
),
body: Text(
_item.toString(),
style: TextStyle(fontSize:32.0),
),
floatingActionButton: FloatingActionButton(
onPressed: _setData,
tooltip: 'set message',
child: Icon(Icons.star),
),
);
}
}
【Flutter】StatefulWidgetについて
class ${widgetClass} extends StatefulWidget {
@override
ステートクラス createState() => ステートクラス();
}
stateクラスの場合は、Widget build(BuildContent context) だった
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
final title = 'Flutterサンプル';
final message = 'サンプルメッセージ';
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(
title: this.title,
message: this.message
),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
final String message;
const MyHomePage({
Key? key,
required this.title,
required this.message
}): super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold (
appBar: AppBar(
title: Text(widget.title),
),
body: Text(
widget.message,
style: TextStyle(fontSize:32.0),
)
);
}
}
_MyHomePageState createState() => _MyHomePageState(); のところをステートの処理に従って変化するよに処理を作成すれば良い
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
final title = 'Flutterサンプル';
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(
title: this.title,
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
required this.title,
}): super();
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _message = 'Hello!';
void _setMessage(){
setState((){
_message = 'タップしました!';
});
}
@override
Widget build(BuildContext context) {
return Scaffold (
appBar: AppBar(
title: Text(widget.title),
),
body: Text(
_message,
style: TextStyle(fontSize:32.0),
),
floatingActionButton: FloatingActionButton(
onPressed: _setMessage,
tooltip: 'set message',
child: Icon(Icons.star),
),
);
}
}
Flutterとは
### Flutterの特徴
Flutterはマルチプラットフォームのための開発環境
SDKにより専用のAPIを提供している
Androidで採用されているマテリアルデザインをベースに設計しているが、iOS用のクパティーノというUIフレームワークを用意している
開発言語はDart
プログラムをプラットフォームごとにネイティブコードにコンパイルする
IntelliJ(Android Studio)/Visual Studio Codeに対応
### Flutter開発に必要なもの
JDK
Dart
Flutter SDK
IntelliJ(Android Studio)/Visual Studio Code
Visual Studio
Xcode
$ java -version
openjdk version “18.0.2.1” 2022-08-18
OpenJDK Runtime Environment Homebrew (build 18.0.2.1+0)
OpenJDK 64-Bit Server VM Homebrew (build 18.0.2.1+0, mixed mode, sharing)
flutterをdownload
https://docs.flutter.dev/get-started/install
パスを通す
$ vi ~/.bash_profile
export PATH=$PATH:/Users/mac/flutter/flutter/bin
$ source ~/.bash_profile
$ flutter upgrade
intelliJ からdownload
https://www.jetbrains.com/ja-jp/idea/
main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Text(
'hello, Flutter world!!',
style: TextStyle(fontSize:32.0),
),
);
}
}
Flutterはwidgetという部品の組み合わせによって作成される
widget treeによって記述する
ウィジェットによって記述
mainはアプリ起動時に呼び出される関数
void main() {
runApp(MyApp());
}
StatelessWidgetはステート(状態を表す値)を持たないwidgetのベースとなるクラス
StatelessWidgetかStatefulWidgetのいずれかを継承して作成する
MaterialAppというインスタンスが返されている
class ${className} extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(...略...),
}
}
titleはアプリケーションのタイトル、homeがwidget
Textウィジェットを指定
return const MaterialApp(
title: 'Flutter Demo',
home: Text(
'hello, Flutter world!!',
style: TextStyle(fontSize:32.0),
),
);
### アプリの構造
– アプリケーションはmain関数として定義
– runApp関数では、StatelessWidget継承クラスのインスタンスを引数に指定
– StatelessWidget継承クラスにはbuildメソッドを用意する
– MaterialAppの引数homeに、実際にアプリ内に表示するwidgetを設定する
標準的なwidgetはwidget.dartというパッケージにまとめられている
material.dartとcupertino.dartが用意されている
ScaffoldとAppBar
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("Hello Flutter!"),
),
body: Text(
'hello, Flutter world!!',
style: TextStyle(fontSize:32.0),
),
),
);
Scaffoldは建築現場の足場
Scaffoldには基本的なデザインとレイアウトが組み込まれている
Scaffoldでは Scaffold(appBar:, body:)としてインスタンスを作成する
空白エリアの表示を担当するのがbody
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", ¶ms).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}} <{{item.mail}}>
</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}}) <{{value.0}}>
</li>
{% endfor %}
</ul>
loop.indexは繰り返し情報がまとめられたオブジェクト
### フィルターの利用
フィルターは、あらかじめRust側で定義した関数を使ってテンプレートの表示を変換する機能
フィルター用の関数は形が決まっている
fn 関数(引数1: &value, 引数2: &HashMap
作成したフィルター関数は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())
}