Isomorphic web programming in Ruby

この記事は[Opal Advent Calendar 2016](http://qiita.com/advent-calendar/2016/opal)の2日目の投稿として書いています。

今日は[RubyConf Taiwan 2016](https://2016.rubyconf.tw/)で同じタイトルのトークをしてきました。台湾でのトークは英語のトークということでなかなかひどいものになってしまいました。
リベンジの意味もこめてその内容ここに書きとめておこうと思います。
基本的にはライブコーディングによるデモだったのでデモプログラムを https://github.com/youchan/menilite-todo-app に置いておきます。

Isomorphic web programming in Ruby
===

Isomorphic programmingといえばNode.jsという感じですが、RubyでもOpalを使うとIsomorphic programmingができますよという話です。
昨日の話と繋がりますが、Rubyでサーバーサイドを書けるので、OpalでIsomorphic programmingというのはとても魅力的な話です。もちろん、他のAltJSでは真似ができません。(Scala.jsとか他の言語からのトランスパイラについてはその限りではありませんが)

Menilite
===

そういうわけで、RubyでIsomorphic programmingするフレームワークMeniliteを開発しました。
MeniliteはModelをサーバーサイドとクライアントサイドで共有します。
サーバーサイドのModelはクライアントサイドから透過的に扱うことができるようになっています。
たとえば、クライアントサイドで`save`すればサーバーサイドでデータベースに保存されるという感じです。
RubyConf Taiwanのトークもそうでしたが、この稿ではMeniliteによるIsomorphic programmingについて書きます。

Demo
===

ライブコーディングにつかったデモアプリについて少し説明しておきます。
作ったのはTodoアプリですが、メインはTodoの管理ではなくユーザー管理のところでした。
Todoを選んだのは単に簡単で動的なものを表現できるからです。
ユーザー管理にフォーカスしたのは、モデルを透過的に扱えるようにするということからアクセス制御が必要になることを強調したかったからです。またサインアップやログインなどのフォームも実装するというところがデモとしてよいと思いました。

モデルの定義
===

モデルは以下のように定義します。

```ruby
class User < Menilite::Model
  field :name
  field :password

  action :signup, save: true do |password|
    self.password =  BCrypt::Password.create(password)
  end

  if_server do
    def auth(password)
      BCrypt::Password.new(self.password) == password
    end
  end
end
```

`Menilite::Model`を継承して`User`というモデルを定義しています。
`name`と`password`というフィールドを持っています。フィールドの型はデフォルトでstring型になります。
もちろん型を指定することもできます。

`action`はサーバーサイドで実行されるロジックで、クライアントサイドではUserクラスの`signup`という名前のインスタンスメソッドとして呼び出すことができます。ちょうどRPCのような感じです。
`save`オプションはこのアクションを実行した後にモデルが保存されることを表わしていて、この場合、`self`はUserモデルのインスタンスを表しています。

`if_server`はサーバーサイドでのみ実行されるブロックを定義していて、この場合、`auth`メソッドはサーバーサイドだけに定義されることを表しています。

アクション
===

先ほど定義した、`signup`アクションはクライアントサイドからはメソッドとして見えます。
このメソッドはサーバーサイドのアクションを呼びだしたあと、コールバックとして引数で渡されたブロックを実行します。

```ruby
def signup
  user = User.new(name: @refs[:name].value)
  user.signup(@refs[:password].value) do |status, res|
    if status == :success
      $window.location.assign('/')
  end
end
```

クライアントサイドからはこのように使います。`status`には`：success`や`：failure`,`:validation_error`などの結果が返ってきます。
この例では成功した場合だけルートパスにリダイレクトしています。

コントローラー
===

サインアップができたら、次はログインの機能です。
ログインの情報はセッションに保存することにします。
セッションのようなリクエストに紐付くコンテキストをモデルに持たせるのはあまり良い方法ではありません。
また、複数のモデルに横断する処理はモデルのアクションに定義するのはむずかしいでしょう。
そこで、コントローラーという機能を用意しました。
MeniliteはMVCフレームワークではありませんが、Viewに当るものがクライアントサイドのViewでModelがクライアントサイドから透過的に扱えるというところから、残るControllerに当るものが必要になると感じました。
さて、コントローラーの例を見せましょう。

```ruby
class ApplicationController < Menilite::Controller
  action :login do |username, password|
    user = User.find(name: username)
    if user && user.auth(password)
      session[:user_id] = user.id
    else
      raise Menilite::Unauthorized.new
    end
  end
end
```

コントローラーにもモデルのようにアクションを定義することができます。モデルと違うのはセッションがあつかえる点です。
セッションのほかにもリクエストのコンテキストに紐付くものを扱うことができます。(クエリパラメータなど)
Railsのアクションに似ていますね。Railsと違うのは、クライアントからはメソッドとして透過的に見えることです。
このアクションの呼びだしは次の例になります。

```ruby
def login
  name = @refs[:name].value
  password = @refs[:password].value
  ApplicationController.login(name, password) do |status, res|
    if status == :success
      $window.location.assign('/')
    end
  end
end
```

先ほどのサインアップの例とよく似ていますね。違うのはインスタンスを作らずにクラスメソッドとして呼びだしているところです。

認証
===

認証には先ほど定義した。`User#auth`メソッドを使います。このメソッドはサーバーサイドだけで呼びだせます。
APIの認証はMeniliteの大部分のアクションの前に挟み込む必要があります。
そこで、コントローラーに`before_action`という機能を用意しました。

```ruby
class ApplicationController < Menilite::Controller
  before_action(exclude: ['ApplicationController#login', 'User#signup']) do
    user = User[session[:user_id]]
    if user
      Menilite::PrivilegeService.current.privileges << UserPrivilege.new(user)
    else
      raise Menilite::Unauthorized.new
    end
  end
...
```

`exclude`パラメータで`ApplicationController#login`や`User#signup`を除外しています。
それ以外のすべてのアクションに対してこの`before_action`は実行されます。
認証が成功したときの処理についてはこのあとに説明しましょう。

アクセスコントロール
===

すでに説明しているとおり、

> モデルを透過的に扱えるようにするということからアクセス制御が必要になる

ということが言えます。つまり、すべてのユーザーがすべてのデータにアクセスできたら困るということです。
そこでデータにアクセス制限を加える方法について説明します。

まず、モデルについてです。デモのTodoアプリには`Entry`というモデルがあります。次のような感じです。

```ruby
class Entry < Menilite::Model
  field :description
  field :done, :boolean
  field :user, :reference

  permit :user_privilege
end
```

このなかに、`user`というフィールドがあります。これはActiveRecordのAssociationのようなものです。
ユーザーで制限するためにはユーザーに紐付けることが必要ということです。
`permit`というのはこのデータへのアクセスに対して特別な権限を与えるということです。
`user_privilege`は次のように定義されます。

```ruby
class UserPrivilege < Menilite::Privilege
  def key
    :user_privilege
  end

  def initialize(user)
    @user = user
  end

  def filter
    { user_id: @user.id }
  end

  def fields
    { user_id: @user.id }
  end
end
```

`key`はさきほどの`permit`の引数に渡してこのPrivilegeを識別するために使います。
`filter`はgetするときのフィルターとして働きます。
`fields`はpostするときの追加フィールドとして働きます。
つまり、postするときは持ち主であるユーザーの情報を付加して、getするときにはそのユーザーのものだけにフィルターします。

そして、認証のところで出てきた、

```ruby
Menilite::PrivilegeService.current.privileges << UserPrivilege.new(user)
```

というのが`UserPrivilege`を`PrivilegeService`に登録して、どのユーザーの権限でアクセスするかを設定しています。

まとめ
===

アドベントカレンダーということで今日中にポストしようと思い駆け足で説明してきました。
この投稿だけでは伝えきれないものがたくさんあります。1時間のトークでも説明しきれませんでした(英語だったというのもあります。)
まだはじめたばかりのプロジェクトですが、RubyでIsomorphic programmingというのは魅力的な方法論だと思います。
この投稿でその一端でも感じてもらえたなら、なんらかのリアクションをいただけるとうれしいです。