Runa: Ruby で中規模アプリケーションを書くためのフレームワーク

Runa という Ruby で Gem を使ったり複数ファイルで構成された中規模のアプリケーションを簡単に書くためのフレームワークを作っています。

Runa を作った経緯

  • Ruby は単独のスクリプトファイルとして実行するときは取り回しも簡単で大変使いやすい(小規模アプリケーション)
  • が、特定の gem に依存したり複数ファイルで構成されるようなアプリケーションを作ろうとするとスタンダードな方法が用意されておらず(特に配布や共有のことを考えると)敷居が高くなってしまう(中規模アプリケーション)
  • これが今まで余り問題にならなかったのは、Web アプリであれば Rails がその辺りも面倒をみてくれたり、コンソールアプリケーションなら gem で配布するみたいな方法でやりくりしてきた経緯がある。しかし gem で配布するには RubyGems のアカウントが必要だったり、昨今のセキュリティ問題などを考えると思いつきのアプリケーションを気軽に置くような感じではすでに無くなっている気がする(大規模アプリケーション)
  • 私が普段作りたいのは個人的な問題を思い付きで解決したちょっとしたアプリケーションであり配布はレポジトリ作ったから使いたい人はどうぞ位にしたい

そこで普段使いのアプリケーションを気軽に作れるようにするために Runa を作りました。Ruby には Bundler という優れた依存関係管理ツールがすでに存在しており、実際のところ Runa は Bundler のラストワンマイルを埋める薄いラッパーです。

Runa のインストール

gem でインストールします。runaコマンドが使えるようになるので以後はこれを使います。

$ gem install runa

runa new アプリケーション名 でプロジェクトを作成します。ディレクトリができるので下に移動します。

$ runa new hello
Created 'hello' application.
$ cd hello

Gem の追加や複数ファイルのアプリケーション作成に必要なものが自動で追加されています。

$ ls
.bundle/
.runa/
lib/
.gitignore
Gemfile
hello.rb

runa run スクリプト名で実行します。runa runで実行すると追加した Gem や lib/ 以下へのパスが通った状態で実行されます。

$ runa run .\hello.rb
Hello, world!

Launchy Gem を追加してみます。Gem の追加にはruna gem_add Gem名を使います。(bundle install した後に runa install でも OK)

$ runa gem_add launchy
.
.
Installing launchy 2.5.2
Generate '.runa/runa_load_path.rb'

これで hello.rb から require "launchy" が可能になります。

require "hello"
require "launchy"

url = "https://www.ruby-lang.org"
Launchy.open(url)
puts "Hello, #{url}"

実行するとブラウザで https://www.ruby-lang.org/ が開けるようになります。

Runa アプリケーションをデプロイする

ここまでは bundler だけでも頑張ればできると思います。

Runa を使うと作成したスクリプトを起動するための実行ファイル(シェルスクリプトWindowsならバッチファイル)を簡単に作成することができます。これが一番の特徴です。私はこの起動スクリプトを PATH の通った場所に配置しています。

実行ファイルの作成には runa deploy コマンドを使います。(ここでは ../bin にパスが通っていると仮定しています)

$ runa deploy hello.rb ../bin
Generate '.runa/runa_load_path.rb'
Deploy execution script to '../bin/hello.bat'.

デプロイしたバッチファイル(もしくはシェルスクリプト)は runa コマンドを経由せずにシェルから実行するができます。(つまり .exe と同じ)

$ ../bin/hello
$ hello                # (注) PATH が通っていれば
Hello, https://www.ruby-lang.org
(ブラウザ起動)

Win+R からも直接実行できます。

デプロイしたアプリケーションの挙動を変更する

実行用ファイルの実体は元々作った runa レポジトリなので、hello.rb を変更すればアプリケーションの挙動を変更することができます。このとき「再デプロイは不要」です。これは gem を追加したときも同様です。

例として hello アプリケーションに clipboard gem を追加して、特定のページを開きつつ URL をクリップボードにコピーしてみます。まずは clipboard gem をインストールします。

$ runa gem_add clipboard
.
.
Generate '.runa/runa_load_path.rb'

hello.rb を書き換えます。

require "hello"
require "launchy"
+ require "clipboard"

url = "https://www.ruby-lang.org"
Launchy.open(url)
puts "Hello, #{url}"
+ Clipboard.copy(url)

再デプロイは不要なのでそのままコマンドラインや Win+R から hello を実行します。

$ hello
Hello, https://www.ruby-lang.org
(ブラウザ起動&クリップボードに https://www.ruby-lang.org がコピーされる)

再ビルド不要でアプリケーションの挙動を変更することができるのはかなり便利です。

他の人の使った Runa アプリケーションを使う

Runa で作られたアプリケーションは他の人の環境でも簡単に動かすことができます。ここでは先ほど作った hello アプリケーションを動かしてみます。

$ git clone https://github.com/ongaeshi/hello.git
cd hello
$ runa install
$ runa run hello.rb

とても簡単ですね。デプロイも簡単です。

$ runa deploy hello.rb /path/to/bin

最後に gosu を使ったゲームを runa で動かして終わりにします。

$ git clone https://github.com/ongaeshi/minesweeper.git
cd minesweeper
$ runa install
$ runa run hello.rb

おわりに

私が自分用に作ったものはongaeshi/toolbox に置いてあります。src2urlとかお気に入りでよく使っています。

みなさんもお気に入りのアプリケーションを runa で作って気軽に公開してください!

Polished Ruby Programming を読んでいる(2)

前回から継続してコツコツ読んでいる。 Chapter4 から先に読んだので、そこから逆戻りして Chapter 1, 2, 3, 5 ... の順に進めている。アラカルトに気になったところをメモ。

Chapter3: 変数

  • 変数名の長さはスコープの広さと反比例させる
    • メソッドの場合は呼び出し回数と反比例させる
    • Ruby に限らずどの言語でも有用な法則
  • 定数の内部実装は変数
    • 値の変更も禁止されていない
    • 警告が出るだけ
  • クラス変数は使ってはいけない。
    • 親子関係のあるクラスやモジュール間で簡単に上書きできてしまうため
    • 代用方法1: 定数を使う(値を変更したときに警告が出るのには目をつぶる)
    • 代用方法2: クラスインスタンス変数を defined?再帰的にルックアップする方法
    • 代用方法3: クラスインスタンス変数を inherited で生成時にサブクラスにコピーする方法
    • クラス変数微妙に使いにくくていつ使うんだろう?と思っていたので「使わないほうがよい」と書いてあってすっきりした
    • 代用方法がたくさんあるのが Ruby のメタ的な柔軟性の勉強になって面白い
  • グローバル変数も使わない方がよい
    • 代用方法1: 定数+instance_eval
    • 代用方法2: シングルトンクラスで置き換えられる
    • メソッドコールのコストをどうしても払いたくない限は代用方法で書くのがよさそう
    • シングルトンクラスの方法しか知らなかったけど、instance_eval はなんか恰好よいのでどこかで使ってみたい

Chapter5: 例外

※ まだ途中

  • hash#[] のキーが見つからないとき Ruby だと nil を返して Python は例外を返す
  • それぞれ逆をするために fetch(Rubyで例外出す) と get(Pythonでデフォルト値返す) が用意されている
  • この辺り言語の特徴を表していて大変面白い

おわり

第1部までは頑張って原著で読んで、その後は研鑽Rubyプログラミングの pdf で読む予定。

ruremai gem を Ruby3 でも動くようにした

ruremai は irb から簡単に日本語リファレンスマニュアル(るりま)を開く便利ツール。

$ gem install ruremai

こんな感じで使う。

irb> require "ruremai"
irb> "".method(:concat).rurema!    # String#<< のリファレンスを開く
irb> [].mean?.join                 # Array#join のリファレンスを開く

Ruby3 で動かなかったので手元で修正して PR を出した。

https://github.com/ongaeshi/ruremai/tree/feature/support-ruby3

取り込まれれば本体 gem でも使えるようになる。 それまでは Gemfile に

gem 'ruremai', github: "ongaeshi/ruremai", branch: "feature/support-ruby3"

と書けば使える。

mermaid.js を使ったコードリーディング

mermaid.js を使ってコードリーディングをするときに便利な機能をまとめてみる。 標準でクラスダイアグラムも使えるが、色々試した結果、応用の効くグラフを使う方法に落ち着いた。

スタイル

注目させたい関数の色やアウトラインを変更できる。

graph LR
  foo --> bar --> baz

  style bar color:#000,fill:#ccc,stroke:#333,stroke-width:4px
graph LR
  foo --> bar --> baz
  style bar color:#000,fill:#ccc,stroke:#333,stroke-width:4px

サブグラフ

名前空間やファイル名を表すのに便利。

graph LR
  subgraph Foo
    f1 --> f2
  end

  f2 --> f3

  subgraph Bar
    f3 --> f4
  end
graph LR
  subgraph Foo
    f1 --> f2
  end

  f2 --> f3

  subgraph Bar
    f3 --> f4
  end

コメントアウト

%% でグラフの一部を無効にすることで1つのグラフを違う視点で表示することができる。
前述のスタイルと組み合わせるのもよい。

f1 に注目。

graph LR
  f1 --> f2
  f1 --> f3
  %% f3 --> f4 --> f5

  style f1 color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  %% style f3 color:#000,fill:#ccc,stroke:#333,stroke-width:4px
graph LR
  f1 --> f2
  f1 --> f3
  %% f3 --> f4 --> f5

  style f1 color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  %% style f3 color:#000,fill:#ccc,stroke:#333,stroke-width:4px

f3 に注目。

graph LR
  %% f1 --> f2
  f1 --> f3
  f3 --> f4 --> f5

  %% style f1 color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  style f3 color:#000,fill:#ccc,stroke:#333,stroke-width:4px
graph LR
  %% f1 --> f2
  f1 --> f3
  f3 --> f4 --> f5

  %% style f1 color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  style f3 color:#000,fill:#ccc,stroke:#333,stroke-width:4px

線にコメント

受け渡されるデータを記述できる。

graph LR
  user -- color --> makeTriangle -- graphic, pos --> drawGraphic
  makeTriangle -- graphic --> saveToPng
graph LR
  user -- color --> makeTriangle -- graphic, pos --> drawGraphic
  makeTriangle -- graphic --> saveToPng

開発環境

本家のライブプレビューを使えば大体のものは書ける。 URL に編集内容が保存されるので他の人に共有することも可能。

https://mermaid.live/

VSCode だと Markdown Preview Enhancedpng への保存もできるのでおすすめ。

実例

blogsync のコードを読むときに書いたグラフ。灰色になっているのが各コマンドに対応していてそこから各実装に分散している。設定ファイルの読み書き部分は全コマンド共通。

graph LR
  subgraph main.go
    main --> cli.NewApp --> cli.Run

    cli.Run --> commandPull
    cli.Run --> commandPush
    cli.Run --> commandPost
    cli.Run --> commandList

    commandPull --> loadConfiguration
    commandPush --> loadConfiguration
    commandPost --> loadConfiguration
    commandList --> loadConfiguration
    loadConfiguration --> loadConfigFiles --> loadSingleConfigFile    
  end

  commandPull --> newBroker --> FetchRemoteEntries --> LocalPath --> StoreFresh
  commandPush --> entryFromReader
  
  subgraph entry.go
    entryFromReader
  end

  subgraph broker.go
    newBroker
    FetchRemoteEntries
    LocalPath
    StoreFresh
  end

  style commandPull color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  style commandPush color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  style commandPost color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  style commandList color:#000,fill:#ccc,stroke:#333,stroke-width:4px
graph LR
  subgraph main.go
    main --> cli.NewApp --> cli.Run

    cli.Run --> commandPull
    cli.Run --> commandPush
    cli.Run --> commandPost
    cli.Run --> commandList

    commandPull --> loadConfiguration
    commandPush --> loadConfiguration
    commandPost --> loadConfiguration
    commandList --> loadConfiguration
    loadConfiguration --> loadConfigFiles --> loadSingleConfigFile    
  end

  commandPull --> newBroker --> FetchRemoteEntries --> LocalPath --> StoreFresh
  commandPush --> entryFromReader
  
  subgraph entry.go
    entryFromReader
  end

  subgraph broker.go
    newBroker
    FetchRemoteEntries
    LocalPath
    StoreFresh
  end

  style commandPull color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  style commandPush color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  style commandPost color:#000,fill:#ccc,stroke:#333,stroke-width:4px
  style commandList color:#000,fill:#ccc,stroke:#333,stroke-width:4px

Obsidianのアウトライナープラグインを試す

普段のメモ書きに Obsidian を愛用しているが、アウトライナーは標準で用意されていない。
※ 見出しを一覧したり移動できる「アウトライン」というコアプラグインはある(これはこれで便利)。

評判のよいコミニティプラグインOutlinerZoom を試してみることにした。

インストール

コミニティプラグインから Outliner と Zoom を検索してそれぞれインストール。

ホットキー一覧

Outliner

  • Ctrl+↑↓でリストの開閉
  • Ctrl+Shift+↑↓ でリストの移動

Zoom

  • ホットキー不要でズームしたいリストのバレットをクリックすればOK

使い方

こんな感じのリストを

こんな感じに操作できる。Ctrl+Shift+↑↓でリストを移動しながら整理。集中して考えたいときはクリックしてズーム。

blogsync で Windows でも標準入力を使えるようにした

github.com

entryFromReader()関数でエントリー時刻を取得する際にエラーが発生していた。 Windowsでは、"/dev/stdin "に対してos.Stat()を行うことができないのが原因。

Windowsで標準入力が渡されたときは、エントリーに現在時刻を設定することにして回避。

これで Windows から直接投稿できるようになった。

WSL の時間が Windows とずれている

WSL を自前でビルドして下書きも作れるようになった のだが、 今度は更新がうまくいかない問題が発生。

調べると Windows の時刻よりも 5 分位 WSL の時間が遅れていて新しい記事だと blogsync が認識できなくなっていた。

WSL2で時刻がずれる - Qiita

WSL を再起動することで解決した。(スリープとかしているとだんだんずれていくらしい・・)

> wsl --shutdown