年末だし、プラグインの入れ替えをしました

この記事は Vim Advent Calendar 2021 11日目の記事です。

My (Neo)Vim RC is here!

github.com

背景

なんだかんだVimを使い初めて9年が経っておりました。

プラグインGitHubで管理し始めて6年です。

更にVimからNeoVimに変えて5年が経ちます。

これまでも色々なプラグインを追加したり取り除いたり乗り換えたりしてきました。最初は10個ほどだったプラグインも気付けば60前後になっています。

設定ファイルは、ずっとひとつのvimrcを加筆修正してきたわけではなく、たまには全体を見直して調整などをしておりました。 それでも積み重なったものはあるようで、喧嘩しているプラグインや、はたまた組み合わせて拡張したいプラグインなどがでてきました。

そこで、直近で全体的な見直しを行いましたので、その時の思いや変化を書き残したいと思います。

解決したいこと

今回見直すにあたって、解決したかった課題は以下のとおりです。

(1) dein を使いこなせてない

プラグインマネージャーは、最初 NeoBundle を使っており、 NeoVim 移行のタイミングで dein を使い始めました。

dein は、プラグインの読み込みタイミングの細かい制御や、依存関係の管理など、高度な機能を数多く提供しています。しかし、私は実際のところこれを使いこなせていません。

例えば、 g:hoge_auto_enable = 1 が設定してあるときに自動で有効になるプラグインがあるのですが、その設定を書いているにも関わらずファイルの開き方(起動時に開くかVim内から開くか)で動かなかったりしました。

また、プラグインAとBが両方読み込まれたときに有効にしたい設定をどこに記述するのかも悩ましいところでした。私は dein の設定をTOMLで管理していましたが、片側のhookに書くのは反対側から見えなくなるので違うな……と思っていました。

(2) LanguageServerが二重起動している

6年前はまだLanguageServerは影もありませんでした。また、静的解析系のプラグインもなしで、書いたコードを逐次コンパイル(または実行)して確認していました。 小さいコードのうちはそれで全然問題なかったのですが、コードが大きくなってくるとエラーメッセージの見やすさやコンパイル時間が問題になってきます。

最初に導入したのは Syntastic でした。これでJavaファイルの保存時にエラーを出すようにしていました。とはいえ、 Syntastic は同期的にチェックしますので、実行時間が問題になってきます。

そこでしばらく悩んでいたところ、 非同期でエラーチェックしてくれる ALE を見つけました。これはいろいろなLinter, compilerのエラー情報を非同期でチェックし、まとめて出してくれるプラグインです。 all-in-one 系のプラグインなので、設定もあまりせずに使えていました。

しばらくしてLanguageServerが(私のvimrcに)登場しました。 LanguageServerでは、静的解析や補完の提供、フォーマットなど、IDEの機能を一通り提供してくれます。 私は deoplete + LanguageClient-neovim を使って、こちらで補完のみを行い、静的解析は ALE で行っていました。 というのも、LanguageServerが提供されたばかりの頃は、あまり安定していなかったり、言語によってまだ「これ」というものがなかったりしており、「linterもLanguageServerも使っていい感じの解析結果になる」くらいの塩梅でした。 この頃から「 ALELanguageClient-neovim の両方でLanguageServerを起動している」問題がありました。これの何が問題かというと、たとえば rust ではビルド結果のディレクトリのロックを取り合うので、片方の解析結果が遅れたり、CPUへの負荷が無駄に高くなったりしていました。

それから時が経って、LanguageServerが成熟してきました。linterが全く必要ないとは言いませんが、私の環境ではLanguageServerメインでやっていってもいい頃になってきたなと思っています。

(3) ファイルビューアーが重い

長らく NERDTree を愛用してきました。これにプラグインを追加して、Gitのステータスを見られるようにしていました。

NERDTree は他のエディタから入ってきたときに、一番わかりやすく便利なプラグインかもしれません。編集しているファイルの横にファイルツリーを表示したいというのは普通の欲求です。また、 NERDTree にはデフォルトで(ヘルプ表示を含む)各種キーバインドがあります。覚えるのに少しかかりますが、入れるだけで無設定で使えるのは便利です。

一方で、比較的重いプラグインでもあります。 NERDTree 単体だとそこまでではないのですが、 nerdtree-git-plugin を入れると少し重くなり(バッファを開くのに若干時間がかかる程度)、 vim-devicons + vim-nerdtree-syntax-highlight の追加で激重になります(j連打でのカーソル移動が数秒遅延する程度)。 ファイルのアイコン表示自体はいいなと思ったのですが、流石にカーソル移動がままならないのでは使い続けるのは難しいので、より軽量な解決策が必要になりました。

今回の大きな変更点

上記の解決したいことを踏まえて、更新をしていきました。ここでは、大きな変更点を紹介します。

vim-plugへの移行

これは後述するプラグイン設定の管理方法のためにやった部分もありますが主に (1) のために、プラグイン管理を dein から vim-plug へと移行しました。

vim-plug は、長くメンテされているプラグインマネージャで、外部依存がありません。ファイルタイプとコマンド実行時の遅延読み込みはあるものの、他の高度な機能は無く、シンプルなものになっています。

今回その他プラグインマネージャーも見たのですが、これだけ機能があれば十分なことと、長く広く使われているためWebから情報を拾いやすい点でこちらにしました。

プラグイン設定の管理方法

プラグイン設定の管理方法は、ほとんど以下を参考にしました。

zenn.dev

ただし、上記はプラグインごとのconfigファイルの場所がruntimepathを前提にしているので、少し変えています(私はvimrcを書き替えるときにvimが使えないと困るので、 nvim -u path/to/init.vim でなるべくどこでも動くようにしています)。

現在、この方法で問題なく作業できています。特に難しいところはなく、プラグインマネージャーの設定ではなくプラグインや本体の設定改善に時間を使えています。

coc frameworkへの移行

お世話になった ALE, deoplete, LanguageClient-nvim, vim-lsp を外し、 coc を導入しました。

踏み切れたのは、LanguageServer関連の成熟が大きいと思います。 また、 cocREADMEWiki におすすめの設定やmappingが書かれていて、非常に導入しやすかったです。

cocになって、大きく不満がある箇所は特にないです。全体的に問題なく動いています。

今は、直前まで使っていた vim-lsp との使用感の違い(例えば、CodeActionの対象があったときに、範囲指定が必要かどうかなど)に慣れていっている段階です。

Fernへの移行

NERDTree から Fern へ移行しました。関連プラグインと、 Wiki記載の NERDTree に寄せるマッピング でおよそ違和感なく使えています。

NERDTree と Fern の差を埋める

以下設定で NERDTree 風のコマンドを定義しました。

私は :NERDTreeFind を多用していたのですが、これに相当するコマンドは Issue に記載されていました。

command! FERNTree       Fern . -drawer
command! FERNTreeToggle Fern . -drawer -toggle
command! FERNTreeFind   Fern . -drawer -reveal=%
command! FEF            FERNTreeFind

コマンド定義はしたものの、絶対に :NEF (:NERDTreeFindマッピングしている) を打ってしまうと思うので、 NERDTree が読み込まれていないときは NERDTree 系のコマンドを fern に寄せます。

まず、 UsePlugin コマンドを拡張して否定形を利用できるようにします。

" UsePlugin 'hoge' で利用チェック
" UsePlugin! 'foo' で不使用チェック
command! -bang -nargs=1 UsePlugin if <bang>!FindPlugin(<args>) | finish | endif

そして、 fern を利用していて NERDTree を入れていないときだけ :NERDTree:FERNTreeマッピングしました。

UsePlugin 'fern.vim'
UsePlugin! 'nerdtree'


" 絶対打ってしまうと思うのでmapping
command! NERDTree       FERNTree
command! NERDTreeToggle FERNTreeToggle
command! NERDTreeFind   FERNTreeFind
command! NEF            FERNTreeFind

プラグインと設定紹介

最後に、いくつかプラグインと設定を紹介します。

easymotion

カーソル移動系のプラグインです。 画面内の任意の位置に、おそらくほぼ最短で辿り着けるプラグインだと思います。

重要な特徴として、 視線を移動目標から動かさずに辿り着ける というのがあるかと思います。 README最初のGIFは、565行目の let s:s へ移動しようとしています。ぜひこれを認識した上でもう一度GIFをご覧いただきたいです。

プラグインの詳細は以下の記事をご覧ください。 haya14busa.com

私の設定ですが、以下のようにして、normalモードでの s, f (行内ジャンプ)をよく使います。

nmap s <Plug>(easymotion-s2)
xmap s <Plug>(easymotion-s2)
map f <Plug>(easymotion-fl)
map t <Plug>(easymotion-tl)
map F <Plug>(easymotion-Fl)
map T <Plug>(easymotion-Tl)

gina

GIt操作系プラグインです。

:Gina statusgit status と同様のバッファを開き、そこで add, reset を行い、 commit していきます。

それほど多くを覚えずとも使えるのと、複数のファイルをまとめてaddできる点が便利です。

設定としては、 lightline に情報を表示するのに直接関数を呼ぶのではなく、適当に状態管理して呼び出しを削減し、変数にキャッシュしています。これは、statuslineに重くなり得るコンポーネントを置くとカーソル移動に影響する(遅くなる)事があるからです。

augroup gina-status-lazy
  autocmd!
  autocmd CursorMoved,CursorMovedI * call s:f_gina_status('CursorMoved')
  autocmd CursorHold,CursorHoldI * call s:f_gina_status('CursorHold')
  autocmd WinEnter * call s:f_gina_status('WinEnter')
  autocmd WinLeave * call s:f_gina_status('WinLeave')

  let s:gina_status = 0
  let g:gina_status_str = MyGinaRepoStatus()
  function! s:f_gina_status(event)
    if a:event ==# 'WinEnter'
      let g:gina_status_str = MyGinaRepoStatus()
      let s:gina_status = 2
    elseif a:event ==# 'WinLeave'
      " Do nothing
    elseif a:event ==# 'CursorMoved'
      if s:gina_status
        if 1 < s:gina_status
          let s:gina_status = 1
        else
          " Do nothing
          let s:gina_status = 0
        endif
      endif
    elseif a:event ==# 'CursorHold'
      let g:gina_status_str = MyGinaRepoStatus()
      let s:gina_status = 1
    endif
  endfunction
augroup END

(今見れば読めたものではないですが、とりあえず呼び出しを削減したかった、ということだけお伝えできればと思います)

vista

LSPなどの情報をもとにファイルの構造情報(アウトラインや関数変数の一覧など)を出せます。 設定することはあまりなく、 :Vista で構造を表示できます。それだけなのですが、IDEライクになり便利です。

その他個別の設定

現在編集しているファイルの相対パスを取得する

チームで作業しているとき、リポジトリpath/to/Hoge.java についてコミュニケーションをとりたいときがあるかと思います。 ではそのファイル名をどのように伝えたら良いのでしょうか。

  1. Hoge.java などファイル名だけtypeする
  2. GitHubで同じファイルを開いてURLで共有する
  3. Vimにコマンドを定義してクリップボードにコピーする

1はシンプルに大変です。

2はファイル名が十分短く一意ならば有効です。一方でファイル名が一意でないときは曖昧な補足情報(「infra の~」とか)を付け加えたりする必要があります。

3はブラウザを開き、同じファイルにたどり着くだけ手間がかかります。

そういうわけで私は4の方法を取りました。 私は基本的にrepository rootでVimを開いているので、以下のような定義で :CopyFilePath を定義しました。

function! s:lineNumberWithSeparator(line1, line2)
  if a:line1 != a:line2
    return ": " . a:line1 . "-" . a:line2
  endif
  return ""
endfunction

"""現在のファイルの相対パスを取得
function! RelativeFilePath(line1, line2)
    return expand("%"). s:lineNumberWithSeparator(a:line1, a:line2)
endfunction

command -range CopyFilePath let @+ = call('RelativeFilePath', [<line1>, <line2>])

このコマンドは範囲選択にも対応しており、その場合は path/to/Hoge.java: 12-15 のように行番号付きでコピーできます。 また、私の環境では :Cop で一意になるのでこのコマンドが実行できます。

セッションファイルの保存先設定

vim-session を使っているのですが、セッションファイルの保存先を以下の優先順位で決めています。

  1. 現在のディレクトリ直下の .vimsessions フォルダ
  2. gitリポジトリ配下ならリポジトリroot直下の .vimsessionsフォルダ
  3. ~/.config/nvim/default-sessions

また、Gitのブランチごとにセッションを簡単に分けられるよう、 :SaveBranchSession, :LoadBranchSession を定義しています。 このあたりの設定は長くなるので 設定ファイル をご覧ください。

おわりに

久しぶりにvimrcの大きな見直しができました。軽量化できたもの、まだ改善したいものなどもありますが、いいデジタル大掃除ができたと思っています。

電動昇降デスクをDIYで自作した

  • より大きい、複数の高さにできるデスクが欲しくなり、電動昇降デスクの購入を決意しました
  • 天板を注文し、DIYしました
  • 満足行くものになり、快適に使っています

【目次】

  • 1. 経緯
    • 1.1. 現状の暮らし
    • 1.2. 引っ越し
    • 1.3. 趣味
  • 1.4. その他インプット
  • 2. 目的
  • 3. 構成検討
    • 3.1. デスク本体検討
    • 3.2. 引っ越しを見越した検討
    • 3.3. ケーブリングの検討
    • 3.4. テーブル塗装の検討
    • 3.5. デスク周りの検討
  • 4. 注文
    • 4.1. 天板
    • 4.2. 昇降脚
    • 4.3. その他
  • 5. 制作・配置・配線
  • 5.1. 到着→研磨(月曜日)
  • 5.2. 塗装(火・水曜日)
  • 5.3. 組み立て、鬼目ナットの埋め込み(木・金曜日)
  • 天板とのネジ止め、配置、配線(土・日曜日)
  • 6. 完成
  • Appendix
    • App. 1. あって便利だったもの
      • 端材
      • 塗料トレー、コテバケ
      • キムワイプ
      • 紙やすりをマジックテープでつけるスポンジ
      • ハンディー掃除機
      • マスク
      • ダンボー
      • ブルーシート
      • 芯ホルダー
    • App. 2. 買ったけど使わなかったもの・やらなくてよかったこと
      • ふつうの刷毛
      • ケーブルダクト穴は不要だった
  • おわりに
続きを読む

Rust 1.49時点のテストカバレッジ事情雑まとめ

背景

Rustのコードを書いていて、テストカバレッジを取りたいなと思ったのですが、インターネット上の情報が新旧入り混じっており困ったので雑にまとめました。

2021/02/11現在、「rust カバレッジ」で検索すると、以下の記事が上位にヒットしてきました。

上記の記事から時間が経って、状況が変化している部分があります。 これらで紹介されているツールをまとめます。

cargo-cov

2018年11月が最後のコミットとなっており、メンテされていないようです。 上記のOPTiMのブログで、「ソースコードの一部をカバレッジ計測から除外する機能が無い」との記述がありますが、メンテ状況からしてその点は変化していないでしょう。

cargo-kcov

cargo-cov と同じ作者のものなのですが、こちらも2019年12月でメンテが止まっています。 stableでも動くようなので、確認してみたのですが、1.44から動かなくなっているようでした。 issueを見る感じだと、しばらく対応される見込みもないかなという感じです。

grcov

Mozilaがメンテしているのですが、nightlyが必要です(ので検証していません)。

tarpaulin

stableで動きますが、Linux限定のようです。 とはいえ、Windows, MacからはDockerを使えば動かせるので、大きな問題はないかもしれません。

tarpaulinでカバレッジをとる

以下のコード src/main.rs に対してカバレッジを取ります。

#[cfg(not(tarpaulin_include))]
fn main() {
    println!("{}", square(10, true));
    println!("Hello, world!");
}

fn square(x: i32, doing: bool) -> i32 {
    if doing {
        x * x
    } else {
        x
    }
}

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

    #[test]
    fn square_with_doing() {
        let x = 10;
        let result = square(x, true);
        assert_eq!(result, 100);
    }

    #[test]
    fn square_without_doing() {
        let x = 10;
        let result = square(x, false);
        assert_eq!(result, 10);
    }
}

#[cfg(not(tarpaulin_include))] のつけられた部分は、カバレッジ計測から除外されます。

実行

シンプルにできます。

$ cargo tarpaulin
(...中略...)
running 2 tests
test tests::square_with_doing ... ok
test tests::square_without_doing ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Feb 11 06:33:06.671  INFO cargo_tarpaulin::report: Coverage Results:
|| Tested/Total Lines:
|| src/main.rs: 12/12
|| 
100.00% coverage, 12/12 lines covered

結果

オプション無しの実行では、結果は上記の標準出力のみになるようです。

--out Html をつけると、 tarpaulin-report.html が生成されます。
f:id:vraisamis:20210211064730p:plain

ファイル名をクリックすると、ファイルごとのカバレッジが表示されました。 f:id:vraisamis:20210211064918p:plain

ついでに: オプション管理

出力形式オプションとセットで管理するなら、 cargo-make などを利用するのがいいかもしれません。

以下の Makefile.toml を用意すると、 cargo make coverage でHTML形式の出力が得られます。

[env]
CARGO_MAKE_COVERAGE_PROVIDER = "tarpaulin"

[tasks.coverage-tarpaulin.linux]
command = "cargo"
args = ["tarpaulin", "--out", "Html"]

(commandはoverrideする必要ない気がするのですが、しないとできませんでした。バグか仕様かは未調査です)

Rust の構造体Vecをsort_byしやすくするマクロを書いた

なんでつくったか

例えば x, y, z がある以下のような構造体があって、

struct Sample {
    x: i64,
    y: i64,
    z: i64,
}

v: Vec<Sample>をソートしたい。ただし、並び替える順番は利用箇所によって違う。

こんなときに、いつも、 sort_by を使って、以下のように書いていた*1

// x昇順→y昇順→z昇順
v.sort_by(|l, r| {
    match l.x.cmp(&r.x) {
        std::cmp::Ordering::Equal => {
            match l.y.cmp(&r.y) {
                std::cmp::Ordering::Equal => {
                    l.z.cmp(&r.z)
                },
                other => other
            }
        },
        other => other
    }
});

これは書くの面倒だし、やりたいことの割に長くて目を引く。

ところでSQLでは order by x, y, z と書けるな、とふと思って、 そんな感じで簡単に書けるようにするマクロが欲しくなった。ので作った。

使い方

以下のように使う。

let mut v = vec![
    Sample {x: 10, y: 200, z:3000},
    Sample {x: 10, y: 100, z:1000},
    // ...
];

// x昇順→y昇順→z昇順
v.sort_by(order_by!(x, y, z));
// x昇順→y降順
v.sort_by(order_by!(x asc, y desc));
// z昇順→x昇順
v.sort_by(order_by!(z asc, x));

さらに、タプルでもいける。

let mut v2: Vec<(i64, i64, i64)> = vec![
    (20, 100, 1000),
    (10, 300, 1000),
    (10, 300, 2000),
    (30, 300, 2000),
    (30, 300, 3000),
    // ...
];

v2.sort_by(order_by!(0, 1, 2));
v2.sort_by(order_by!(0 desc, 1, 2));
v2.sort_by(order_by!(2, 0, 1));

マクロコード

macro_rules! order_by {
    ($($x:tt)*) => {
        |l, r| {
            order_by_inner!(l, r, $($x)*)
        }
    }
}
macro_rules! order_by_inner {
    () => {};
    ($l:ident) => {std::cmp::Ordering::Equal};
    ($l:ident , ) => {std::cmp::Ordering::Equal};
    ($l:ident , $r:ident) => {std::cmp::Ordering::Equal};
    ($l:ident , $r:ident , ) => {std::cmp::Ordering::Equal};

    // last
    ($l:ident , $r:ident , $x:tt asc) => {
        $l.$x.cmp(&$r.$x)
    };
    ($l:ident , $r:ident , $x:tt desc) => {
        $l.$x.cmp(&$r.$x).reverse()
    };
    ($l:ident , $r:ident , $x:tt) => {
        order_by_inner!($l, $r, $x asc)
    };

    // mid
    ($l:ident , $r:ident , $x:tt asc , $($p:tt)+) => {
        match $l.$x.cmp(&$r.$x) {
            std::cmp::Ordering::Equal => {
                order_by_inner!($l, $r, $($p)+)
            },
            other => other
        }
    };
    ($l:ident , $r:ident , $x:tt desc , $($p:tt)+) => {
        match $l.$x.cmp(&$r.$x).reverse() {
            std::cmp::Ordering::Equal => {
                order_by_inner!($l, $r, $($p)+)
            },
            other => other
        }
    };
    ($l:ident , $r:ident , $x:tt , $($p:tt)+) => {
        order_by_inner!($l, $r, $x asc, $($p)+)
    };
}

*1:もっといいやり方があったら教えてほしい

Rust Rocket v0.4.0 の入門

この記事はRustその2 Advent Calendar 2018の23日目の記事です。

Rust の Web フレームワークである Rocket に入門していきます。

この記事では、公式のサンプルを参照しながら、それを解説していきます。

執筆環境

$ rustc --version
rustc 1.33.0-nightly (2d3e909e4 2018-12-22)

インストール

Rust のインストール

公式のインストール手順に従って下さい。

執筆環境と同等の環境を用意するには、以下のようにします。

$ rustup toolchain install nightly-2018-12-23
$ rustup default nightly-2018-12-23

リポジトリのダウンロード

公式のサンプルは、リポジトリに含まれています。 clone してきましょう。

$ git clone -b v0.4.0 https://github.com/SergioBenitez/Rocket.git
$ cd Rocket/examples

準備ができました。以下でサンプルを見ていきます。 以下では、各章開始時点では examples ディレクトリ直下に居る前提で話を進めます。

hello_world

概要

何はともあれhello worldからやっていきましょう。

$ cd hello_world
$ cargo run

難なく起動してくれるはずです。 この状態で、 localhost:8000 にアクセスすれば、 Hello, world! とだけ表示されたページが出てきます。

ソース

src/main.rs を見て下さい。 書いてある重要な内容は、大きく2つです。

ハンドラ

1つは、ハンドラの定義です。

hello_world では、ルートパスにアクセスした際に、 "Hello, world" が返されました。 この処理を定義する記述が以下になります。

#[get("/")]
fn hello() -> &'static str {
    "Hello, world!"
}

Rocketでは、カスタム属性(Custom Attribute) を使います。 #[get("/")] で、この関数が パス / へのGETリクエストに対するハンドラであると示しています。 get 以外にも、 put, post, delete, head, patch, options が利用でき、それぞれ HTTP のリクエストメソッドと対応しています。

ハンドラ自体は、引数なし、文字列を返すだけの関数となっています。 この戻り値がそのままレンダリングされていたわけです。 ハンドラの引数が何の役目を果たすのか、戻り値の型は単純な文字列以外には何が指定できるのか、などは、別のサンプルで解説されています。

マウント、起動

さて、やっていることのもうひとつは、マウントとアプリケーションの起動です。

fn main() {
    rocket::ignite().mount("/", routes![hello]).launch();
}

rocket::ignite() で、アプリケーションを作成します。 ここに、なんやかんや設定をして、最後に launch() すると、アプリケーションが開始され、アクセスできるようになります。

hello_world で設定しているのは、ルーティングがひとつだけです。 mount("/", routes![hello]) が、hello関数をルート直下にマウントしています。

hello_person

概要

この例では、パスから動的にパラメータを取得します。

$ cd hello_person
$ cargo run

以下のページにアクセスできます。

  • localhost:8000/hello/foo
    • foo とだけ描画されます
    • foo を別の文字に変えると、描画される内容も変わります
    • foo に何も入れないと Not Found になります
  • localhost:8000/hello/foo/100
    • Hello, 100 year old named foo! と描画されます
    • 100 を別の数字に変えると、描画される内容の 100 の部分が変化します
    • 100 に数字以外(abc, 1+1 など)を入れると Not Found になります
  • それ以外は Not Found です

ソース

src/main.rs に全て書かれています。

hello_world からは、以下の点が変更されています。

  • ハンドラが2つになり、それぞれの内容が変更されています
  • ルーティングにハンドラが2つ指定されています

ルーティング

まずは、ルーティングに関して確認していきます。

    rocket::ignite().mount("/", routes![hello, hi]).launch();

routes! マクロの中にハンドラを与えていますが、これは複数並べることができます。

また、 "/" を "/yo"に変更し、起動してみましょう。

localhost:8000/hello/foo/100 へはアクセスできなくなり、 localhost:8000/yo/hello/foo/100 へアクセスできるようになりました。 同時に、 localhost:8000/hello/foo へはアクセスできなくなり、 localhost:8000/yo/hello/foo へアクセスできるようになっています。

このように、 mount() の第一引数でルーティングをディレクトリ的単位でまとめて制御することができます。

ハンドラの引数とパラメータ

ハンドラが2つありますが、 hello() だけ確認します。

#[get("/hello/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}

カスタム属性のパスに、 <name><age> が現れました。 対応するように、 hello() の引数に、 nameage が現れました。 これらが、パスからパラメータを受け取るための記述になります。

<name> の位置に対応したものが、引数 name に与えられ、 <age> の位置に対応したものが、引数 age に与えられ、関数が呼ばれます。

特に、 age は自動的に数値型にパースされます。 では、数値として読み込めなかった時はどうなるかというと、概要で示したように Not Found になります。

パースのできる/できないはどのようにして決定するのでしょうか?そもそも、パースできる型はどのように決まっているのでしょうか? 鍵は、 FromParam トレイトにあります。

FromParamトレイトは、プリミティブの数値型、 String&RawStrOption<T>Result<T, T::Error> などに実装されています。 パス文字列から各型への変換は、 from_param() によって行われます。

この変換が成功した場合、ハンドラが実行されます。

補足としては、以下のとおりです。

  • &RawStr : 生のパス文字列そのものを表す型で、変換等しないのでマッチは必ず成功します
  • String : 生のパス文字列から文字列に変換します。失敗する場合があります
  • Option<T> : 変換が成功した時は Some(T) 、失敗したときは None を返します。マッチは必ず成功します
  • Result<T, T::Error> : 変換が成功した時は Ok(T) 、失敗した時は Err(T::Error) が返ります。マッチは必ず成功します

tera_templates

概要

hello_world、hello_personでは、レスポンスは全て単純な文字列で返してきましたが、 Rocket ではテンプレートエンジンを利用してレスポンスを作成できます。

Rocketで利用できるのは、 TeraHandlebars です。ここでは Tera を紹介します。

$ cd tera_templates
$ cargo run

起動して、 localhost:8000/hello/foo にアクセスしてみましょう。HTML文章が表示されると思います。

ソース

src/main.rs からみていきます。

        .attach(Template::fairing())

テンプレートを利用するには、 Template::fairing() をアタッチしておく必要があります。

#[get("/hello/<name>")]
fn get(name: String) -> Template {
    let context = TemplateContext { name, items: vec!["One", "Two", "Three"] };
    Template::render("index", &context)
}

ハンドラの戻り値が Template となっています。 Template::render() を返すと、テンプレートの内容をレンダリングしてくれます。

render() の引数には、テンプレートの名前と、テンプレートに渡すコンテキストを指定します。 テンプレートの名前は、 "index" を指定しているので、 templates/index.tera が解釈されます。

コンテキストには、Serde でシリアライズ可能なものを指定できます。 この例では、自前で以下のように定義しています。

#[derive(Serialize)]
struct TemplateContext {
    name: String,
    items: Vec<&'static str>
}

さて、 templates/index.tera の内容を確認していきます。

    <h1>Hi {{name}}</h1>

コンテキストとして渡した、 name が使用されています。

        {% for s in items %}
            <li>{{ s }}</li>
        {% endfor %}

ここでは items の内容をループで表示しています。


さいごに

今回は、公式のexamplesの中から基礎の基礎である hello_world 、 動的パスを扱った hello_person 、 レスポンスをリッチにする tera_template の3つを紹介しました。

計画的に執筆していなかったため、あまり紹介できませんでした。 examples は30個位上あるので、余裕のある際に追加していければと思います。