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

仕事で、WebAPIで取得した結果を処理するとか、自分自身が処理した結果をWebAPIに返すとか仕事でよくあります。
ですが、開発中に、そのWebAPIサーバーが準備できているとは限らないので、ダミーのWebAPIサーバーが欲しくなってきました。
ということでなんちゃってWebAPIサーバーを作ってみます。

## 要件
- リクエストがあったときに、リクエストのパス、HTTPメソッド、パラメータの情報などが表示されること
- リクエストがあったときに、リクエストに応じた結果をJSON形式で返却できること。
- 返す結果のJSON形式を自分で設定できること。
- リクエストに対する処理は何もしない。

## 方針

Ruby なら、Rails とか Sinatra とか色々あるんでしょうが、gemは極力使いたくないです。
標準のRubyのライブラリの範囲で動く手軽な感じのものにしたい。。と言うわけで、WEBrick を使います。
車輪の再発明上等。だってコードを書く方が楽しいから。

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

とりあえず、なんちゃってWebAPIサーバーなので、ファイル名は、 `ueb_api.rb` としておきます。
デフォルトではローカルホストにbindして、デフォルトポートは、18080にしておきます。
要求が来ても何もしません。

```ruby: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
```

実行してみます。

```shell-session: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
```

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

```shell-session:別コンソール
$ curl http://localhost:18080/foo/bar/baz
```

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

```shell-session: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 を停止するとこんなエラーになります。

```shell-session: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で停止します。

```shell-session
$ 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した時のパラメータとか表示されないので、表示されるように修正します。

```ruby: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を返すようにします。

```ruby:ueb_api.rb
require 'json'
...
srv.mount_proc('/') do |req, res|
  res.body = {}.to_json
```

## コード全体

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

```ruby: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 メソッドで行うようにします。

```ruby: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 に倣うことにしました。

```ruby: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 を返す設定ができるようにします。

```ruby:ueb_api.rb
srv = UebAPI.new
srv.mount_proc(
  '/for/bar',
  method: :POST,
  body: {result: :ok}.to_json,
  status: 201
)
```

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

```ruby: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` を用意します。

```ruby:ueb_api.rb
  def initialize(bind_address: '127.0.0.1', port: 18_080)
    ...
    @mount_procs = {}
    ...
  end
```

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

```ruby: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` で実際の動きを設定します。

```ruby: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 では、インスタンス変数の初期化だけ行うようにします。
コードの全体はこんな感じになりました。

```ruby: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 を返すようにしておきます。

```ruby: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
```