9
12

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-04-09

概要

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

ルーティング

アクションは一つで良いのでかなりシンプルです。

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

コントローラー

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

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パラメータを元に処理を実行します。

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

プロセッサー

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

# 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の中身を実装します。

# 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を返すようにしました。

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でリクエストを送ると、

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}
9
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
12