JSON-RPCのAPIサーバをRails4で作った

# 概要

JSON-RPCのAPIサーバをRails4で作ってみました。
Ruby初級者なので諸々突っ込んで頂けるとありがたいです。

コードはGithubに公開してます。
https://github.com/hidechae/jsonrpc-with-rails

# JSON-RPCの仕様
以下のページにまとまってます。
http://www.jsonrpc.org/specification

# 全体構造
lib以下にRPCのディスパッチャーを、app/processor以下にRPCから呼び出す実処理を配置しました。

```
.
|-- app
|   |-- controllers
|   |   |-- application_controller.rb
|   |   `-- rpc_controller.rb
|   |-- processor
|   |   |-- application_processor.rb
|   |   `-- user
|   |       `-- get_processor.rb
|   `-- views
|       `-- rpc
|           `-- execute.json.jbuilder
`-- lib
    `-- rpc
        |-- dispatcher.rb
        `-- error
            |-- base.rb
            |-- internal_error.rb
            |-- invalid_params.rb
            |-- invalid_request.rb
            |-- method_not_found.rb
            `-- parse_error.rb
```

# ルーティング
アクションは一つで良いのでかなりシンプルです。

```rb
Rails.application.routes.draw do
  root :to => 'rpc#execute', :via => :post, :format => :json
end
```

# コントローラー
コントローラーでは、リクエストをディスパッチャ-に渡してるだけです。

```rb
class RpcController < ApplicationController
  def execute
    dispatcher = Rpc::Dispatcher.new payload
    @response = dispatcher.execute
  end

  private

  def payload
    params ? JSON.parse(params[:payload]).deep_symbolize_keys : nil
  end
end
```

# ディスパッチャー
リクエストパラメータのバリデーション、methodパラメータを元に処理を実行します。

```rb
class Rpc::Dispatcher
  def initialize(request)
    @id      = request.delete(:id)
    @jsonrpc = request.delete(:jsonrpc)
    @method  = request.delete(:method)
    @params  = request.delete(:params)
    @result  = {}
    @error   = 0
  end

  def execute
    begin
      fail Rpc::Error::InvalidRequest.new unless valid?
      @result = Object.const_get("#{@method.camelize}Processor").new(@params).execute
    rescue Rpc::Error::Base => e
      @error = error e
    rescue => e
      @error = error Rpc::Error::InternalError.new(message: e.message)
    end

    response
  end

  private

  def valid?
    return false if @jsonrpc != '2.0'
    return false if @method.blank?
    true
  end

  def response
    {
      id:      @id,
      jsonrpc: @jsonrpc,
      result:  @result,
      error:   @error
    }
  end

  def error(e)
    {
      code:    e.code,
      message: e.message,
      data:    e.data
    }
  end
end
```

# プロセッサー
ベースクラスは以下のようになっています。引数設定のバリデーションを行っています。

```rb
# application_processor.rb
class ApplicationProcessor
  def input_setting
    fail 'method missing'
  end

  def initialize(params)
    @params = filter params
    fail Rpc::Error::InvalidParams.new(
      message: 'invalid parameter',
      data: { input_setting: input_setting, params: params }
    ) unless valid?
  end

  def execute
    fail 'method missing'
  end

  private

  def filter(params)
    filtered_parameters = {}
    input_setting.each_key do |key|
      filtered_parameters.store(key, params[key]) if params.key?(key)
    end
    filtered_parameters
  end

  def valid?
    input_setting.each do |key, setting|
      return false unless @params.key?(key) if setting[:required]
    end
    true
  end
end
```

以下はサンプルです。ベースクラスを継承してinput_settingとexecuteの中身を実装します。

```rb
# user/get_processor.rb
class User::GetProcessor < ApplicationProcessor
  def input_setting
    {
      id: { required: true }
    }
  end

  def execute
    user_data.each do |user|
      return user if user[:id] == @params[:id]
    end
    nil
  end

  private

  def user_data
    [
      {
        id:   1,
        name: 'user1'
      },
      {
        id:   2,
        name: 'user2'
      }
    ]
  end
end
```

# レスポンス
レスポンスはjbuilderを使ってjsonを返すようにしました。

```rb
json.extract! @response, :id, :jsonrpc

if @response[:error] == 0
  json.extract! @response, :result, :error
else
  json.error do
    json.extract! @response[:error], :code, :message, :data
  end
end
```

# 動作確認
以下のようにPOSTでリクエストを送ると、

```rb
require 'net/http'
require 'uri'
require 'json'
require 'pp'

URL = 'http://localhost:3000'
uri = URI.parse(URL)

request = Net::HTTP::Post.new(uri.request_uri)
json = {
  id:       '12345',
  jsonrpc:  '2.0',
  method:   'user/get',
  params:   { id: 1 }
}.to_json
request.body = 'payload=' + json

http = Net::HTTP.new(uri.host, uri.port)

http.start do |h|
  response = h.request(request)
  pp JSON.parse(response.body)
end
```

以下の様なレスポンスが返ってきます。

```
{"id"=>"12345",
 "jsonrpc"=>"2.0",
 "result"=>{"id"=>1, "name"=>"user1"},
 "error"=>0}
```