Rails の 有限オートマトン


[RailsはActiveRecord](http://qiita.com/ogomr/items/1c5844b82daebd0580c2)でデータを永続化します。そのデータが状態を保持するならオートマトンが有効です。
オートマトンのライブラリはいくつかありますが、シンプルな[StatefulEnum](https://github.com/amatsuda/stateful_enum)を紹介します。

## environment

* Ruby: 2.3.3
* Rails: 5.0.1
* Node.js: 6.9.2

## finite automaton

有限オートマトンは有限状態機械(finite state machine)とも呼ばれ、有限個の *状態(state)* と *遷移の規則(rule)* と *入力(input)* を組み合わせたモデルです。状態遷移が理解しやすく、また推測しやすくなります。

* 状態1で文字aを読むと、状態2に移動する。
* 状態2で文字aを読むと、状態1に移動する。

![](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/ruby/stateful_enum/finite_automaton.png)

> [アンダースタンディング コンピュテーション](https://www.oreilly.co.jp/books/9784873116976/)より

## rails new

まず、[Rails](http://rubyonrails.org/)でデータベースを作成します。

* `rails generate model` コマンドで *user* を作成します。

```bash
$ rails new rails_stateful_enum
$ cd rails_stateful_enum/
$ rails generate model user name status:integer --no-timestamps
```

* `create_table` の `status` に `default: 0` を設定します。

```ruby:db/migrate/[datetime]_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :name
      t.integer :status, default: 0
    end
  end
end
```

* `rails db:migrate` コマンドでデータベースを作成します。

```bash
$ rails db:migrate
```

## enum

次は、[ActiveRecord::Enum](http://api.rubyonrails.org/classes/ActiveRecord/Enum.html)のおさらいです。

* ActiveRecord で列挙型を利用することができます。
* `enum` に *属性の名前* と *属性の値* を設定します。

```ruby:app/models/user.rb
class User < ApplicationRecord
  enum status: {active: 0, inactive: 1, closed: 2}
end
```

* インスタンスメソッドに **属性の値の確認** や **属性の値の更新** が追加されます。

```bash
$ rails console
> user = User.create
# INSERT INTO "users" DEFAULT VALUES
=> #<User id: 1, name: nil, status: "active">

> user.status
=> "active"

# 属性の値の確認
> user.active?
=> true

# 属性の値の更新
> user.inactive!
# UPDATE "users" SET "status" = ? WHERE "users"."id" = ?  [["status", 1], ["id", 1]]
=> true

> user.status
=> "inactive"
```

* クラスメソッドに **属性の値の一覧** や **属性の値のスコープ** が追加されます。

```bash
$ rails console

# 属性の値の一覧
> User.statuses
=> {"active"=>0, "inactive"=>1, "closed"=>2}

# 属性の値のスコープ
> User.active
# SELECT "users".* FROM "users" WHERE "users"."state" = ?  [["state", 0]]
```

## stateful enum

そして、StatefulEnum でオートマトンを実装します。

* `bundle` コマンドで `stateful_enum` を追加します。
	* `graphviz` は図形の出力に利用します。

```ruby:Gemfile
gem 'stateful_enum'
gem 'ruby-graphviz'
```

```bash
$ bundle
```

* `enum` にブロックで *イベント(event)* と *遷移(transition)* を設定します。
	* *属性の値* がオートマトンの *状態(state)*
	* *イベント(event)* がオートマトンの *入力(input)*
	* *遷移(transition)* オートマトンの *規則(rule)*

```ruby:app/models/user.rb
class User < ApplicationRecord
  enum status: {active: 0, inactive: 1, closed: 2} do
    event :disable do
      transition :active => :inactive
    end

    event :enable do
      transition :inactive => :active
    end

    event :close do
      transition [:active, :inactive] => :closed
    end
  end
end
```

* インスタンスメソッドに イベントの **属性の値の更新** が追加されます。
* インスタンスメソッドに **イベントの可能を確認** や **イベントの遷移後の値** が追加されます。

```bash
$ rails console
> user = User.create

# 属性の値の更新
> user.disable
# UPDATE "users" SET "status" = ? WHERE "users"."id" = ?  [["status", 1], ["id", 2]]
=> true

> user.status
=> "inactive"

# イベントの可能を確認
> user.can_enable?
=> true

# イベントの遷移後の値
> user.enable_transition
=> :active
```

### diagram

* 状態遷移図を出力したり、PlantUMLのテキストを出力することができます。

```bash
$ mkdir -p doc/diagrams
$ DEST_DIR=doc/diagrams bin/rails generate stateful_enum:graph user
$ DEST_DIR=doc/diagrams bin/rails generate stateful_enum:plantuml user
```

* `stateful_enum:graph` は `doc/diagrams/User.png` に出力されます。

![graphviz.png](https://dl.dropboxusercontent.com/u/14690051/images/notes/lang/ruby/stateful_enum/graphviz.png)


* `stateful_enum:plantuml` は `doc/diagrams/User.puml` に出力されます。

```doc/diagrams/User.puml
[*] --> active
active --> inactive :disable
inactive --> active :enable
active --> closed :close
inactive --> closed :close
closed --> [*]
```

### gulp

状態遷移図は [gulp](http://gulpjs.com/) で自動化にすると捗ります。

```bash
$ yarn init --yes
```

* `scripts` の `start` に `gulp` を設定します。

```javascript:package.json
{
  "name": "rails_stateful_enum",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "gulp"
  }
}
```

```bash
$ yarn add --dev gulp gulp-watch
$ touch gulpfile.js
```

* [gulp-watch](https://github.com/floatdrop/gulp-watch) は新規ファイルも監視対象なので便利です。

```javascript:gulpfile.js
const gulp = require('gulp')
const gutil = require('gulp-util')
const watch = require('gulp-watch')
const spawn = require('child_process').spawn

gulp.task('watch', () => {
  watch('app/models/**/*.rb', (event) => {
    gutil.log(event.path.replace(event.cwd, 'watching: '))
    spawn('DEST_DIR=doc/diagrams bin/rails generate stateful_enum:plantuml', [event.stem], {shell:true})
  })

  watch('doc/**/*.puml', (event) => {
    console.log(event.contents.toString())
  })
})

gulp.task('default', ['watch'])
```

* `yarn start` コマンドで状態遷移図が自動で出力されるようになりました。 

```bash
$ yarn start
```

* Atom は [PlantUML Viewer](https://atom.io/packages/plantuml-viewer) で PlantUML のテキストを状態遷移図に出力することができます。
  * gulp と組み合わせて、コードを修正すると状態遷移図が変更されるようになりました。

[![https://gyazo.com/b4114b9b70bd24e9e458ed5373bb22b7](https://i.gyazo.com/b4114b9b70bd24e9e458ed5373bb22b7.gif)](https://gyazo.com/b4114b9b70bd24e9e458ed5373bb22b7)

## draw uml

* Rails でも [PlantUML Viewer](http://ogom.github.io/draw_uml/) で PlantUML を表示することができます。 

```ruby:Gemfile
gem 'draw_uml'
```

* `routes` に `Engine` をマウントします。

```ruby:config/routes.rb
Rails.application.routes.draw do
  mount DrawUml::Engine, at: '/rails/draw/uml' if Rails.env.development?
end
```

```bash
$ bundle
$ rails server
```

* http://localhost:3000/rails/draw/uml/User にアクセスすると状態遷移図が表示されます。

###  browser sync

* gulp で [browser-sync](https://browsersync.io/) を利用すると、状態遷移図が自動で表示することができます。

```bash
$ yarn add --dev browser-sync
```

* `localhost:3000` を `proxy` に設定します。

```javascript:gulpfile.js
const gulp = require('gulp')
const gutil = require('gulp-util')
const watch = require('gulp-watch')
const spawn = require('child_process').spawn
const browserSync = require('browser-sync')

gulp.task('browser-sync', () => {
  return browserSync.init(null, {
    proxy: 'localhost:3000',
    reloadDelay: 1000
  })
})

gulp.task('watch', () => {
  watch('app/models/**/*.rb', (event) => {
    gutil.log(event.path.replace(event.cwd, 'watching: '))
    spawn('DEST_DIR=doc/diagrams bin/rails generate stateful_enum:plantuml', [event.stem], {shell:true})
    browserSync.reload()
  })

  watch('doc/**/*.puml', (event) => {
    console.log(event.contents.toString())
  })
})

gulp.task('default', ['browser-sync', 'watch'])
```

* `yarn start` コマンドで状態遷移図が自動で出力されるようになりました。 
	* http://localhost:3001/rails/draw/uml/User
	
```bash
$ yarn start
```

自動化は楽しくて楽ですね。:blush: 

# Tips

## draw smd

[DrawUML](http://ogom.github.io/draw_uml/) 以外に [DrawERD](http://ogom.github.io/draw_erd/) と [DrawSMD](http://ogom.github.io/draw_smd/) を利用すると Rails で ER図 や 状態遷移図 を出力することができます。

```ruby:Gemfile
gem 'draw_erd'
gem 'draw_smd'
gem 'state_machine'
```

* `routes` に `Engine` をマウントします。

```ruby:config/routes.rb
Rails.application.routes.draw do
  mount DrawUml::Engine, at: '/rails/draw/uml' if Rails.env.development?
  mount DrawErd::Engine, at: '/rails/draw/erd' if Rails.env.development?
  mount DrawSmd::Engine, at: '/rails/draw/smd' if Rails.env.development?
end
```

```bash
$ bundle
$ rails server
```

* http://localhost:3001/rails/draw/erd にアクセスするとER図が表示されます。
* http://localhost:3001/rails/draw/smd にアクセスすると状態遷移図が表示されます。

## state machine

* `rails generate model` コマンドで *group* を作成します。

```bash
$ rails generate model group name state
$ rails generate model member user:references group:references
$ rails db:migrate
```

* [state_machine](https://github.com/pluginaweek/state_machine) のブロックは [StatefulEnum](https://github.com/amatsuda/stateful_enum) と同じです。

```ruby:app/models/group.rb
class Group < ApplicationRecord
  has_many :users, through: :members
  has_many :members

  state_machine :state, initial: :active do
    event :disable do
      transition :active => :inactive
    end

    event :enable do
      transition :inactive => :active
    end

    event :close do
      transition [:active, :inactive] => :closed
    end
  end
end
```

*enjoy automaton*