仕事で、WebAPIで取得した結果を処理するとか、自分自身が処理した結果をWebAPIに返すとか仕事でよくあります。
ですが、開発中に、そのWebAPIサーバーが準備できているとは限らないので、ダミーのWebAPIサーバーが欲しくなってきました。
ということでなんちゃってWebAPIサーバーを作ってみます。
要件
- リクエストがあったときに、リクエストのパス、HTTPメソッド、パラメータの情報などが表示されること
- リクエストがあったときに、リクエストに応じた結果をJSON形式で返却できること。
- 返す結果のJSON形式を自分で設定できること。
- リクエストに対する処理は何もしない。
方針
Ruby なら、Rails とか Sinatra とか色々あるんでしょうが、gemは極力使いたくないです。
標準のRubyのライブラリの範囲で動く手軽な感じのものにしたい。。と言うわけで、WEBrick を使います。
車輪の再発明上等。だってコードを書く方が楽しいから。
超最低限の何もしないWeb APIサーバ
とりあえず、なんちゃってWebAPIサーバーなので、ファイル名は、 ueb_api.rb
としておきます。
デフォルトではローカルホストにbindして、デフォルトポートは、18080にしておきます。
要求が来ても何もしません。
require 'webrick'
srv = WEBrick::HTTPServer.new(
BindAddress: '127.0.0.1',
Port: 18_080
)
srv.mount_proc('/') do |req, res|
# 何もしない
end
srv.start
実行してみます。
$ 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側で要求をちゃんと返しているみたいです。
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 を停止するとこんなエラーになります。
- -> /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 します。
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した時のパラメータとか表示されないので、表示されるように修正します。
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を返すようにします。
require 'json'
...
srv.mount_proc('/') do |req, res|
res.body = {}.to_json
コード全体
コードは全体でこんな感じになりました。
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 メソッドで行うようにします。
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 に倣うことにしました。
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 を返す設定ができるようにします。
srv = UebAPI.new
srv.mount_proc(
'/for/bar',
method: :POST,
body: {result: :ok}.to_json,
status: 201
)
パラメータに応じて、結果を変更できた方が便利かも知れません。ということで、以下のような書き方もできるようにします。
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
を用意します。
def initialize(bind_address: '127.0.0.1', port: 18_080)
...
@mount_procs = {}
...
end
WebAPI#mount_proc では、引数の情報を @mount_procs
に情報をそのまま格納します。
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
で実際の動きを設定します。
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 では、インスタンス変数の初期化だけ行うようにします。
コードの全体はこんな感じになりました。
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 を返すようにしておきます。
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