Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
14
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

今更ですが、Ruby で SOAP をやってみる

いやホント、今更感たっぷりですけど (^^

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

一般的な感じで作って見ます (^^

test.wsdl
<?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 自体を覗いて見ます。

www.webservicex.net/ConvertTemperature.asmx?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>

嗚呼なってます、なってます。

ですが、どうやら定型なので任意の型は使用できないみたいです。
ちょっとあれですが、定型で使用するなら大丈夫かな??

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
14
Help us understand the problem. What are the problem?