概要
rustを勉強し始めてから英語ドキュメントを読む機会が格段に増えました。
英語が苦手なので頻繁に調べる必要があるのですけど、
調べた結果は何とか保存しておきたいと思ったわけです。
そんな動機からrust + web(PC、スマホで使いたいから)で単語帳アプリを作ってみました。
内容には全く自信がないですがrustでwebアプリを作ろうと思った人の
ご参考に少しでもなればと思います。(rustでwebアプリ・・・無いかな
環境
WEB部分は大量のcrateを使っていますが基本はironとhandlebars_iron、reqwestです。
DBはmongodbでセッション管理にはredisを使いました。
WEB部分だけでもお腹一杯なのでmongodb、redisは別の記事に書きたいと思っています。
注意
このプログラムを書き始めてからironは開発中止?(メンテナを募って継続する?)
という話を知りました。
今から作成するならironは使わない方がいいかもしれません。
##メイン処理
とりあえず、main.rsの全容です。
fn create_handlers() -> Vec<Box<PageHandler>> {
let handlers: Vec<Box<PageHandler>> = vec![
Box::new(Top::new()),
Box::new(Auth::new()),
Box::new(Detail::new()),
Box::new(Translate::new())
];
handlers
}
fn main() {
//request controller
fn controller(req: &mut Request) -> IronResult<Response> {
let handlers = create_handlers();
//if url not registered. show auth page
let auth: Box<PageHandler> = Box::new(Auth::new());
let target: _ = {
let path_str = req.url.path()[0];
let handler = if let Some(handler) = handlers.as_slice().iter().filter(|h| h.path() == path_str).next() {
handler
} else {
&auth
};
handler
};
target.handler(req)
}
//Create Router
let mut router = Router::new();
for handler in create_handlers() {
if handler.is_get() {
router.get(handler.bind_url(), controller, handler.template());
}
if handler.is_post() {
router.post(handler.bind_url(), controller, handler.template());
}
}
//crate mount
let mut mount = Mount::new();
mount.mount("/", router)
.mount("/css/", Static::new(Path::new("./src/css")));
//Create Chain
let mut chain = Chain::new(mount);
// Add HandlerbarsEngine to middleware Chain
let mut hbse = HandlebarsEngine::new();
hbse.add(Box::new(DirectorySource::new("./src/templates/", ".hbs")));
if let Err(r) = hbse.reload() {
panic!("{}", r.description());
}
chain.link_after(hbse);
println!("Listen on localhost:3000");
let iron_instance = Iron::new(chain);
println!("worker threads:{}", iron_instance.threads);
iron_instance.http("localhost:3000").unwrap();
}
pub trait PageHandler{
fn is_get(&self) -> bool;
fn is_post(&self) -> bool;
fn path(&self) -> &str;
fn template(&self) -> &str;
fn bind_url(&self) -> &str;
fn handler(&self, req: &mut Request) -> IronResult<Response>;
}
各ページ用のリクエストハンドラは上記のtraitを実装することを前提にしています。
create_handlersを見て頂ければ分かると思いますが
現状は4つのWEBページが存在しています。
//Create Router
let mut router = Router::new();
for handler in create_handlers() {
if handler.is_get() {
router.get(handler.bind_url(), controller, handler.template());
}
if handler.is_post() {
router.post(handler.bind_url(), controller, handler.template());
}
}
これらのtraitをvectorに突っ込んで廻しながらgetもしくはpostのリクエストと
実際のリクエストハンドラを紐づけていきます。
ここで使用しているのはrouterというcrateです。
第一引数がurl中のパスで、そのパスに対するhttpリクエストがあったときに
第二引数の関数が呼ばれます。ここではクロージャを渡しています。
第三引数は対応するHandlebarsのファイル名(拡張子無し)です。
fn controller(req: &mut Request) -> IronResult<Response> {
let handlers = create_handlers();
//if url not registered. show auth page
let auth: Box<PageHandler> = Box::new(Auth::new());
let target: _ = {
let path_str = req.url.path()[0];
let handler = if let Some(handler) = handlers.as_slice().iter().filter(|h| h.path() == path_str).next() {
handler
} else {
&auth
};
handler
};
target.handler(req)
}
クロージャ部分です。ここでは2つ理解が及ばなくて諦めたことがあります。
一つはhandlersはクロージャの外でも使っているので
外で宣言した変数をキャプチャできれば、
毎回create_handlers関数を呼び出す必要が無いと思うのですがエラーが解消できなかった点
もう一つはハンドラが無かった時に返すauthをあらかじめ生成している点です。
できれば必要になった段階で生成したいのですがlifetimeの書き方が良く分からず。
今後の課題です。
targetの代入部がブロックになっているのはBorrow Checker逃れのためです。
req.url.path()がimmutable borrow、target.handler(req)がmutable borrowで
コンパイラに怒られます。 こんな回避で良いのかは謎です。
//crate mount
let mut mount = Mount::new();
mount.mount("/", router)
.mount("/css/", Static::new(Path::new("./src/css")));
//Create Chain
let mut chain = Chain::new(mount);
// Add HandlerbarsEngine to middleware Chain
let mut hbse = HandlebarsEngine::new();
hbse.add(Box::new(DirectorySource::new("./src/templates/", ".hbs")));
if let Err(r) = hbse.reload() {
panic!("{}", r.description());
}
chain.link_after(hbse);
残りに関しては、大したことをしてないのでざっくりと。
mountとstaticというcrateでcssファイルへのリクエストがあった場合に
ダウンロードできるように紐づけを行っています。
もし、外部javascriptファイルを使うのであればここに追加します。
HandlebarsEngineはhtmlのテンプレートを用意しておいて
サーバーサイドで生成した値とかを埋め込んで返す仕組みです。
このサイトを参考にさせていただきました。
リクエストハンドラ
単語と意味の一覧を表示するページです。
pub struct Top {}
impl Top {
pub fn new() -> Top {
Top {}
}
}
impl PageHandler for Top {
fn is_get(&self) -> bool {
true
}
fn is_post(&self) -> bool {
false
}
fn path(&self) -> &str {
""
}
fn template(&self) -> &str {
"index"
}
fn bind_url(&self) -> &str {
"/"
}
fn handler(&self, req: &mut Request) -> IronResult<Response> {
match check_authorized(req) {
None => return authrized_handler(req),
_ => {},
}
let mut resp = Response::new();
let mut data = HashMap::new();
let mongo = Mongo::new();
let url = format!("{}", url_for(req, "answer", HashMap::new()));
let url_detail = format!("'{}'", format!("{}", url_for(req, "detail", HashMap::new())) + "?word=");
let word_list = format!("[{}]", mongo.get_translated_list());
data.insert(String::from("translate_path"), url);
data.insert(String::from("detail_path"), url_detail);
data.insert(String::from("list"), word_list);
resp.set_mut(Template::new("index", data)).set_mut(status::Ok);
Ok(resp)
}
}
data.insertでHandlebarsのテンプレートに値を埋め込みます。
例えば、data.insert(String::from("translate_path"), url);であれば
テンプレート側のtranslate_pathという部分をurlという変数の値で置き換えてhtmlを返します。
reqwestでWEB API呼び出し
実際の英単語の翻訳はglosbeというWEBサービスを利用させていただいています。
//登録が無いため WEB APIで取得する。
let enc_word = match serde_urlencoded::to_string(word.clone()) {
Ok(word) => word.trim().to_string(),
Err(_e) => word.trim().to_string()
};
let url = format!("https://glosbe.com/gapi/translate?from=en&dest=ja&format=json&phrase={}&pretty=false", enc_word);
let json_str = match reqwest::get(url.as_str()) {
Ok(mut req) => {
match req.text() {
Ok(t) => t,
Err(e) => return format!("'get error(to text) {}'", e.to_string())
}
},
Err(e) => return format!("'get error {}'", e.to_string())
};
let data: TranslateResult = match serde_json::from_str(json_str.as_str()) {
Ok(j) => j,
Err(e) => return format!("'serialize error {}'", e.to_string())
};
一応、入力された文字列をurlエンコードしてパラメータ付きのurlを生成し
reqwestというcrateでhttpリクエストを投げています。
戻り値はserde_jsonというcrateを使えば文字列から直に構造体に変換してくれます。
下記のような感じで戻ってくるjsonに合わせた構造体の定義は必要です。
#[derive(Debug, Serialize, Deserialize)]
pub struct TranslateResult {
pub result: String,
pub tuc: Vec<Tuc>,
pub phrase : Option<String>,
pub from : Option<String>,
pub dest : Option<String>,
}
#[allow(non_snake_case)]
#[derive(Debug, Serialize, Deserialize)]
pub struct Tuc {
pub phrase: Option<MeanItem>,
pub meanings: Option<Vec<MeanItem>>,
pub meaningId: Option<i64>,
pub authors: Option<Vec<Option<i64>>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MeanItem {
pub language: String,
pub text: String,
}
jsonの配列はvecで受け取れます。
また、値や項目があったりなかったりする場合はOption<T>にしないとエラーになりました。
使う際にOption<T>だとSomeかNoneを見ないといけないので
ぶっちゃけ面倒ではありますね。
最後
現状のソースはここに置いてあります。
絶賛、書き換え中なので本文と違ったらすいません。
内容に誤り等々、あったらすいませんがご指摘いただけるとありがたいです。