11 分のあれに 5 分くらいでウェブサービス API を追加するシナリオ

http://itoshi.tv/d/?date=20060506#p01ActionWebService (AWS) をつかってウェブサービス API を追加してみた.タイプするだけなら 5 分くらいでできるシナリオである.

仕様

今回のウェブサービス API の仕様は,データベースのエントリを ID で取得したり,ID のリストを取得する基本的で簡単なものを考える.

  • API 名:Expression
  • affyid の発現量を取得するメソッド:find_expressions_by_id(affyid)
    • 引数 affyid は String
    • 返り値は Float の Array
  • affyid の任意の day の発現量を取得するメソッド:find_expression_by_id(affyid, day)
  • 引数 affyid は String
    • 引数 day は Int
    • 返り値は Float
  • affyid のリストを取得するメソッド:find_all_affyid
    • 引数はなし
    • 返り値は String の Array

次に,ウェブサービスのためのコードを生成する.なお,以降の作業は実働する11 分のあれディレクトリ expview の中でおこなう.

ruby script/generate web_service Expression find_expression_by_id find_expressions_by_id find_all_affyids

仕様にもとづいて API 定義 app/api/expression_api.rb を埋める.

生成された状態.

class ExpressionApi < ActionWebService::API::Base
  api_method :find_expression_by_id
  api_method :find_expressions_by_id
  api_method :find_all_affyids
end

こうする.

class ExpressionApi < ActionWebService::API::Base
  api_method :find_expression_by_id,
             :expects => [{:affyid => :string}, {:day => :int}],
             :returns => [:float]

  api_method :find_expressions_by_id,
             :expects => [{:affyid => :string}],
             :returns => [[:float]]

  api_method :find_all_affyids,
             :expects => [],
             :returns => [[:string]] 
end

expects に引数の名前と型情報,returns に返り値の型情報を Hash の形で記述する.DSL 風味.

つぎに,仕様にもとづいて API のコントローラ app/controllers/expression_controller.rb を書く.
生成された状態.

class ExpressionController < ApplicationController
  wsdl_service_name 'Expression'

  def find_expression_by_id
  end

  def find_expressions_by_id
  end

  def find_all_affyids
  end
end

こうする.

class ExpressionController < ApplicationController
  wsdl_service_name 'Expression'
  web_service_scaffold :invoke

  def find_expression_by_id(affyid, day)
    @expression = Expression.find_by_affyid(affyid) 
    if [0, 2, 4, 10].include?(day)
      @expression["d#{day}"]
    else
      -1.0
    end
  end

  def find_expressions_by_id(affyid)
    @expression = Expression.find_by_affyid(affyid) 
    ['d0', 'd2', 'd4', 'd10'].map {|m| @expression[m] }
  end

  def find_all_affyids
    Expression.find(:all).map {|x| x.affyid }
  end
end

サーバの起動と web service scaffold で動作確認

ruby script/server
open http://localhost:3000/expression/invoke

app/controllers/expression_controller.rb の web_service_scaffold :invoke によって,ウェブサービスのクライアントが提供されている.なお,open コマンドは Mac OS X 付属のものである.
ここで注意する必要があるのは find_all_affyids() は返り値が非常に大きいため,レンダリングに非常に多くのリソースを必要とするのでブラウザが固まりがち.invoke ボタンをクリックしてはいけない.
これくらいの API だったら 5 分くらいでタイプできるでしょう.

SOAP::WSDLDriver をつかったクライアント

SOAP::WSDLDriver をつかったクライアントの例.ウェブサービスで利用できるメソッドは methods(false) で調べることができる.

require 'soap/wsdlDriver'

wsdl_uri = "http://localhost:3000/expression/service.wsdl"
factory = SOAP::WSDLDriverFactory.new(wsdl_uri)
driver = factory.create_driver

method_list = driver.methods(false)
p method_list #=> ["findExpressionsById", "FindAllAffyids", "findAllAffyids", "FindExpressionById", "findExpressionById", "FindExpressionsById"]

expression = driver.findExpressionById("100015_at", 0) 
p expression #=> 5.55660043997955

expression = driver.send("FindExpressionById", "100015_at", 0)
p expression #=> 5.55660043997955

expressions = driver.findExpressionsById("100015_at") 
p expressions #=> [5.55660043997955, 5.6629197160292, 5.60480888509297, 5.72528846920502]

affyid_list = driver.findAllAffyids()
p affyid_list #=> ["100001_at", "100002_at", "100003_at", ...

まとめ

Rails すごい.Action Web Service をつかうと本当に簡単に API の公開ができる.WSDL を手書きしたくない.
API 名を考えるのはしんどい.
ウェブサービス API とくに SOAP を使ったサービスを提供すると,Taverna といったワークフローエディタから利用することができる.(じつはこの例では問題があり,そのままでは利用できない.)

AWS でつくった WSDL なウェブサービスを Taverna から利用する

WSDL の提供されているウェブサービスは Taverna で容易に追加して扱うことができる.id:nakao_mitsuteru:20060511:AWSウェブサービス API を追加した expview を試してみた.

expview の起動

id:nakao_mitsuteru:20060511:AWSAPI 付きの expview を起動する.

$ cd expview
$ ruby script/server
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2006-05-15 00:02:57] INFO  WEBrick 1.3.1
[2006-05-15 00:02:57] INFO  ruby 1.8.4 (2005-12-24) [i686-darwin8.6.1]
[2006-05-15 00:02:57] INFO  WEBrick::HTTPServer#start: pid=1559 port=3000

WSDL を確認する.

$ open http://localhost:3000/expression/service.wsdl

Safari.app の場合は真っ白なページになるけど,ソースを表示(opt + cmd + u)で WSDL が読める.

Taverna に WSDL を登録

Taverna project website: Documentation Home の Basic Introdutory Tutorial (pdf) を読んでいないと以下の内容は分からないかもしれません.

  1. Taverna.app 起動.
  2. Taverna の Available services ウィンドウの Available Processors のうえで右クリック(もしくは ctrl + クリック)で Available Processors メニュー を表示.
  3. Available Processors メニューの Add New WSDL scavenger... を選択.
  4. WSDL URI (http://localhost:3000/expression/service.wsdl) を登録.
  5. Available Processors ウィンドウに WSDL @ http://localhost:3000/expression/service.wsdl が追加されればオッケー.

FindExpressionsById

  1. Available services ウィンドウの WSDL @ http://localhost:3000/expression/service.wsdl を展開して FindExpressionsById を表示.
  2. FindExpressionsById を右クリック(もしくは ctrl + クリック)してメニューを表示.
  3. メニューの中の invoke を選択.
  4. Run Workflow ウィンドウが表示される.
  5. Run Workflow ウィンドウの Input Document パネルの affyid を右クリック(もしくは ctrl + クリック)して New Input Value を選択.
  6. 右のパネルの Some input data goes here を適当な affyid(例えば 100015_at) に書き換える.
  7. Run Workflow ウィンドウの右下の Run Workflow をクリックして実行.
  8. Enactor invocation ウィンドウが表示される.

ところが,ここで processor が ServiceFallure になる.困りました.

Enactor invocation の Process Report

エラーを確認する.

<workflowReport workflowID="2" workflowStatus="COMPLETE">
  <processorList>
    <processor name="processor">
      <ServiceFailure TimeStamp="2006/05/15 0:37:44" />
      <ServiceError Message="Error occured during invocation [D" TimeStamp="2006/05/15 0:37:44">Error occured during invocation [D&lt;br&gt;   WSDLInvocationTask.execute(..) : l
ine 192 &amp;lt;WSDLInvocationTask.java&amp;gt;&lt;br&gt;   ProcessorTask.runAndGenerateTemplates(..) : line 503 &amp;lt;ProcessorTask.java&amp;gt;&lt;br&gt;   ProcessorTask.doI
nvocationWithRetryLogic(..) : line 453 &amp;lt;ProcessorTask.java&amp;gt;&lt;br&gt;   ProcessorTask.invokeOnce(..) : line 376 &amp;lt;ProcessorTask.java&amp;gt;&lt;br&gt;   Proc
essorTask.invokeWithoutIteration(..) : line 561 &amp;lt;ProcessorTask.java&amp;gt;&lt;br&gt;   ProcessorTask.invoke(..) : line 301 &amp;lt;ProcessorTask.java&amp;gt;&lt;br&gt;  
 ProcessorTask.handleRun(..) : line 237 &amp;lt;ProcessorTask.java&amp;gt;&lt;br&gt;   NewState$1.run(..) : line 67 &amp;lt;NewState.java&amp;gt;&lt;br&gt;&lt;br&gt;[D&lt;br&gt;
   DataThingFactory.convertObject(..) : line 146 &amp;lt;DataThingFactory.java&amp;gt;&lt;br&gt;   DataThing.&lt;init&gt;(..) : line 234 &amp;lt;DataThing.java&amp;gt;&lt;br&gt;
   WSDLInvocationTask.execute(..) : line 134 &amp;lt;WSDLInvocationTask.java&amp;gt;&lt;br&gt;   ProcessorTask.runAndGenerateTemplates(..) : line 503 &amp;lt;ProcessorTask.java&
amp;gt;&lt;br&gt;   ProcessorTask.doInvocationWithRetryLogic(..) : line 453 &amp;lt;ProcessorTask.java&amp;gt;&lt;br&gt;   ProcessorTask.invokeOnce(..) : line 376 &amp;lt;Proces
sorTask.java&amp;gt;&lt;br&gt;   ProcessorTask.invokeWithoutIteration(..) : line 561 &amp;lt;ProcessorTask.java&amp;gt;&lt;br&gt;   ProcessorTask.invoke(..) : line 301 &amp;lt;P
rocessorTask.java&amp;gt;&lt;br&gt;   ProcessorTask.handleRun(..) : line 237 &amp;lt;ProcessorTask.java&amp;gt;&lt;br&gt;   NewState$1.run(..) : line 67 &amp;lt;NewState.java&am
p;gt;&lt;br&gt;</ServiceError>
      <Invoking TimeStamp="2006/05/15 0:37:44" />
      <ProcessScheduled TimeStamp="2006/05/15 0:37:44">
        <s:arbitrarywsdl xmlns:s="http://org.embl.ebi.escience/xscufl/0.1alpha">
          <s:wsdl>http://localhost:3000/expression/service.wsdl</s:wsdl>
          <s:operation>FindExpressionsById</s:operation>
        </s:arbitrarywsdl>
      </ProcessScheduled>
    </processor>
  </processorList>
</workflowReport>

対処法

FindExpressionsById は返り値は Float の Array です.試行錯誤の結果,これを String の Array にすると値は(文字列としてですが)取り出すことができます.

その場合は expview の二つのファイルを変更する必要があります.

app/api/expression_api.rb の :returns の float を string に変更

class ExpressionApi < ActionWebService::API::Base
  api_method :find_expression_by_id,
             :expects => [{:affyid => :string}, {:day => :int}],
             :returns => [:string]

  api_method :find_expressions_by_id,
             :expects => [{:affyid => :string}],
             :returns => [[:string]]

  api_method :find_all_affyid,
             :expects => [],
             :returns => [[:string]]
end

app/controllers/expression_controller.rb のメソッドの返り値に to_s を追加.

class ExpressionController < ApplicationController
  wsdl_service_name 'Expression'
  web_service_scaffold :invoke

  def find_expression_by_id(affyid, day)
    @expression = Expression.find_by_affyid(affyid) 
    if [0, 2, 4, 10].include?(day)
      @expression["d#{day}"].to_s
    else
      -1.0
    end
  end

  def find_expressions_by_id(affyid)
    @expression = Expression.find_by_affyid(affyid) 
    ['d0', 'd2', 'd4', 'd10'].map {|m| @expression[m].to_s }
  end

  def find_all_affyid
    Expression.find(:all).map {|x| x.affyid }
  end
end

再試行

  1. Taverna.app を一度終了してから起動し直す.そうしないと,あたらしい WSDL を読み込まない.
  2. Available services ウィンドウの WSDL @ http://localhost:3000/expression/service.wsdl を展開して FindExpressionsById を表示.
  3. FindExpressionsById を右クリック(もしくは ctrl + クリック)してメニューを表示.
  4. メニューの中の invoke を選択.
  5. Run Workflow ウィンドウが表示される.
  6. Run Workflow ウィンドウの Input Document パネルの affyid を右クリック(もしくは ctrl + クリック)して New Input Value を選択.
  7. 右のパネルの Some input data goes here を適当な affyid(例えば 100015_at) に書き換える.
  8. Run Workflow ウィンドウの右下の Run Workflow をクリックして実行.
  9. Enactor invocation ウィンドウが表示される.
  10. Result browser パネルが表示されると,正常に実行されています.
  11. 左側のパネルをクリックして値を表示.affyid が 100015_at ならこんな値.
Self
5.55660043997955
5.6629197160292
5.60480888509297
5.72528846920502

まとめ

すべてが Taverna に(繋がるように)なる,を実践するのに Ruby on Rails の Action Web Service (AWS) を使うとローカルで使っているサービスを手軽に扱うことができる.

問題点:AWS と Taverna で型変換上の問題がある.Float が扱えていない.

GUI アプリの解説をテキストだけでしてもなんだかよく分からない.

課題:ソースコードの一部をハイライトして表示したいけどやり方がわからない.