いやホント、今更感たっぷりですけど (^^
SOAP について
かつて WebAPI といえば SOAP という時代がありました。
現在は、JSON形式による REST が主流になっていますが、XML形式によるリモートサーバとのやりとりの一つの形式として注目を集めていました。
データのやりとりをXMLで行うために、通信のデータ量が増えたり、コンピュータで扱う時にパースしたりと、ちょっと手間がかかるので、面倒といえば面倒なんですが、WSDLという書式でデータの型とか決めれるので親切といえば親切な方式なんですけどね。
SOAP(Simple Object Access Protocol) って言うくらいですから。
#現在は違うらしい。。。
Savon
で、ネットで探してみると、Savon という gem が見つかりました。
→ http://savonrb.com
というわけで、使ってみる。
$ gem install savon
テスト用 WSDL を作る
→ http://www.wakhok.ac.jp/~sakamoto/webservice/web_service_c3.html
→ http://itdoc.hitachi.co.jp/manuals/link/cosmi_v0870/APWK/EU310265.HTM#ID00735
一般的な感じで作って見ます (^^
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions
targetNamespace="https://web-service"
xmlns:impl="https://web-service"
xmlns:intf="https://web-service"
xmlns:tns="https://hoge.web-service"
xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:apachesoap="http://xml.apache.org/xml-soap"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<wsdl:types>
<schema targetNamespace="https://hoge.web-service"
xmlns="http://www.w3.org/2001/XMLSchema">
<import namespace="http://schemas.xmlsoap.org/soap/encoding/"/>
<complexType name="UserInfo">
<sequence>
<element name="name" nillable="false" type="xsd:string"/>
<element name="age" type="xsd:int"/>
<element name="birthday" nillable="true" type="xsd:string"/>
<element name="favorite" nillable="true" type="xsd:string"/>
</sequence>
</complexType>
</schema>
</wsdl:types>
<wsdl:message name="getInfoRequest">
<wsdl:part name="username" type="xsd:string"/>
<wsdl:part name="password" type="xsd:string"/>
</wsdl:message>
<wsdl:message name="getInfoResponse">
<wsdl:part name="userinfo" type="tns:UserInfo"/>
</wsdl:message>
<wsdl:portType name="TestService">
<wsdl:operation name="getInfo" parameterOrder="username password">
<wsdl:input message="impl:getInfoRequest" name="getInfoRequest"/>
<wsdl:output message="impl:getInfoResponse" name="getInfoResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="TestServiceSoapBinding" type="impl:TestService">
<wsdlsoap:binding style="rpc"
transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="getInfo">
<wsdlsoap:operation soapAction=""/>
<wsdl:input name="getInfoRequest">
<wsdlsoap:body encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
namespace="https://web-service" use="encoded"/>
</wsdl:input>
<wsdl:output name="getInfoResponse">
<wsdlsoap:body encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
namespace="https://web-service" use="encoded"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="TestServiceService">
<wsdl:port binding="impl:TestServiceSoapBinding" name="TestService">
<wsdlsoap:address
location="https://web-service/test_service"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
先頭の xmlns:xxxx
はおまじない的ななにか (^^
前半は 各WebService に合わせて変更します。後半はおまじない (^^
<wsdl:types>
は任意の型の定義。
ここでは、UserInfo という型を定義しています。
UserInfo には name(string型) と age(int型) と birthday(string型) と favorite(string型) があります。
<wsdl:message>
は 構造を定義します。
<wsdl:types> と同じ感じですね。
getInfoRequest には username(string型) と password(string型) があって、
getInfoResponse には、先ほど上で定義した userinfo(UserInfo型) がある、と定義しています。
<wsdl:portType>
は 入出力の操作の組み合わせを定義します。
getInfo という 操作(operation) には 入力(input)に getInfoRequest、出力(output)には getInfoResponse があるよ、と定義しています。
<wsdl:binding>
は portType で定義した組み合わせの具体的な定義をします。
getInfoRequest や getInfoResponse が先頭で定義したどの WebService の範囲にあるのか、とかとか。
<wsdl:service>
は 上で定義した binding が実際にどのアドレスにアクセスするのかを定義します。
location がそれですね。
ruby でやってみる
$ irb
> require "savon"
> client = Savon.client(:wsdl => "test.wsdl")
エラーがなければとりあえず読み込みは成功ということで。
確認してみましょう。
> client.operations
=> [:get_info]
メソッドが認識されているようです。
実行は以下。
> response = client.call(:get_info, :message => { "username" => "hoge", "password" => "tara" })
まあエラーになります。WebService がないので。。。
ConvertTemperature WebService
これは困った、ということで、とりあえず Savon のソースをみてみると、Test がありました。
→ https://github.com/savonrb/savon/blob/master/spec/integration/temperature_example_spec.rb
これをやってみることにします。
$ irb
> client = Savon.client(:wsdl => "http://www.webservicex.net/ConvertTemperature.asmx?WSDL")
=> #<Savon::Client:0x007fd535c241a8 @globals=#<Savon::GlobalOptions:0x007fd535c24180 @option_type=:global, @options={:encoding=>"UTF-8", :soap_version=>1, :namespaces=>{}, :logger=>#<Logg ... c1fc20 @document="http://www.webservicex.net/ConvertTemperature.asmx?WSDL", @adapter=nil, @request=#<HTTPI::Request:0x007fd535c1fbd0 @follow_redirect=false>>>
> client.operations
=> [:convert_temp]
はい、ConvertTemperature WebService には convert_temp という関数があることがわかりましたが、これだけでは引数とか戻り値とかがわかりません。
ということで、少し詳しく見て見ます。
> client.wsdl.operations[:convert_temp]
=> {:action=>"http://www.webserviceX.NET/ConvertTemp", :input=>"ConvertTemp", :output=>"ConvertTempResponse", :namespace_identifier=>"tns", :parameters=>{:Temperature=>{:name=>"Temperature", :type=>"double"}, :FromUnit=>{:name=>"FromUnit", :type=>"TemperatureUnit"}, :ToUnit=>{:name=>"ToUnit", :type=>"TemperatureUnit"}}}
なんとなく、見えてきたような気がします。
入力として ConvertTemp があって、戻りは ConvertTempResponse で、(入力の)パラメータには Temperature、FromUnit、ToUnit があるということがわかります。
がしかし、これだけでは詳細がわからないので、やはり WSDL 自体を覗いて見ます。
<前略>
<wsdl:types>
<s:schema elementFormDefault="qualified" targetNamespace="http://www.webserviceX.NET/">
<s:element name="ConvertTemp">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="Temperature" type="s:double"/>
<s:element minOccurs="1" maxOccurs="1" name="FromUnit" type="tns:TemperatureUnit"/>
<s:element minOccurs="1" maxOccurs="1" name="ToUnit" type="tns:TemperatureUnit"/>
</s:sequence>
</s:complexType>
</s:element>
<s:simpleType name="TemperatureUnit">
<s:restriction base="s:string">
<s:enumeration value="degreeCelsius"/>
<s:enumeration value="degreeFahrenheit"/>
<s:enumeration value="degreeRankine"/>
<s:enumeration value="degreeReaumur"/>
<s:enumeration value="kelvin"/>
</s:restriction>
</s:simpleType>
<s:element name="ConvertTempResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="ConvertTempResult" type="s:double"/>
</s:sequence>
</s:complexType>
</s:element>
<s:element name="double" type="s:double"/>
</s:schema>
</wsdl:types>
<後略>
というわけで、実行してみます。
> response = client.call(:convert_temp, :message => {"Temperature" => 27.0, "FromUnit" => "degreeCelsius", "ToUnit" => "kelvin"})
=> #<Savon::Response:0x007fd535b444e0 @http=#<HTTPI::Response:0x007fd535b44850 @code=200, @headers={"cache-control"=>"private, max-age=0", "content-type"=>"text/xml; charset= ... Celsius", "ToUnit"=>"kelvin"}, :soap_action=>"http://www.webserviceX.NET/ConvertTemp"}>>
> response.body
=> {:convert_temp_response=>{:convert_temp_result=>"300.15", :@xmlns=>"http://www.webserviceX.NET/"}}
なんか正しく返ってきているみたいですね。
とっても素敵
簡単にできました。Savon って素敵!
ネット上を探すのですが、だいたいが 2006年とか2009年とかそれくらいの記事しかなくて、やはり今時 SOAP 使ってるところってないよなぁ、自前で作らんといかんのか、って思ってたのでとても助かりました。ありがとう!
認証のある WebSerice にアクセスする場合
上記は普通にアクセスする場合なのですが、https で Basic認証 とか クライアント認証 を行なっているサービスにアクセスする場合は最初のコードが少し違うのでメモしておきます。
Basic認証の場合
→ https://qiita.com/niku4i/items/0a262abb4666f834b6af
クライアント認証の場合
前項のサイトを参考に、savon のソースを覗いてみました。
require "openssl"
require "savon"
@client_cert_file = "client.pfx"
@client_password = "client_password"
@ca_cert_file = "ca.pem"
pkcs = OpenSSL::PKCS12.new(File.read(@client_cert_file),
@client_password)
client = Savon.client(:wsdl => "test.wsdl",
:ssl_verify_mode => :peer,
:ssl_ca_cert_file => @ca_cert_file,
:ssl_cert_key => pkcs.key,
:ssl_cert => pkcs.certificate)
これでいける感じです。
@client_cert_file
は クライアント証明書のファイル名
@client_password
は クライアント証明書のパスワード
@ca_cert_file
は CA局の証明書ファイル
test.wsdl
は WSDLファイル(又はアドレス)
XMLRPC(おまけ)
自作しないとダメかもと思って色々探してたときですが、XMLを使った通信がある、ということだったので、使って見ました。
gem インストールする必要があるみたいなので、インストール
$ gem 'xmlrpc'
使い方は以下な感じらしい。
# -*- coding: utf-8 -*-
require "xmlrpc/client"
client = XMLRPC::Client.new2("http://web-service.server")
result = client.call("method", 1, 2)
ネット上ではなんか簡単にかいてあるので、本当に XML になってるかちょっと確認してみました。
<?xml version=\"1.0\" ?>
<methodCall>
<methodName>method</methodName>
<params>
<param><value><i4>1</i4></value></param>
<param><value><i4>2</i4></value></param>
</params>
</methodCall>
嗚呼なってます、なってます。
ですが、どうやら定型なので任意の型は使用できないみたいです。
ちょっとあれですが、定型で使用するなら大丈夫かな??