Ruby

開発用なんちゃってWeb APIサーバーを作ってみた

仕事で、WebAPIで取得した結果を処理するとか、自分自身が処理した結果をWebAPIに返すとか仕事でよくあります。

ですが、開発中に、そのWebAPIサーバーが準備できているとは限らないので、ダミーのWebAPIサーバーが欲しくなってきました。

ということでなんちゃってWebAPIサーバーを作ってみます。


要件


  • リクエストがあったときに、リクエストのパス、HTTPメソッド、パラメータの情報などが表示されること

  • リクエストがあったときに、リクエストに応じた結果をJSON形式で返却できること。

  • 返す結果のJSON形式を自分で設定できること。

  • リクエストに対する処理は何もしない。


方針

Ruby なら、Rails とか Sinatra とか色々あるんでしょうが、gemは極力使いたくないです。

標準のRubyのライブラリの範囲で動く手軽な感じのものにしたい。。と言うわけで、WEBrick を使います。

車輪の再発明上等。だってコードを書く方が楽しいから。


超最低限の何もしないWeb APIサーバ

とりあえず、なんちゃってWebAPIサーバーなので、ファイル名は、 ueb_api.rb としておきます。

デフォルトではローカルホストにbindして、デフォルトポートは、18080にしておきます。

要求が来ても何もしません。


ueb_api.rb

require 'webrick'

srv = WEBrick::HTTPServer.new(
BindAddress: '127.0.0.1',
Port: 18_080
)

srv.mount_proc('/') do |req, res|
# 何もしない
end

srv.start


実行してみます。


ueb_api側

$ ruby ueb_api.rb

[2018-07-01 09:03:22] INFO WEBrick 1.4.2
[2018-07-01 09:03:22] INFO ruby 2.6.0 (2018-06-30) [x86_64-linux]
[2018-07-01 09:03:22] INFO WEBrick::HTTPServer#start: pid=27167 port=18080

別コンソールからアクセスしてみます。


別コンソール

$ curl http://localhost:18080/foo/bar/baz


ueb_api側で要求をちゃんと返しているみたいです。


ueb_api側

127.0.0.1 - - [01/Jul/2018:09:06:48 JST] "GET /foo/bar/baz HTTP/1.1" 200 0

- -> /foo/bar/baz


Ctrl+Cで穏やかに停止する

Ctrl+Cで、ueb_api.rb を停止するとこんなエラーになります。


ueb_api側

- -> /foo/bar/baz

^C[2018-07-01 09:12:08] FATAL Interrupt:
/home/suke/.anyenv/envs/rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/webrick/server.rb:170:in `select'
/home/suke/.anyenv/envs/rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/webrick/server.rb:170:in `block in start'
/home/suke/.anyenv/envs/rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/webrick/server.rb:32:in `start'
/home/suke/.anyenv/envs/rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/webrick/server.rb:157:in `start'
ueb_api.rb:12:in `<main>'
[2018-07-01 09:12:08] INFO going to shutdown ...
[2018-07-01 09:12:08] INFO WEBrick::HTTPServer#start done.
Traceback (most recent call last):
4: from ueb_api.rb:12:in `<main>'
3: from /home/suke/.anyenv/envs/rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/webrick/server.rb:157:in `start'
2: from /home/suke/.anyenv/envs/rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/webrick/server.rb:32:in `start'
1: from /home/suke/.anyenv/envs/rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/webrick/server.rb:170:in `block in start'
/home/suke/.anyenv/envs/rbenv/versions/2.6.0-dev/lib/ruby/2.6.0/webrick/server.rb:170:in `select': Interrupt

こういうの見るとなんか落ち着かないので、穏やかに停止するように trap します。


ueb_api.rb

trap('INT'){ srv.shutdown }

srv.start

起動して、すぐに、Ctrl+cで停止します。

$ ruby ueb_api.rb

[2018-07-01 09:17:36] INFO WEBrick 1.4.2
[2018-07-01 09:17:36] INFO ruby 2.6.0 (2018-06-30) [x86_64-linux]
[2018-07-01 09:17:36] INFO WEBrick::HTTPServer#start: pid=31452 port=18080
^C[2018-07-01 09:17:37] INFO going to shutdown ...
[2018-07-01 09:17:37] INFO WEBrick::HTTPServer#start done.

これで穏やかになりました。


リクエストの情報を表示する

このままだと、POSTした時のパラメータとか表示されないので、表示されるように修正します。


ueb_api.rb

srv.mount_proc('/') do |req, res|

info = "method=#{req.request_method}, uri=#{req.request_uri}, query=#{req.query}, body=#{req.body}"
srv.logger.info(info)

body を表示させるのはちょっと乱暴かもですが、そこは、「なんちゃって」なので...


空のJSONを返すようにする

空のJSONを返すようにします。


ueb_api.rb

require 'json'

...
srv.mount_proc('/') do |req, res|
res.body = {}.to_json


コード全体

コードは全体でこんな感じになりました。


ueb_api.rb

require 'webrick'

require 'json'

srv = WEBrick::HTTPServer.new(
BindAddress: '127.0.0.1',
Port: 18_080
)

srv.mount_proc('/') do |req, res|
info = "method=#{req.request_method}, uri=#{req.request_uri}, query=#{req.query}, body=#{req.body}"
srv.logger.info(info)
res.body = {}.to_json
end

trap('INT') { srv.shutdown }
srv.start



リファクタリングする

class を定義してオブジェクト指向っぽくリファクタリングしてみます。

コンストラクタの引数はポートとバインドアドレスにします。

パラメータの情報を出力する処理や、mount_procの処理は、private メソッドで行うようにします。


ueb_api.rb

require 'webrick'

require 'json'

class UebAPI
def initialize(bind_address: '127.0.0.1', port: 18_080)
@srv = WEBrick::HTTPServer.new(
BindAddress: bind_address,
Port: port
)
default_mount_proc
trap('INT') { @srv.shutdown }
end

def start
@srv.start
end

private

def default_mount_proc
@srv.mount_proc('/') do |req, res|
log_request(req)
res.body = {}.to_json
end
end

def log_request(req)
info = "method=#{req.request_method}, uri=#{req.request_uri}, query=#{req.query}, body=#{req.body}"
@srv.logger.info(info)
end
end

srv = UebAPI.new
srv.start



リクエストに応じて返す結果を設定できるようにする

ここがちょっと考えどころです。UebAPIを使う側でリクエストに応じて自由に結果を設定できるようにしておきたいです。

UebAPIクラスにどんなメソッドを追加してあげると良いか考えます。

ある程度、WEBRick に倣うことにしました。


ueb_api.rb

srv = UebAPI.new

srv.mount_proc(
'/for/bar',
method: :GET,
body: {bar:1}.to_json, # 結果
status: 200 # 返すHTTPステータス
)

とやれば、 /foo/bar に GET で呼び出せば、HTTPステータス 200 で、json を返す設定ができるようにします。

POST で呼び出すときは、次のように書けば、HTTPステータス 201 を返す設定ができるようにします。


ueb_api.rb

srv = UebAPI.new

srv.mount_proc(
'/for/bar',
method: :POST,
body: {result: :ok}.to_json,
status: 201
)

パラメータに応じて、結果を変更できた方が便利かも知れません。ということで、以下のような書き方もできるようにします。


ueb_api.rb

srv = UebAPI.new

srv.mount_proc('/for/bar', method: :GET) do |req, res|
if req.query["param"] == "invalid"
res.body = {result: :error}
else
res.body = {result: :ok}
end
end

上記が実現できる UebAPI#mount_proc を実装していきます。


UebAPI#mount_proc の実装

WebAPIを呼び出した時に返す結果の設定を保持する @mount_procs を用意します。


ueb_api.rb

  def initialize(bind_address: '127.0.0.1', port: 18_080)

...
@mount_procs = {}
...
end

WebAPI#mount_proc では、引数の情報を @mount_procs に情報をそのまま格納します。


ueb_api.rb

  def mount_proc(dir, method: :GET, body: {}.to_json, status: WEBrick::HTTPStatus::RC_OK, &block)

@mount_procs[dir] ||= {}
@mount_procs[dir][method] = { body: body, status: status, block: block }
end

あとは、 WebAPI#start の中で、 WEBRick#mount_proc で実際の動きを設定します。


ueb_api.rb

  def start

do_mount_procs
@srv.start
end
...
private
...
def do_mount_procs
@mount_procs.each do |dir, methods|
do_mount_proc(dir, methods)
end
end

def do_mount_proc(dir, methods)
@srv.mount_proc(dir) do |req, res|
log_request(req)
method = methods.fetch(req.request_method.to_sym)
if method
res.status = method[:status]
res.body = method[:body]
method[:block].call(req, res) if method[:block]
end
end
end



全体のコード

最後にちょっとリファクタリングして、 UebAPI#initialize では、インスタンス変数の初期化だけ行うようにします。

コードの全体はこんな感じになりました。


ueb_api.rb

require 'webrick'

require 'json'

class UebAPI
def initialize(bind_address: '127.0.0.1', port: 18_080)
@srv = WEBrick::HTTPServer.new(
BindAddress: bind_address,
Port: port
)
@mount_procs = {}
end

def start
do_mount_procs
default_mount_proc
trap('INT') { @srv.shutdown }
@srv.start
end

def mount_proc(dir, method: :GET, body: {}.to_json, status: WEBrick::HTTPStatus::RC_OK, &block)
@mount_procs[dir] ||= {}
@mount_procs[dir][method] = { body: body, status: status, block: block }
end

private

def default_mount_proc
@srv.mount_proc('/') do |req, res|
log_request(req)
res.body = {}.to_json
end
end

def do_mount_procs
@mount_procs.each do |dir, methods|
do_mount_proc(dir, methods)
end
end

def do_mount_proc(dir, methods)
@srv.mount_proc(dir) do |req, res|
log_request(req)
method = methods.fetch(req.request_method.to_sym)
if method
res.status = method[:status]
res.body = method[:body]
method[:block].call(req, res) if method[:block]
end
end
end

def log_request(req)
info = "method=#{req.request_method}, uri=#{req.request_uri}, query=#{req.query}, body=#{req.body}"
@srv.logger.info(info)
end
end

srv = UebAPI.new

# この部分は、必要に応じて追加、修正します。
srv.mount_proc('/foo/bar', method: :GET, status: 200) do |req, res|
res.body = { value: 'baz' }.to_json
end
srv.mount_proc('/foo/bar', method: :POST, body: { result: 'ok' }.to_json, status: 201)

srv.start



まとめ

gem 使った方が早いんでしょうが、自分でプログラミングする方が楽しいです。車輪の再発明上等。

あと、Railsで使われなくなってWEBRickのメッセージ見ることが減りましたが、これでまた見ることができますね。


2018/07/03 追記 content-typeの追加

content-type を指定した方が良いとご指摘いただいたので、とりあえず、決めうちで content-type で json を返すようにしておきます。


ueb_api.rb

  def do_mount_proc(dir, methods)

@srv.mount_proc(dir) do |req, res|
log_request(req)
method = methods.fetch(req.request_method.to_sym)
if method
res.content_type = 'application/json' # この行を追加
res.status = method[:status]
res.body = method[:body]
method[:block].call(req, res) if method[:block]
end
end
end