概要
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}