Yandex Direct API用のRuby gemを作成します

私はRubyをもっと良く学びたかったのですが、実際のドラフトはありませんでした。 そして、Yandex Direct APIで動作するgemを作成しようとしました。







いくつかの理由がありました。 その中でも、Yandex Direct APIは、Yandexおよび最新のRESTサービス全般で非常に一般的です。 典型的な間違いを理解して克服すれば、他のYandex APIのアナログを簡単かつ迅速に書くことができます(それだけではありません)。 それでもなお、私が見つけたすべてのアナログは、Directのバージョンのサポートに問題がありました:いくつかは4の下でシャープにされ、他は新しい5の下でシャープにされ、ユニットはどこでもサポートしていませんでした。







メタプログラミングは素晴らしいことです



gemの主なアイデアは、RubyやPythonのような言語では新しいメソッドとJSONのようなオブジェクトをその場で作成できるため、RESTサービスにアクセスするためのインターフェースメソッドはRestサービス自体の機能を繰り返すことができるということです。 次のように書くことができます:







request = { "SelectionCriteria" => { "Types" => ["TEXT_CAMPAIGN"] }, "FieldNames" => ["Id", "Name"], "TextCampaignFieldNames" => ["BiddingStrategy"] } options = { token: Token } @direct = Ya::API::Direct::Client.new(options) json = direct.campaigns.get(request)
      
      





そして、ヘルプを書く代わりに、指定されたAPIを使用してユーザーをマニュアルに送ります。







たとえば、次のような古いバージョンのメソッドを呼び出します。







 json = direct.v4.GetCampaignsList
      
      





読むことに興味はないが、試してみたい場合は、ここから完成した宝石を入手できます:









twitterの例から、レールから omn​​iauth-tokenを取得する方法を学ぶことができます。 また、メソッドの名前と登録手順は、Yandexドキュメントで非常に詳しく説明されています







詳細が興味深い場合-さらに詳細です。







開発を開始



もちろん、この記事では最も基本的な経験と最も簡単なことについて説明しています。 しかし、それは典型的な宝石を作成するためのリマインダーとして、初心者(私のような)に役立ちます。 もちろん、記事に関する情報を収集することは興味深いですが、長い間です。







最後に、読者の中には、Yandex Direct APIサポートをプロジェクトにすばやく追加する必要がある場合があります。







また、フィードバックの観点から、私にとっても役立つでしょう。







テストスクリプト



まず、Yandex Directに登録し、そこでテストアプリケーションを作成し、そのための一時トークンを取得します。







次に、 Yandex Direct APIヘルプを開き、メソッドを呼び出す方法を学びます。 このようなもの:







バージョン5の場合:







 require "net/http" require "openssl" require "json" Token = "TOKEN" #    TOKEN. def send_api_request_v5(request_data) url = "https://%{api}.direct.yandex.com/json/v5/%{service}" % request_data uri = URI.parse(url) request = Net::HTTP::Post.new(uri.path, initheader = { 'Client-Login' => request_data[:login], 'Accept-Language' => "ru", 'Authorization' => "Bearer #{Token}" }) request.body = { "method" => request_data[:method], "params" => request_data[:params] }.to_json http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE response = http.request(request) if response.kind_of? Net::HTTPSuccess JSON.parse response.body else raise response.inspect end end p send_api_request_v5 api: "api-sandbox", login: "YOUR LOGIN HERE", service: "campaigns", method: "get", params: { "SelectionCriteria" => { "Types" => ["TEXT_CAMPAIGN"] }, "FieldNames" => ["Id", "Name"], "TextCampaignFieldNames" => ["BiddingStrategy"] }
      
      





バージョン4 Liveの場合(トークンは両方に適しています):







 require "net/http" require "openssl" require "json" Token = "TOKEN" #    TOKEN. def send_api_request_v4(request_data) url = "https://%{api}.direct.yandex.com/%{version}/json/" % request_data uri = URI.parse(url) request = Net::HTTP::Post.new(uri.path) request.body = { "method" => request_data[:method], "param" => request_data[:params], "locale" => "ru", "token" => Token }.to_json http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE response = http.request(request) if response.kind_of? Net::HTTPSuccess JSON.parse(response.body) else raise response.inspect end end p send_api_request_v4 api: "api-sandbox", version: "live/v4", method: "GetCampaignsList", params: []
      
      





これらのスクリプトは、デバッグおよびクイックテストクエリに既に適しています。







しかし、(神話上の)マン・マンスが教えているように、それ自体のスクリプトと他の人のライブラリは、2つの異なるクラスのアプリケーションです。 そして、1つを別のものに渡すには、汗をかかなければなりません。







宝石を作成する



そもそも、名前を決定する必要がありました-シンプルで忙しくありません。 そして、 ya-api-directが必要なものであるという結論に達しました。







まず、構造自体が論理的です。たとえば、ya-api-weatherも表示される場合、それが何を指しているかが明確になります。 第二に、商標をプレフィックスとして使用するYandexの公式製品がまだありません。 さらに、これはya.ruのヒントであり、以前の簡潔なデザインが注意深く保存されています。







すべてのフォルダーを手で作成するのは少し面倒です。 バンドラーにこれをさせてください:







 bundle gem ya-api-direct
      
      





UnitTestのツールとして、ミニテストを示しました。 次に、理由が明らかになります。







これでフォルダーが作成され、その中にアセンブリgemの準備が整いました。 唯一の欠点は、完全に空であることです。







しかし、今それを修正します。







テストを書く



UnitTestは、unningな隠れたバグを検出するのに非常に役立ちます。 それにもかかわらず、ソースコードに隠れていた方法に沿ってそれらを書き、数十個のバグを修正することができたほとんどすべてのプログラマーは、彼が常にそれらを書くことを約束します。 しかし、彼はまだ書きません。







一部のプロジェクト(特に怠け者のプログラマーによって書かれたもの)は、テストと仕様テストの両方を同時に行います。 しかし、minitestの最新バージョンでは、突然仕様インターフェイスを学習し、仕様だけで対処することにしました。







オンラインインターフェースがあり、さらに、リクエストごとにポイントが差し引かれるので、Yandex Direct APIからの回答を偽造します。 これを行うには、トリッキーなgem webmockが必要です







宝石に追加







 group :test do gem 'rspec', '>= 2.14' gem 'rubocop', '>= 0.37' gem 'webmock' end
      
      





更新し、テストフォルダーの名前をspecに変更します。 急いでいたので、外部インターフェイスのみのテストを書きました。







 require 'ya/api/direct' require 'minitest/autorun' require 'webmock/minitest' describe Ya::API::Direct::Client do Token = "TOKEN" #  , .. API   . before do @units = { just_used: 10, units_left: 20828, units_limit: 64000 } units_header = {"Units" => "%{just_used}/%{units_left}/%{units_limit}" % @units } @campaigns_get_body = { #     Yandex Direct API    } #    stub_request(:post, "https://api-sandbox.direct.yandex.ru/json/v5/campaigns") .with( headers: {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Accept-Language'=>'en', 'Authorization'=>'Bearer TOKEN', 'Client-Login'=>'', 'User-Agent'=>'Ruby'}, body: {"method" => "get", "params"=> {}}.to_json) .to_return(:status => 200, body: @campaigns_get_body.to_json, headers: units_header) #     @clientV4 = Ya::API::Direct::Client.new(token: Token, api: :v4) @clientV5 = Ya::API::Direct::Client.new(token: Token) end
      
      





webmockは、HTTPを操作するための標準ライブラリのメソッドを置き換えます。これにより、特定のボディとヘッダーを持つリクエストが返されたときに、対応するレスポンスが返されます。







設定を間違えたとしても、大したことではありません。 フィルターにないリクエストを送信しようとすると、webmockはエラーを報告し、スタブを正しく書く方法を教えてくれます。







そして、仕様を書きます:







 describe "when does a request" do it "works well with version 4" do assert @clientV4.v4.GetCampaignsList == @campaigns_get_body end it "works well with version 5" do assert @clientV5.campaigns.get == @campaigns_get_body end end #   
      
      





すくい



Rakeは非常に柔軟かつ簡単に実装されているため、ほぼすべてのライブラリに独自の方法があります。 だから私は彼にspec _ *。Rbと呼ばれるすべてのファイルを実行するように彼に言った、それはspecディレクトリにある:







 require "bundler/gem_tasks" require "rake/testtask" task :spec do Dir.glob('./spec/**/spec_*.rb').each { |file| require file} end task test: [:spec] task default: [:spec]
      
      





これで、仕様を次のように呼び出すことができます。







 rake test
      
      





または:







 rake
      
      





確かに、彼はまだテストするものがありません。







宝石を書く



まず、gemに関する情報を入力します(このバンドルがないと、起動が拒否されます)。 次に、使用するサードパーティライブラリをgemspecに書き込みます。







 gem 'jruby-openssl', platforms: :jruby gem 'rake' gem 'yard' group :test do gem 'rspec', '>= 2.14' gem 'rubocop', '>= 0.37' gem 'webmock' gem 'yardstick' end
      
      





やる







 bundle install
      
      





libに移動してファイルを作成します。







用意するファイルは次のとおりです。









異なるバージョンのコントローラー



最初に、定数を含むファイルを作成します。このファイルに、APIのすべての関数を書き込みます。







contants.rb







 module Ya module API module Direct API_V5 = { "Campaigns" => [ "add", "update", "delete", "suspend", "resume", "archive", "unarchive", "get" ], #  .. } API_V4 = [ "GetBalance", #  .. ] API_V4_LIVE = [ "CreateOrUpdateCampaign", #  .. ] end end end
      
      





次に、バージョン4および5のサービスを継承する基本的なサービスラッパーを作成します。







direct_service_base.rb







 module Ya::API::Direct class DirectServiceBase attr_reader :method_items, :version def initialize(client, methods_data) @client = client @method_items = methods_data init_methods end protected def init_methods @method_items.each do |method| self.class.send :define_method, method do |params = {}| result = exec_request(method, params || {}) callback_by_result result result[:data] end end end def exec_request(method, request_body) client.gateway.request method, request_body, @version end def callback_by_result(result={}) end end end
      
      





コンストラクターで、彼はソースクライアントとメソッドのリストを取得します。 そして、define_methodを介して、それらを作成します。







respond_to_missingメソッドでうまくいかないのはなぜですか? (多くの宝石がまだそうであるように)? 遅いのであまり便利ではないからです。 そして、それなしでは、遅いインタープリターが例外の後にそれに入り、is_respond_to_missing?..をチェックインします。さらに、この方法で作成されたメソッドはメソッド呼び出しの結果に分類され、デバッグに便利です。







次に、バージョン4および4 Liveのサービスを作成します。







direct_service_v4.rb







 require "ya/api/direct/constants" require "ya/api/direct/direct_service_base" module Ya::API::Direct class DirectServiceV4 < DirectServiceBase def initialize(client, methods_data, version = :v4) super(client, methods_data) @version = version end def exec_request(method, request_body = {}) @client.gateway.request method, request_body, nil, (API_V4_LIVE.include?(method) ? :v4live : @version) end end end
      
      





バージョン5では、サーバーはユーザーリクエストに応答するだけでなく、最後のリクエストに費やされたポイント数、残っているポイント数、現在のセッションにあったポイント数も報告します。 私たちのサービスはそれらを分解できるはずです(しかし、これをどのように行うかについてはまだ書いていません)。 ただし、メインクライアントクラスのフィールドを更新する必要があることを事前に示します。







direct_service_v5.rb







 require "ya/api/direct/direct_service_base" module Ya::API::Direct class DirectServiceV5 < DirectServiceBase attr_reader :service, :service_url def initialize(client, service, methods_data) super(client, methods_data) @service = service @service_url = service.downcase @version = :v5 end def exec_request(method, request_body={}) @client.gateway.request method, request_body, @service_url, @version end def callback_by_result(result={}) if result.has_key? :units_data @client.update_units_data result[:units_data] end end end end
      
      





ところで、謎めいたゲートウェイがリクエストの呼び出しに関与していることに気づきましたか?







ゲートウェイとUrlHelper



Gatewayクラスはリクエストを提供します。 スクリプトのほとんどのコードはそこに移動しました。

gateway.rb







 require "net/http" require "openssl" require "json" require "ya/api/direct/constants" require "ya/api/direct/url_helper" module Ya::API::Direct class Gateway #    def request(method, params, service = "", version = nil) ver = version || (service.nil? ? :v4 : :v5) url = UrlHelper.direct_api_url @config[:mode], ver, service header = generate_header ver body = generate_body method, params, ver uri = URI.parse url request = Net::HTTP::Post.new(uri.path, initheader = header) request.body = body.to_json http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = @config[:ssl] ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE response = http.request(request) if response.kind_of? Net::HTTPSuccess UrlHelper.parse_data response, ver else raise response.inspect end end #     generate_header  generate_body #    ,   end end
      
      





標準のNet :: HTTPは、熊手のように単純であるため、関係しています。 ファラデーからクエリを送信することもできます。 OmniAuthは既に動作します(これについては以下で説明します)ので、アプリケーションがgemでオーバーロードされることはありません。







最後に、URLを生成し、データを解析し、ユニットを解析する静的関数をUrlHelperに入力します(簡単です)。







 require "json" require "ya/api/direct/exception" module Ya::API::Direct RegExUnits = Regexp.new /(\d+)\/(\d+)\/(\d+)/ class UrlHelper def self.direct_api_url(mode = :sandbox, version = :v5, service = "") format = :json protocol = "https" api_prefixes = { sandbox: "api-sandbox", production: "api" } api_prefix = api_prefixes[mode || :sandbox] site = "%{api}.direct.yandex.ru" % {api: api_prefix} api_urls = { v4: { json: '%{protocol}://%{site}/v4/json', soap: '%{protocol}://%{site}/v4/soap', wsdl: '%{protocol}://%{site}/v4/wsdl', }, v4live: { json: '%{protocol}://%{site}/live/v4/json', soap: '%{protocol}://%{site}/live/v4/soap', wsdl: '%{protocol}://%{site}/live/v4/wsdl', }, v5: { json: '%{protocol}://%{site}/json/v5/%{service}', soap: '%{protocol}://%{site}/v5/%{service}', wsdl: '%{protocol}://%{site}/v5/%{service}?wsdl', } } api_urls[version][format] % { protocol: protocol, site: site, service: service } end def self.extract_response_units(response_header) matched = RegExUnits.match response_header["Units"] matched.nil? ? {} : { just_used: matched[1].to_i, units_left: matched[2].to_i, units_limit: matched[3].to_i } end private def self.parse_data(response, ver) response_body = JSON.parse(response.body) validate_response! response_body result = { data: response_body } if [:v5].include? ver result.merge!({ units_data: self.extract_response_units(response) }) end result end def self.validate_response!(response_body) if response_body.has_key? 'error' response_error = response_body['error'] raise Exception.new(response_error['error_detail'], response_error['error_string'], response_error['error_code']) end end end end
      
      





サーバーがエラーを返した場合、そのテキストとともに例外をスローします。







コードは自明のようで、それはかなり良いことです。 自明のコードは保守が簡単です。







クライアント



次に、外部インターフェイスとやり取りするクライアントクラス自体を記述する必要があります。 ほとんどの機能は既に内部クラスに移動しているため、非常に短くなります。







 require "ya/api/direct/constants" require "ya/api/direct/gateway" require "ya/api/direct/direct_service_v4" require "ya/api/direct/direct_service_v5" require "ya/api/direct/exception" require 'time' module Ya::API::Direct AllowedAPIVersions = [:v5, :v4] class Client attr_reader :cache_timestamp, :units_data, :gateway, :v4, :v5 def initialize(config = {}) @config = { token: nil, app_id: nil, login: '', locale: 'en', mode: :sandbox, format: :json, cache: true, api: :v5, ssl: true }.merge(config) @units_data = { just_used: nil, units_left: nil, units_limit: nil } raise "Token can't be empty" if @config[:token].nil? raise "Allowed Yandex Direct API versions are #{AllowedVersions}" unless AllowedAPIVersions.include? @config[:api] @gateway = Ya::API::Direct::Gateway.new @config init_v4 init_v5 start_cache! if @config[:cache] yield self if block_given? end def update_units_data(units_data = {}) @units_data.merge! units_data end def start_cache! case @config[:api] when :v4 result = @gateway.request("GetChanges", {}, nil, :v4live) timestamp = result[:data]['data']['Timestamp'] when :v5 result = @gateway.request("checkDictionaries", {}, "changes", :v5) timestamp = result[:data]['result']['Timestamp'] update_units_data result[:units_data] end @cache_timestamp = Time.parse(timestamp) @cache_timestamp end private def init_v4 @v4 = DirectServiceV4.new self, (API_V4 + API_V4_LIVE) end def init_v5 @v5 = {} API_V5.each do |service, methods| service_item = DirectServiceV5.new(self, service, methods) service_key = service_item.service_url @v5[service_key] = service_item self.class.send :define_method, service_key do @v5[service_key] end end end end end
      
      





バージョン4のメソッドはv4プロパティに書き込まれ、バージョン5のメソッドは個別のサービスによってグループ化され、既にわかっている構築を通じてクライアントクラスのメソッドになります。 さて、client.campaigns.getを呼び出すと、Rubyは最初にclient.campaigns()を実行してから、結果のサービスでgetメソッドを呼び出します。







クラスをdo ... end構文で使用できるように、コンストラクターの最後の用語が必要です。







初期化の直後に、(設定で指定されている場合)start_cache!を実行して、キャッシュを有効にするコマンドをAPIに送信します。 設定のバージョンはこれにのみ影響し、クラスインスタンスからは両方のバージョンのメソッドを呼び出すことができます。 受信した日付はcache_timestampプロパティで利用可能になります。







units_dataプロパティには、Unitsの最新情報が含まれます。







また、プロジェクトには、バージョン設定と例外を含むクラスがあります。 彼らにはすべてがはっきりしているので、言うことはありません。 バージョン設定を持つクラスは、プロジェクトとともにバンドルによって生成されます。







さて、 direct.rbファイルでは、外部からユーザーに表示されるクラスを指定する必要があります。 私たちの場合、これはクライアントクラスのみです。 さらに、バージョンと例外(完全に公式です)。







コンパイルして塗りつぶす



gemをコンパイルするには、RubyGems.orgマニュアルに従ってください(複雑なことは何もありません)。 またはRailsからMountable Engineを適用します。







そして、rubygemsにアップロードします-突然、このgemは私たちだけでなく役に立つかもしれません。







Ruby on Railsからトークンを取得する方法



RailsからYandec APIにログインしてトークンを取得することは、開発者にとって非常に簡単なことです...初めてではありません。







既に学んだように、Direct APIにアクセスするにはトークンが必要です。 Yandexヘルプから 、TwitterやFacebookを含む多くのサービスで使用される古き良きOAuth2が私たちの前にあることがわかります。







Rubyには、さまざまなサービスのOAuth2実装が継承する古典的なgem omn​​iauthがあります。 すでに実装され、 omn​​iauth-yandex 。 私たちは彼に対処しようとします。







新しいRailsアプリケーションを作成しましょう(学習した後、作業中のプロジェクトに追加します)。 Gemfileに追加:







 gem "omniauth-yandex"
      
      





そして、バンドルインストールを行います。







そして、レール用のOmniauth認証をインストールするためのマニュアルを使用します。 twitterの例を次に示します。 それを翻訳し、語り直すことは、それだけの価値はないと思います-記事は巨大であることが判明しました。







この記事で説明されている例は、私にとってはうまくいきました。 唯一の修正は、SQLiteがそれらをサポートしないため、Userテーブルに追加のインデックスを書き込まなかったことです。







確かに、この記事はトークンが隠れている場所を示していません。 しかし、これはまったく秘密ではありません。 SessionControllerで取得できます







  request.env['omniauth.auth'].credentials.token
      
      





覚えておいてください-そのような認証はすべてトークンを再生成します。 そして、後で直接トークンを使用してスクリプトを使用しようとすると、サーバーは古いスクリプトはもはや適切ではないと言います。 Yandexアプリケーションの設定に戻り、デバッグコールバックURL(__ https://oauth.yandex.ru/verification_code__ )を再度指定してから、トークンを再生成する必要があります。







さらに良いことに、デバッグに干渉しないように、静的トークン用に別のアプリケーションを作成します。







参照資料






All Articles