Zengin Code Search API on Rails

:bank: サービスで利用する金融機関コードのAPIを公開するには [全国銀行協会](http://www.zenginkyo.or.jp/) から [金融機関店舗情報](https://www.jba-shuppancenter.jp/content/cd/tenpoCD1.html) を購入するのでしょうが、まず入手までに時間がかかります。そして入手できても厳しいハードルがあって、メディアが :cd: **CD-ROM** で届きます。さらに動作OSが **Windows(日本語)** という！もちろんファイルのコードは **シフトJIS** です。

そんなわけでOSSの金融機関コードを使ってプロトタイプを作ります。全銀協の注文フォームを入力する時間より早くAPIが完成するでしょう。
[ZenginCode source data](https://github.com/zengin-code/source-data) は **JavaScript** や **Python** にも対応していますが [Ruby on Rails](http://rubyonrails.org/) で作成しました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8369/09d79ddb-d079-a8f0-9d09-ae8e932355b8.png)

:octocat: [GitHub](https://github.com/ogom/rails-zengin-code-search-api) に今回のコードを公開しています。

## Tutorial

### Rails New

* Ruby on Rails は `--api` のオプションで簡単に **REST API** を作成することができます。

```
$ rails new zengin-code-search --api
$ cd zengin-code-search/
```

### Generate Scaffold

* モデルは一つの金融機関に複数の店舗が属する関係なので `bank has many branches` を構築します。
* 名前には漢字と片仮名さらに平仮名とローマ字があるので、それぞれに項目を作成します。

```
$ rails generate scaffold bank code name name_kana name_hira name_en
$ rails generate scaffold branch code name name_kana name_hira name_en bank:references
```

:memo: `Kana` や `Hira` は [ISO 15924](http://unicode.org/iso15924/iso15924-codes.html) で分類されているコードで、ちなみに漢字は `Hani` です。

### Rails Database Migration

* `code` に `UNIQUE制約` を設定します。他に `NOT NULL制約` などデータベースの設定をします。

```ruby:db/migrate/20171201132032_create_banks.rb
class CreateBanks < ActiveRecord::Migration[5.1]
  def change
    create_table :banks do |t|
      t.string :code, null: false, default: '', index: { unique: true }
      t.string :name, null: false, default: ''
      t.string :name_kana, null: false, default: ''
      t.string :name_hira, null: false, default: ''
      t.string :name_en, null: false, default: ''

      t.timestamps
    end
  end
end
```

* `bank_id` と `code` に `UNIQUE制約` を設定します。他に `NOT NULL制約` などデータベースの設定をします。（大事なことなので）

```ruby:db/migrate/20171201132039_create_branches.rb
class CreateBranches < ActiveRecord::Migration[5.1]
  def change
    create_table :branches do |t|
      t.string :code, null: false, default: ''
      t.string :name, null: false, default: ''
      t.string :name_kana, null: false, default: ''
      t.string :name_hira, null: false, default: ''
      t.string :name_en, null: false, default: ''
      t.references :bank, foreign_key: true

      t.timestamps
    end
    add_index :branches, [:bank_id, :code], unique: true
  end
end
```

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

```
$ rails db:migrate
```

### Nested Resources

* [ネストしたリソース](https://railsguides.jp/routing.html#%E3%83%8D%E3%82%B9%E3%83%88%E3%81%97%E3%81%9F%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9)のパターンはよく使います。ですが `rails generate` コマンドで作成したコードを少し編集する必要があります。

#### Rails Models

* `has_many :branches` は金融機関が複数の店舗を持つことを示します。
* `dependent: :destroy` は金融機関が削除されると店舗も削除されます。

```ruby:app/models/bank.rb
class Bank < ApplicationRecord
  has_many :branches, dependent: :destroy
end
```

* `belongs_to :bank` は店舗が金融機関に属することを示します。

```ruby:app/models/branch.rb
class Branch < ApplicationRecord
  belongs_to :bank
end
```

#### Rails Controllers

* `POST:create`, `GET:index`, `GET:show`, `PATCH/PUT:update`, `DELETE:destroy` のHTTPとRubyのメソッドで **CRUD** に対応しています。

```ruby:app/controllers/banks_controller.rb
class BanksController < ApplicationController
  before_action :set_bank, only: [:show, :update, :destroy]

  # GET /banks
  def index
    @banks = Bank.all

    render json: @banks
  end

  # GET /banks/1
  def show
    render json: @bank
  end

  # POST /banks
  def create
    @bank = Bank.new(bank_params)

    if @bank.save
      render json: @bank, status: :created, location: @bank
    else
      render json: @bank.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /banks/1
  def update
    if @bank.update(bank_params)
      render json: @bank
    else
      render json: @bank.errors, status: :unprocessable_entity
    end
  end

  # DELETE /banks/1
  def destroy
    @bank.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def bank_params
      params.require(:bank).permit(:code, :name, :name_kana, :name_hira, :name_en)
    end
end
```

* 入れ子にするには `Branch` クラスを `@bank.branches` に変更します。

```ruby:app/controllers/branches_controller.rb
class BranchesController < ApplicationController
  before_action :set_bank
  before_action :set_branch, only: [:show, :update, :destroy]

  # GET /branches
  def index
    @branches = @bank.branches

    render json: @branches
  end

  # GET /branches/1
  def show
    render json: @branch
  end

  # POST /branches
  def create
    @branch = @bank.branches.new(branch_params)

    if @branch.save
      render json: @branch, status: :created, location: [@bank, @branch]
    else
      render json: @branch.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /branches/1
  def update
    if @branch.update(branch_params)
      render json: @branch
    else
      render json: @branch.errors, status: :unprocessable_entity
    end
  end

  # DELETE /branches/1
  def destroy
    @branch.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find(params[:bank_id])
    end

    def set_branch
      @branch = Branch.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def branch_params
      params.require(:branch).permit(:code, :name, :name_kana, :name_hira, :name_en, :bank_id)
    end
end
```

#### Rails Routes

* `resources` のルーティングを `do` と `end` に記述するこで入れ子にします。

```ruby:config/routes.rb
Rails.application.routes.draw do
  resources :banks do
    resources :branches
  end
end
```

#### Rails Server

* `rails server` コマンドで起動して **URL** にアクセスします。

```
$ rails server
$ open http://localhost:3000/banks
```

:memo: APIを作るだけなら数分で完成します。

### Rails Database Seed

* 金融コードは [ZenginCode](https://github.com/zengin-code/zengin-rb) のライブラリを使ってデータベースに登録します。
* `Gemfile` ファイルに `require: false` を記述すると明示的に `require` を記述しないとロードされません。

```ruby:Gemfile
gem 'zengin_code', require: false
```

* `zengin_code` を使うのは **Seed** にのみになので `require` をこちら記述します。

```ruby:db/seeds.rb
require 'zengin_code'

ZenginCode::Bank.all.each do |original_code, original_bank|
  puts "== #{original_code}:#{original_bank.name}"
  bank = Bank.find_or_initialize_by(code: original_code)
  bank.name = original_bank.name
  bank.name_kana = original_bank.kana
  bank.name_hira = original_bank.hira
  bank.name_en = original_bank.roma
  bank.touch unless bank.new_record?
  bank.save!

  original_bank.branches.each do |original_code, original_branch|
    puts "-- #{bank.code}:#{bank.name} #{original_code}:#{original_branch.name}"
    branch = bank.branches.find_or_initialize_by(code: original_code)
    branch.name = original_branch.name
    branch.name_kana = original_branch.kana
    branch.name_hira = original_branch.hira
    branch.name_en = original_branch.roma
    branch.touch unless branch.new_record?
    branch.save!
  end
end

puts "Bank: #{Bank.count}, Branch: #{Branch.count}"
```

* `rails db:seed` コマンドをで金融コードを登録します。

```
$ rails db:seed
Bank: 1345, Branch: 31169
```

:memo: 数分かかりますがサンプルデータが作成されます。

#### Rails Server

* 入れ子で **URL** にアクセスすることができます。

```
$ rails server
$ open http://localhost:3000/banks/1
$ open http://localhost:3000/banks/1/branches/1
```


### Overriding Named Route Parameters

* [名前付きルーティングのパラメータをオーバーライドする](https://railsguides.jp/routing.html#%E5%90%8D%E5%89%8D%E4%BB%98%E3%81%8D%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%AE%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%82%92%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%A9%E3%82%A4%E3%83%89%E3%81%99%E3%82%8B)ことでデフォルトのリソース識別子(`:id`)を変更します。

#### Rails Models

* `to_param` メソッドに変更する項目を記述します。

```ruby:app/models/bank.rb
class Bank < ApplicationRecord
  has_many :branches, dependent: :destroy

  def to_param
    code
  end
end
```

```ruby:app/models/branch.rb
class Branch < ApplicationRecord
  belongs_to :bank

  def to_param
    code
  end
end
```

#### Rails Controllers

* `find(params[:id])` メソッドを `find_by(code: params[:code])` メソッドに変更します。

```ruby:app/controllers/banks_controller.rb
class BanksController < ApplicationController
  before_action :set_bank, only: [:show, :update, :destroy]

  # GET /banks
  def index
    @banks = Bank.all

    render json: @banks
  end

  # GET /banks/:code
  def show
    render json: @bank
  end

  # POST /banks
  def create
    @bank = Bank.new(bank_params)

    if @bank.save
      render json: @bank, status: :created, location: @bank
    else
      render json: @bank.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /banks/:code
  def update
    if @bank.update(bank_params)
      render json: @bank
    else
      render json: @bank.errors, status: :unprocessable_entity
    end
  end

  # DELETE /banks/:code
  def destroy
    @bank.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find_by(code: params[:code])
    end

    # Only allow a trusted parameter "white list" through.
    def bank_params
      params.require(:bank).permit(:code, :name, :name_kana, :name_hira, :name_en)
    end
end
```

```ruby:app/controllers/branches_controller.rb
class BranchesController < ApplicationController
  before_action :set_bank
  before_action :set_branch, only: [:show, :update, :destroy]

  # GET /banks/:bank_code/branches
  def index
    @branches = @bank.branches

    render json: @branches
  end

  # GET /banks/:bank_code/branches/:code
  def show
    render json: @branch
  end

  # POST /banks/:bank_code/branches
  def create
    @branch = @bank.branches.new(branch_params)

    if @branch.save
      render json: @branch, status: :created, location: [@bank, @branch]
    else
      render json: @branch.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /banks/:bank_code/branches/:code
  def update
    if @branch.update(branch_params)
      render json: @branch
    else
      render json: @branch.errors, status: :unprocessable_entity
    end
  end

  # DELETE /banks/:bank_code/branches/:code
  def destroy
    @branch.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find_by(code: params[:bank_code])
    end

    def set_branch
      @branch = @bank.branches.find_by(code: params[:code])
    end

    # Only allow a trusted parameter "white list" through.
    def branch_params
      params.require(:branch).permit(:code, :name, :name_kana, :name_hira, :name_en, :bank_id)
    end
end
```

#### Rails Routes

* `param` オプションに変更する項目を記述します。

```ruby:config/routes.rb
Rails.application.routes.draw do
  resources :banks, param: :code do
    resources :branches, param: :code
  end
end
```

#### Rails Server

* 金融コードで **URL** にアクセスすることができます。

```
$ open http://localhost:3000/banks/0001
$ open http://localhost:3000/banks/0001/branches/001
```

:memo: [FriendlyId](https://github.com/norman/friendly_id) も同様の機能のライブラリです。

### Search API

* 検索機能には [SearchCop](https://github.com/mrkamel/search_cop) のライブラリを使います。

```ruby:Gemfile
# https://github.com/mrkamel/search_cop
gem 'search_cop'
```

#### Rails Models

* `search_scope` のオプションの `left_wildcard: false` で前方一致検索ができます。

```ruby:app/models/bank.rb
class Bank < ApplicationRecord
  include SearchCop

  search_scope :search do
    attributes names: [:name_kana, :name_hira, :name_en]
    options :names, left_wildcard: false
  end

  has_many :branches, dependent: :destroy

  def to_param
    code
  end
end
```

```ruby:app/models/branch.rb
class Branch < ApplicationRecord
  include SearchCop

  search_scope :search do
    attributes names: [:name_kana, :name_hira, :name_en]
    options :names, left_wildcard: false
  end

  belongs_to :bank

  def to_param
    code
  end
end
```

:memo: 片仮名と平仮名とローマ字を検索の対象に設定しています。

#### Rails Controllers

* 検索のコントローラを作成します。

```
$ rails generate controller search/banks index
$ rails generate controller search/branches index
```

`search` メソッドで前方一致検索ができます。

```ruby:app/controllers/search/banks_controller.rb
class Search::BanksController < ApplicationController

  # GET /search/banks
  def index
    @banks = Bank.search(params.require(:query))

    render json: @banks
  end
end
```

```ruby:app/controllers/search/branches_controller.rb
class Search::BranchesController < ApplicationController
  before_action :set_bank

  # GET /search/banks/:bank_code/branches
  def index
    @branches = @bank.branches.search(params.require(:query))

    render json: @branches
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_bank
      @bank = Bank.find_by(code: params[:bank_code])
    end
end
```

:bulb: `params.require(:query)` で `query` パラメータを必須にすることができます。

#### Rails Routes

* 検索のルーティングを記述します。

```ruby:config/routes.rb
Rails.application.routes.draw do
  namespace :search do
    resources :banks, param: :code, only: [:index] do
      resources :branches, only: [:index]
    end
  end

  resources :banks, param: :code do
    resources :branches, param: :code
  end
end
```

#### Rails Server

* 入れ子で検索の **URL** にアクセスすることができます。

```
open http://localhost:3000/search/banks?query=mizu
open http://localhost:3000/search/banks/0001/branches?query=hon
```

### Model Serializer

* レスポンスの項目に `id` やタイムスタンプが含まれるので [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers) のライブラリで必要な項目のみを設定します。

```ruby:Gemfile
# https://github.com/rails-api/active_model_serializers
gem 'active_model_serializers'
```

```
$ rails generate serializer bank
$ rails generate serializer branch
```

```ruby:app/serializers/bank_serializer.rb
class BankSerializer < ActiveModel::Serializer
  attributes :code, :name, :name_kana, :name_hira, :name_en
end
```

```ruby:app/serializers/branch_serializer.rb
class BranchSerializer < ActiveModel::Serializer
  attributes :code, :name, :name_kana, :name_hira, :name_en
end
```

## Tips

OSSのライブラリだけで簡単に金融コードのAPIができましたね。フロントエンドは [React](https://reactjs.org/) でインクリメンタルサーチができるコンポーネントを作るところまで書くと長くなるのでまたの機会に公開します。この記事の作成中に [Rails ガイド(日本語)](https://railsguides.jp/) に [Overriding Named Route Parameters](http://guides.rubyonrails.org/routing.html#overriding-named-route-parameters) の翻訳がないのに気がついて [Pull Request](https://github.com/yasslab/railsguides.jp/pull/554) をしました。こんな感じにOSS開発に参加したいなと思ったら [OSS Gate](https://oss-gate.github.io/) のワークショップに参加してみましょう。