WebAPIのCLIクライアントの試作

#WebAPIのCLIクライアントの試作


###最初に
すいません、requireではありません。

今日は、コマンドラインからWebAPIを利用するためのCLIクライアントを作成しようかと思います。

作成するアプリでは、mruby Advent Calendarで @kaishuu0123さんが作成してくださったmruby-readlineの他、多く方々が作成されたgemを利用させていただいています。mruby/mrubyデフォルトのgemに加えて、下記のgemを利用しています。
作者の方々には、この場を借りてお礼申し上げます。


```
  conf.gem :github => 'kaishuu0123/mruby-readline', :branch => 'master'
  conf.gem :git => 'https://github.com/iij/mruby-regexp-pcre.git'
  conf.gem :git => 'https://github.com/iij/mruby-io.git'
  conf.gem :git => 'https://github.com/iij/mruby-socket.git'
  conf.gem :git => 'https://github.com/iij/mruby-mtest.git'
  conf.gem :git => 'https://github.com/luisbebop/mruby-polarssl.git'
  conf.gem :git => 'https://github.com/matsumoto-r/mruby-simplehttp.git'
``` 

###やりたいこと

お題ですが、下記の通りです。独自シェル（っぽい）のWebAPIクライアントを試作してみます。

- コマンドラインからWebAPIを叩けるアプリを作る
- シェルっぽい感じのUIを提供
- 入力の補完が出来たら嬉しい (readline任せ



###WebAPIを叩いてみる

WebAPIのCLIクライアントを作成するのに先立って、まずはhttp経由で何かしらのWebAPIを叩いてみます。
今回はYahoo! JAPANさんにて公開されているWebAPIを利用します。
仕様を調べるために http://developer.yahoo.co.jp/  を見ます。眺めます。
利用するためにはappidが必要とのことなので、取得しておきます。

APIによってはPOSTメソッドにも対応しているものがありますが、ひとまず対象はGETメソッドのみとします。
とりあえず、クエリストリングを作成するメソッドだけ実装したクラスを用意しておきます。
appidをベタ書きするのはアレなのでスクリプト起動時の引数として渡すことにします。

```ruby::api1
$appid = ARGV[0]

class YahooAPIBase
  USER_AGENT = "webapi_test"

  def build_querystring(params)
    query = []

    params.each do |k,v|
      query << "#{k}=#{v}"
    end

    query << "output=json"
    "?"+query.join("&")
  end

end

```


次に、実際に叩いてみるAPIを決めます。千葉県の天気が気になるので幕張メッセ付近の緯度経度を指定して気象情報APIを叩いてみます。

```ruby::api2
class WeatherAPI < YahooAPIBase
  def exec(req_params = { "coordinates" => "35.647306,140.034503"})
    protocol = "http"
    domain = "weather.olp.yahooapis.jp"
    endpoint = "/v1/place"
    port = "80"
    method = "GET"

    req_params.merge!({ "appid" => $appid })

    query = build_querystring(req_params)
    SimpleHttp.new(protocol, domain, port).request(method, endpoint+query, {'User-Agent' => "#{USER_AGENT}"}).body
  end

end

```

このクラスのインスタンスを生成し、実行すると以下のようなレスポンスが得られます。

```::Response
{"ResultInfo":{"Count":1,"Total":1,"Start":1,"Status":200,"Latency":0.007859,"Description":"","Copyright":"(C) Yahoo Japan Corporation."},"Feature":[{"Id":"201312140225_35.647306_140.0345","Name":"地点(35.647306,140.0345)の2013年12月14日 02時25分から60分間の天気情報","Geometry":{"Type":"point","Coordinates":"35.647306,140.0345"},"Property":{"WeatherList":{"Weather":[{"Type":"observation","Date":"201312140225"},{"Type":"forecast","Date":"201312140235"},{"Type":"forecast","Date":"201312140245"},{"Type":"forecast","Date":"201312140255"},{"Type":"forecast","Date":"201312140305"},{"Type":"forecast","Date":"201312140315"},{"Type":"forecast","Date":"201312140325"}]}}}]}
```

...と、本来はレスポンスに降水量の情報が入っていることを期待したのですが、入っていません。きっと雨は降らないのでしょう。
リクエストが間違っているような気もしますが、とりあえず今回はレスポンスが得られることが確認できれば良いので、このまま進めます。

###CLIを作る

次に入力受け付けるコマンドラインインターフェースを提供するクラスを作ります。


```ruby::Cli
class Cli
  def initialize
    @cli_commands = {}
  end

  def add_command(command)
    @cli_commands.merge!(command)
  end

  def run
    comp = proc { |s| @cli_commands.keys.grep( /^#{s}/ ) }

    Readline.completion_append_character = " "
    Readline.completion_proc = comp

    while command = Readline.readline('hogegeshell$ ', true)
      toplevel_command = command.split[0]
      if @cli_commands[toplevel_command]
        @cli_commands[toplevel_command].call command
      else
        puts "#{toplevel_command} is not defined."
      end
    end
  end

end

```

コマンドの登録ははkeyがString、valueがProcのHashを引数としてadd_commandで登録します。
valueのProcオブジェクトが実際に実行される処理となります。

```ruby::
cli = Cli.new

cli.add_command({
                  "weather" => proc { |s|
                    api = WeatherAPI.new
                    puts api.exec
                  }
                })

cli.run
```

実際に実行すると下記のようになります。

```
$ bin/mruby cli.rb ${APPID}
hogegeshell$ weather 
{"ResultInfo":{"Count":1,"Total":1,"Start":1,"Status":200,"Latency":0.015032,"Description":"","Copyright":"(C) Yahoo Japan Corporation."},"Feature":[{"Id":"201312140240_35.647306_140.0345","Name":"地点(35.647306,140.0345)の2013年12月14日 02時40分から120分間の天気情報","Geometry":{"Type":"point","Coordinates":"35.647306,140.0345"},"Property":{"WeatherList":{"Weather":[{"Type":"observation","Date":"201312140240"},{"Type":"observation","Date":"201312140250"},{"Type":"observation","Date":"201312140300"},{"Type":"observation","Date":"201312140310"},{"Type":"observation","Date":"201312140320"},{"Type":"observation","Date":"201312140330"},{"Type":"observation","Date":"201312140340"},{"Type":"forecast","Date":"201312140350"},{"Type":"forecast","Date":"201312140400"},{"Type":"forecast","Date":"201312140410"},{"Type":"forecast","Date":"201312140420"},{"Type":"forecast","Date":"201312140430"},{"Type":"forecast","Date":"201312140440"}]}}}]}
hogegeshell$ 
```

と、ここまででひとまずはコマンドの入力、WebAPIの実行、結果の取得が出来るようになりました。

全体のコードは[gist](https://gist.github.com/lyrical-hogege/ede40b9e6e69f96df3a4)に貼ってあります。

###やったこと
 - WebAPIをCLIから叩けるスクリプトを作成した
 - gemを組み合わせてシェルっぽいものを作成できることを確認した


### 課題

 - gemにする
   - WebAPIのクライアントとCLI部分を分け、もっと汎用的なものにした上でgemにしたい
 - 補完機能を改善する
   - サブコマンドや引数についても補完の対象にしたい
 - JSONのレスポンスをよしなに処理できるようにしたい


### 終わりに
readlineを利用すれば独自のシェルを比較的容易に実装できそうです。
真面目に作り込むと補完機能等で詰まることもありそうですが、現状でも簡易な独自シェルを作成する分には十分機能するものが作れると思います。