HitoHana(ひとはな)の舞台裏

HitoHana(ひとはな)運営の舞台裏をご紹介いたします!

RMSのAPIをRubyで実装したい 4/3 (楽天ペイ対応)

前回のブログより

新人の田中です。この記事ではRMSの受注APIをRailsのアプリケーションに導入した事例を紹介しています。前回一応出来上がりはしたのですが、完成度に物足りなさを感じ、機能を追加しよう!というところまで進みました。

追加しようとした機能はこちら

  • ログを出力する
  • APIの深いネスト構造をカプセル化し、外から必要な属性に簡単にアクセスできるようにする

なんですが

会社のRMSの契約が変更となり、楽天ペイというサービスを使うことになったため、楽天ペイのAPIで上記の機能も含めて実装することになりました。

ですので今回の記事からは受注APIではなく、楽天ペイAPIのお話です。ご注意ください。

backstage.hitohana.tokyo

まずはいつものように完成形をイメージしてみる

完成形はいつもみたいにクラスメソッドを呼び出す感じにします。

また、クラスメソッド名、リクエストのパラメタ名、レスポンスへのアクセス方法もいままでと同じようなルールにします。

# メソッドの種類や引数の型や内容は公式ページを参照してください。
# - メソッド名
#     公式のAPI名をスネークケースにしたもの。
#         ex) getOrder => get_order, updateOrderShipping => update_order_shipping
# - 引数名
#     公式では引数名をスネークケースとしたもの。ただし、先頭文字が大文字の場合は引数名の先頭のみ大文字とする。(ちょっと苦しい)
#         ex) order_number => orderNumber, ShippingModelList => Shipping_model_list

response = Rms::PayOrder.get_order(order_number_list: ['楽天の注文番号1', '楽天の注文番号2'])

# - レスポンスへのアクセス
#     公式の属性名をスネークケースにしたメッセージをレスポンスのボディへ送ることでアクセスする。
#         ex) orderModelList => order_model_list

r_orders = response.body.order_model_list

# ついでにレスポンスのHTTPステータスはAPIの戻り値にstatusメッセージを投げることで取得する。

result = response.status
# => 200

実装の方針

楽天ペイAPIは受注APIとは違ってSoap通信ではありません。そこで今回は拡張性の高いMiddlewareパターンを実装できるFaradayを使うことにしました。

github.com

github.com

実装

まずメインのソースはこちら。クラス全体のデザインはこれまでと変わりありません。なお、初期設定やRms::Auth.keyについてはこのシリーズの第2回の記事を参照してください。

require 'faraday'
require 'rms/auth'
require 'rms/middleware'

module Rms
  class PayOrder
    BASE_URL = 'https://api.rms.rakuten.co.jp'.freeze
    API_METHODS = %w[
      get_order
      search_order
      confirm_order
      update_order_shipping
      update_order_delivery
      update_order_orderer
      update_order_remarks
      update_order_sender
      update_order_memo
      get_payment
      cancel_order
      get_sub_status_list
      update_order_sub_status
    ].freeze

    class << self
      API_METHODS.each do |api_method|
        define_method api_method do |args = nil|
          connection.post do |req|
            req.url "/es/2.0/order/#{api_method.camelize(:lower)}"
            req.headers['Content-Type'] = 'application/json; charset=utf-8'
            req.headers['Authorization'] = Rms::Auth.key
            req.body = args
          end
        end
      end

      private

      def connection
        Faraday.new(url: BASE_URL) do |faraday|
          faraday.request  :camelcase
          faraday.request  :url_encoded
          faraday.request  :json

          faraday.response :logger
          faraday.response :mashify
          faraday.response :snakecase
          faraday.response :json, content_type: /\bjson$/

          faraday.use :instrumentation
          faraday.adapter Faraday.default_adapter
        end
      end
    end
  end
end

connectionメソッドで使用するmiddlewareのほとんどはfaradayやfaraday_middlewareのものをそのまま使用していますが、一部自作しました。それらのソースがこちらです。 なお、deep_transform_keysメソッドなど、active_supportが読み込まれていることが前提のコードです。

require 'faraday'
require 'faraday_middleware'

module Rms
  class Middleware
    autoload :Camelcase, 'rms/middleware/camelcase'
    autoload :Snakecase, 'rms/middleware/snakecase'

    if Faraday::Middleware.respond_to? :register_middleware
      Faraday::Request.register_middleware \
        camelcase: -> { Camelcase }

      Faraday::Response.register_middleware \
        snakecase: -> { Snakecase }
    end
  end
end
module Rms
  class Middleware
    class Camelcase < Faraday::Middleware
      def call(env)
        if env[:body].respond_to? :deep_transform_keys!
          env[:body].deep_transform_keys! do |key|
            if key.to_s.first =~ /[A-Z]/
              key.to_s.camelize(:upper)
            else
              key.to_s.camelize(:lower)
            end
          end
        end
        @app.call env
      end
    end
  end
end
module Rms
  class Middleware
    class Snakecase < Faraday::Response::Middleware
      def on_complete(env)
        if env[:body].respond_to? :deep_transform_keys!
          env[:body].deep_transform_keys! { |key| key.to_s.underscore }
        end
      end
    end
  end
end

これではじめのサンプルコードが動くようになりました! 記事のボリュームが多くなってしまったので今回はここまでとします。faradayやmiddlewareについては次回記載しようと思います。

すでに 4/3 回となってしまいましたが、このシリーズはもうちょっとだけ続きます。

おわりに

HitoHana(ひとはな)では、仕事が大好きで個性的なエンジニアを大募集しています!

www.wantedly.com

RMSのAPIをRubyで実装したい 3/3

前回のブログより

新人の田中です。この記事ではRMSの受注APIをRailsのアプリケーションに導入した事例を紹介しています。
前回、前々回はRMSのAPIをRubyで使えるようにコードを書いてきました。
今回はこのシリーズの締めくくりとして、前回作成したAPIのRubyのラッパーと、ひとはなのアプリケーションをつなげるためのコードを書いていきます。

backstage.hitohana.tokyo

まずはいつものように完成形をイメージしてみる

Proxyパターンを使って実装します。ですので以下の条件を満たすようなコードを考えていきます。

  • アプリケーションの注文モデルのインスタンス(order)の情報を保持する。
  • RMSのAPIを使用して楽天側の情報にアクセスできる。
  • アプリケーションの注文モデルのメソッドはそのまま使える。
# orderはアプリケーションで使用している注文モデルのインスタンス
r_order = Rms::OrderProxy.new(order)

r_order.rakuten_info
# => 楽天の注文情報

# do_somethingメソッドはもともとの注文モデルのインスタンスメソッド
r_order.do_something
# => order.do_somethingと同じ戻り値

これだけなら簡単なんだけど

以上の条件を満たせるようにProxyオブジェクトを作ってみました。

module Rms
  class OrderProxy
    def initialize(order)
      @order = order
    end

    def method_missing(method_name, *args)
      @order.send method_name, *args
    end

    def rakuten_info
      return nil unless rakuten_order?
      # Rms::Order.get_orderについては前回の記事を参照。
      Rms::Order.get_order(
        is_order_number_only_flag: false,
        order_number: [@order.platform_number],
      )
    end

    private

    def rakuten_order?
      @order.platform_name == '楽天'
    end
  end
end

たしかにこれだけでもちゃんと動くんですけど、

なんかつまんない

ので、APIを呼び出す部分に関してもう少し工夫してみることにしました。

イケてないところは?

実際にAPIを使う際に不便に思っていることを挙げてみて、そこから機能を追加していくことにします。

不便その1:レスポンスの構造

本家のAPIのレスポンスの構造の問題なんですが、ネストが深くて、実際に業務で仕様する部分に到達するためには複数のメッセージを投げないといけません。

response = Rms::Order.get_order(args)
# => #<Hashie::Mash>

# 実際に注文情報を得るためにはこのように複数のメッセージを投げないといけない
order_info = response.get_order_response.return.order_model
# => 楽天の注文情報

不便その2:エラーハンドリング

Soap通信が失敗したり、RMSのシークレットキーの有効期限切れなどの際に発生したエラーを検知する機能がありません。

機能を追加しよう

なので、以下の機能を追加することにしました。

  • APIの生のレスポンスそのままを返すのではなく、業務で使う部分だけを抽出して返します。
  • ログを出力します。

ただ、これらの機能を普通にくっつけても

つまんない

ので、以前から気になっていたgemを使ってみることにしました。

github.com

これなら有名なRackのように再利用可能なMiddlewareを作って、その組み合わせでサービスを構成できます。

次回は

Middlewareを実装してこのシリーズを締めたいと思います。

さっき今回でこのシリーズを締めると言ったな。あれは嘘だ。

おわりに

HitoHana(ひとはな)では、仕事が大好きで個性的なエンジニアを大募集しています!

www.wantedly.com

復帰を使って進捗状況を出力する

暑くなってきましたね

まだクーラーを付けてない方のエンジニアの新原です。

さて、本題ですが、

バッチを書く時に進捗状況が気になりますね。
puts "#{index}番目です!!"というように、進捗状況を表示してもよいですが大量にログが流れるのであまり見やすいとは言えません。
そこで、復帰"\r"を利用して処理の進捗などを見やすくできます。

10.times do |index|
  print index.to_s + "\r"
  sleep 1
end

f:id:hitohana-backstage:20180708165810g:plain

どういうふうに動くのか

"\r"は行頭までカーソルを戻すというエスケープ文字です。
「復帰」と呼ぶそうです。
キャリッジリターン(CR、復帰)というと馴染みがありますね。

突然ですが

HitoHana(ひとはな)では、Rubyエンジニアを大募集しています!
私一人で開発を進めていましたが今は3人の開発チームになっています。
まだまだ開発リソースが必要ですので、少しでも興味があればエントリーをお願いします!

www.wantedly.com

RMSのAPIをRubyで実装したい 2/3

前回のブログより

新人の田中です。この記事ではRMSの受注APIをRailsのアプリケーションに導入した事例を紹介しています。
前回は動作確認まででしたが、今回からは実際にアプリケーションに取り込むためのコードを書いていきたいと思います。

backstage.hitohana.tokyo

今回の目標

今回の目標はライブラリ層の実装です。前回の記事にも記載しましたが、全体の設計上は3階層に分けて考えています。

  • モデル層(業務ロジック部分)
  • サービス層(業務ロジックとRMSのAPIをつなぐ部分)
  • ライブラリ層(RMSのAPIをRubyでも使用できるようにした部分)

気をつけたこと

ライブラリ層はAPIをRubyで使えるようにするだけなので、インターフェースの加工はRubyで使いやすいようにするだけの最小限に抑えます。

ひとはなのアプリケーションで使いやすいようにインターフェースを加工するのはサービス層で行います。ライブラリ層ではなるべく本家のAPIの仕様書に沿った形のインターフェースで作成することにしました。

とはいえレスポンスがHashのままだと扱いにくいので、Hashie::Mashを使用してレスポンスの各データにはドットノーテーションでアクセスできるようにしました。

github.com

実装

の前に

最終的にどんなコードでAPIを呼び出すことにするのか考えました。こんな感じにできるように実装していこうと思います。

args = {
  is_order_number_only_flag: false,
  order_number: ['楽天の注文番号'],
}

response = Rms::Order.get_order(args)
# => #<Hashie::Mash>

ope_response = response.get_order_response.return
# => #<Hashie::Mash>

ope_response.error_code
# => 'N00-000'

ope_response.message
# => '正常終了'

rakuten_order_model = ope_response.order_model
# => #<Hashie::Mash> ※注文情報が入っている

初期設定

初期設定関連はよくあるものを使用したので詳細は割愛します。以下のように設定してあとから呼び出せるようにしています。

Rms.configure do |config|
  config.service_secret = 'WEB API契約時に配布されるサービスシークレット'
  config.license_key    = 'WEB API契約時に配布されるライセンスキー(有効期限付き)'
  config.shop_url       = '楽天の店舗URL'
  config.user_name      = 'なんでもよい'
end

# Rms.config.service_secret の形で呼び出せるように実装しています(詳細は割愛)

認証

APIを呼び出すごとに何回も認証キーを計算しなくてよいように、認証キーはシングルトンクラスに作らせるようにしました。

require 'singleton'
require 'base64'

module Rms
  class Auth
    include Singleton

    attr_reader :key

    class << self
      def key
        instance.key
      end
    end

    def initialize
      @key = "ESA #{Base64.strict_encode64(Rms.config.service_secret + ':' + Rms.config.license_key)}"
    end
  end
end

Soapクライアント

本家RMSの受注APIのページを見てみると、メソッドはたくさんあるけど基本的なつくりは同じようだったのでSoapクライアントをひとつ作って、各APIはそれを使ってリクエストをするという構成にしました。

require 'savon'
require 'singleton'
require 'hashie/mash'

module Rms
  class Order
    class SoapClient
      include Singleton

      WSDL = 'https://api.rms.rakuten.co.jp/es/1.0/order/ws?WSDL'.freeze

      class << self
        def call(operation, args)
          instance.call(operation, args)
        end
      end

      def initialize
        @client = Savon.client(wsdl: WSDL)
      end

      def call(operation, args)
        response = @client.call(operation, message: message(args))
        Hashie::Mash.new(response.body)
      end

      private

      def message(args)
        auth_params.merge(arg1: args)
      end

      def auth_params
        {
          arg0: {
            auth_key: Rms::Auth.key,
            shop_url: Rms.config.shop_url,
            user_name: Rms.config.user_name
          }
        }
      end
    end
  end
end

各メソッド

各メソッドはSoapクライアントを呼び出すだけで中身は変わらないのでMethod Missingを使って実装してみました。 define_methodで一括して全てのメソッドを作成しました。

module Rms
  class Order
    API_METHODS = %i(
      get_order
      update_order
      cancel_order
      change_status
      decision_point
      r_bank_account_transfer
      change_r_bank_to_unprocessing
      do_enclosure
      do_un_enclosure
      change_enclosure_parent
      get_enclosure_list
      get_request_id
      get_result
    ).freeze

    class << self
      API_METHODS.each do |api_method|
        define_method api_method do |args = nil|
          SoapClient.call(api_method, args)
        end
      end
    end
  end
end

できました!実際にはじめのテストコードの通りに動いてくれました!

次回は

今回の記事ではRMS受注APIのRubyのラッパーを作成しました。次回は最後にひとはなの業務ロジックと今回実装したAPIとをつなぐサービス層を実装していきたいと思います。

おわりに

HitoHana(ひとはな)では、仕事が大好きで個性的なエンジニアを大募集しています!

www.wantedly.com

RMSのAPIをRubyで実装したい 1/3

発端

新人の田中です。HitoHanaは、楽天でもお店を出してます!
で、本家のECサイトと楽天の店舗とのデータのやりとりを手軽にやりたいってことで、RMS(Rakuten Marchant Service)のWEBAPIサービスを利用してみようってことになりました。
この記事では3回に分けてRMSの受注APIをRailsアプリケーションに導入した事例を紹介します。

イチから実装してみることに

APIサービスの利用は決まったんですが、Rubyで実装されてる参考ソースがなかなか見つからず、本家もJavaしか書いてないんでイチから実装してみることにしました。

完成形のイメージ

新人の僕がいきなり実装するとごっちゃごちゃになっちゃうので、まずは完成形をイメージします。
外部のシステム(RMS)と自社システム(HitoHana)をスムーズにつなげられるように緩衝材(Proxy)を用意することにしました。

  1. モデル層:HitoHanaのRailsアプリケーションのモデルです。基本ここはさわらない感じにしたい。

  2. サービス層:HitoHanaとRMSのAPI(ライブラリ層)をつなぐProxyです。HitoHanaアプリのモデルをこのProxyに渡してRMSのAPIをコントロールします。

  3. ライブラリ層:RMSのWEBAPIをRubyで使うためのモジュールです。

でもまずはとりあえずつなげよう

完成形のイメージはつくってみたけど、でもそもそもRMSのAPIを呼び出せないとお話にならないので、完成形のイメージはいったんほっといて、単純に楽天の注文の情報だけを取得するコードを書いてみます。
RMSの受注APIはSoapを使用しているのでSoap通信のgemとしてSavonを使いました。

github.com

require 'base64'
require 'savon'

# 初期設定
service_secret = 'WEB API契約時に配布されるサービスシークレット'
license_key    = 'WEB API契約時に配布されるライセンスキー(有効期限付き)'
shop_url       = '楽天の店舗URL'

# 受注APIのうち、試しにgetOrderを使うことにした
operation = :get_order
message = {
  # 認証系パラメタ
  # user_nameはタグとしては必須だが、入力不要らしい
  arg0: {
    auth_key: "ESA #{Base64.strict_encode64(service_secret + ':' + license_key)}",
    shop_url: shop_url,
    user_name: 'hitohana',
  },
  # 業務系パラメタ
  arg1: {
    is_order_number_only_flag: false,
    order_number: ['楽天の注文番号(やたら長い)'],
  },
}

# Savon client でRMSのAPIを呼び出す
wsdl = 'https://api.rms.rakuten.co.jp/es/1.0/order/ws?WSDL'
client = Savon.client(wsdl: wsdl)

# Debug用コード リクエストのxmlを確認できる
# client.operation(operation).build(message: message).pretty

response = client.call(operation, message: message)
response_body = response.body["#{operation}_response".to_sym][:return]

結果は以下のような感じ

{
    error_code: 'N00-000',
    message: '正常終了',
    order_model: '注文情報のHashインスタンス'
}

うまくいきました。
RubyでRMSのWEB APIを使用できるようになったので次回からは構造的にアプリケーションを作っていきます。
第2回はライブラリ層を実装していく予定です。

おわりに

HitoHana(ひとはな)では、仕事が大好きで個性的なエンジニアを大募集しています!

www.wantedly.com

Google Drive APIを使うときにファイルIDじゃなくてURLを引数にしておくとコミュニケーションがスムーズになるかもしれないというお話

雨の日も晴れの日も

エナジードリンクを飲みすぎて、エナジードリンクを飲んでいないときに気持ちが落ち込んでしまうエンジニアの新原です。

Google Drive

Google Drive、皆さん使っていますか?
僕らのような規模の小さい会社や団体に所属している方であれば、神のようなツールですよね。
今の時代、スケジュールやら仕様やら飲み会候補やら分析結果やら何から何までがGoogle Spread Sheetで飛んできます。

仕事をしているとGoogle Spread Sheetに作成された商品データをシステムに投入するというようなミッションがあったりするので、僕らみたいなエンジニアはRubyから参照できると便利ですよね。
そう、Google Drive REST APIとgemですね!!

developers.google.com

github.com

でも、ちょっと不便。。。

すごく便利なのですが、引数がファイルIDというところに少し不便を感じてしまいます。
エンジニア同士であればファイルIDをやりとりするのは簡単ですが、非エンジニアと会話するときに「Spread SheetのファイルIDを教えて!」と言っても伝わらないときがあります。
URL階層を説明したあとで「#ditの前のスラッシュまでがファイルIDなので、こちらを...」のような形になっちゃいますね。
非エンジニアからしたら「なんでそんなにめんどくさいことしなきゃいけないの?」「めんどくさジニア!」っていう感じですよね。
「ファイルのURLを教えて!」といえば一発です。めんどくさジニアにもならなくてすみます!

解決策

URLだけをやり取りすればいいので、こんな正規表現をかいてみました。
これをいい感じにどこかのメソッドに設置しておけば、あら不思議!

/^https:\/\/docs.google.com\/spreadsheets\/d\/(?<google_file_id>.+)\/edit/ =~ url

前まではファイルIDを指定してもらっていましたが、このようにURLを指定することでデータの投入ができるようになりました🖖

f:id:nyaahara:20180529150216p:plain

おわりに

HitoHana(ひとはな)では、仕事が大好きで個性的なエンジニアを大募集しています!

www.wantedly.com