watson-api-client gem の背景

watson-api-client gem の背景
============================

「[Ruby からの Watson Natural Language Classifier 利用例](http://qiita.com/suchowan/items/4bb4db40c4ff434ee847)」の記事に関連して、watson-api-client gem の背景をまとめておきます。

1.発端
------

まずは 「Watson の機能のひとつ“Relationship Extraction”を試しにコマンドラインから動かしてみよう」と思い立ったのが発端。

一番慣れている Ruby から REST API をたたいて応答を得るスクリプトを書いてみることにしました。

ふむふむ API のパスは https://gateway.watsonplatform.net/relationship-extraction-beta/api/v1/sire/0 で、text やら rt やらのパラメータをつけて POST すればいいのか…とドキュメントを読み取る[^1]。

だったら、いちいち毎回こういうパスなんか書かずにクラスやメソッドにまとめてしまえば可読性が上がると思うのは誰しも同じです。そして、こういうときいつも困るのが命名規則。

ところが API のドキュメントを読むと「API の nickname は extract」とある。いったいこれは何？

もし、すべての API に nickname がついているなら、それをそのままメソッド名にすれば命名に悩まなくて済む！

でも、Web 上の API ドキュメントをひとつひとつ開いて API の nickname を確認してスクリプトをコーディングするってめんどくさいな。それに、仕様変更で名前がふらふら変わるかもしれない。

2.メタプログラミング
--------------------

…と、ここで気が付いたのが、Watson API ドキュメントのからくり。

どうやら Web の Watson API ドキュメントは個別に用意されているのではなくて、[Swagger](http://swagger.io/) というフォーマットで書かれた JSON ファイル(例えば[こちら](https://web.archive.org/web/20150918024822/http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/apis/listings/relationship-extraction))を“神様”にして、それをインタープリトして駆動されているらしい[^2]。

だったら「Swagger フォーマットの“神様”JSON ファイルを読んで API の nickname を確認してスクリプトをコーディングする」というメタスクリプトを書けばいいじゃないか！

…という経緯で出来上がったのが watson-api-client の[当初の安定バージョン](https://github.com/suchowan/watson-api-client/blob/73951c8b507b947d6ad71430c6f1e937b1644537/lib/watson-api-client.rb)です。

ポイントとなるコードを書き出してみると、こんな感じ↓

###- 全体起動時
全体起動時には[“API一覧”](https://web.archive.org/web/20150903190650/http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/apis/listings/api-docs.json)をもとに各機能クラスの入れ物のみ用意して、各機能クラスの定数“API”が参照された時点で初めて“神様”JSON ファイルを読みに行くようにしています。

```rb
listings = JSON.parse(open(Base + path, Options).read)
listings['apis'].each do |list|
  module_eval %Q{
    class #{('-'+list['path']).gsub(/[-_\/]+(.)/) {$1.upcase}} < self
      Service = superclass::Services['#{list['path'].sub(/^[-_\/]+/,'').gsub(/-/, '_')}']
      RawDoc  = "#{Base + listings['basePath'] + list['path']}"

      class << self
        alias :_const_missing :const_missing

        def const_missing(constant)
          if constant == :API
            const_set(:API, listings(JSON.parse(open(RawDoc, superclass::Options).read)))
          else
            _const_missing(constant)
          end
        end
      end
      pp [self, 'See ' + RawDoc, API['digest']] if '#{__FILE__}' == '#{$PROGRAM_NAME}'
    end
  }
end
```

定数“API” 参照時に呼ばれる listings メソッドは下記の通りで、“神様”JSON ファイルを読んで “nickname” などを取得しています。

```rb
def listings(apis)
  methods = {}
  digest  = {}
  apis['apis'].each do |api|
    api['operations'].each do |operation|
      body = nil
      (operation['parameters']||[]).each do |parameter|
        next unless parameter['paramType'] == 'body'
        body = parameter['name']
        break
      end
      nickname = operation['nickname'].sub(/(.)/) {$1.downcase}
      methods[nickname] = {'path'=>api['path'], 'operation'=>operation, 'body'=>body}
      digest[nickname]  = {'method'=>operation['method'], 'path'=>api['path'], 'summary'=>operation['summary']}
    end
  end
  {'apis'=>apis, 'methods'=>methods, 'digest'=>digest}
end
```

###- 機能開始時
各機能クラスのインスタンスを生成する際に定数“API”が参照されるようにし、つづいて個々の API に対応するインスタンスメソッドをメタプログラミングしています。

```rb
def initialize(options={})
  self.class::API['methods'].each_pair do |method, definition|
    self.class.module_eval %Q{define_method("#{method}",
      Proc.new {|options={}| rest_access_#{definition['body'] ? 'with' : 'without'}_body("#{method}", options)}
    )} unless respond_to?(method)
  end
  …
end

```

メタプログラミングを使ったおかげで、全体で有効行にして約100行程度のコンパクトなスクリプトになりました。

3.Watson API 日本語対応版?
---------------------

事件が起こったのは約半年後。突然“API一覧”がなくなったという[Issue#1](https://github.com/suchowan/watson-api-client/issues/1) が発行されました。

ちょうど Watson API 日本語対応版のアナウンスがされた時期です。たぶん大規模な変更に伴い Swagger ドキュメントまわりも変わってしまったのでしょう。

主な変化は、

・[“API一覧”](https://web.archive.org/web/20150903190650/http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/apis/listings/api-docs.json)が[空](http://www.ibm.com/watson/developercloud/apis/listings/api-docs.json)になってしまった。

結果、“API一覧”を人間用の Webドキュメントから文字列のパターンマッチをして“神様”JSON ファイルの所在を抽出せねばならなくなりました。

・Swagger フォーマット仕様が変わった。

もともと“nickname”というのは Watson API のローカルルール。これが“operationId”に変わって、しかも存在が保証されなくなった。同時にフォーマット仕様のバージョン自体が 1.2 から 2.0 に上がった。

…などです。

現状もとどおり動くようにはなっていますが、人間用の Webドキュメントから文字列のパターンマッチをせねばならない[^3]という問題は解決していないので、Issue#1 はオープンのままにしています。

4.Alchemy API 対応
------------------

次に起こったのは、そもそも当初の目的だった Watson の機能“Relationship Extraction”がなくなってしまったという事件。

もともとベータ版だった“Relationship Extraction”が廃止されてAlchemy API のひとつである“AlchemyLanguage”に統合されてしまったのです。

困ったことに Alchemy API は、サードパーティであった Alchemy 社を IBM 社が買収して Watson に後から統合したサービスのため、結構 Swagger ドキュメントのノリが違いました。

しかし結局塞翁が馬、「どのようなパラメータを指定してメソッドが呼ばれたかに応じて、賢く POST / GET / DELETE などを選択できるようになる」というオマケつきでこの問題は解決することができました。

5.今後
------

もともとは「仕様変更で名前がふらふら変わるかもしれない」という問題にメタプログラミングでロバストに対応できるのではないかという期待があったのですが、実際はそれほどでもなかったようです。

watson-api-client はボランティアで公開していて動作確認での課金発生は最小限にしているという事情があり、今後も Issue ドリブンでやっていきたいと思っています。

[^1]: “Relationship Extraction”はなくなったので、この部分のドキュメントは今はありません。

[^2]: おそらく nickname の  extract というのは API サーバー側の内部関数名なのでしょう。

[^3]: 当初からこのような状況だったら、そもそも watson-api-client を作ろうとは思わなかったでしょう。