LoginSignup
5
5

More than 5 years have passed since last update.

Rubyで爆速API (Twitter, Slack, Git, Facebook)

Last updated at Posted at 2016-09-24

CSVからAPIクライアントをつくります

参考

謝辞
- メタプログラミングの勉強になりました。
- いろいろなAPIを復習できたり、本当にありがとうございます。

使用したメタプログラミング

  • 動的ディスパッチ
  • デリゲート
  • メソッドミッシング
  • オープンクラス
  • オーバーライド

結果として、CSVファイルはDSLとして機能します
CSVがコードとして機能し、そのコード自体がAPIドキュメントの役割を果たします

実装

APIの仕様を、CSVに定義する

こちらのAPI仕様を参考にしました

このようにファイルをつくります。
これらのファイルを後述するプログラムがAPIの仕様として認識し、APIクライアントを生成することになります

git
get,/repos/:owner/:repo/issues/:number,[]
get,/repos/:owner/:repo/events,[]
slack
get,channels.list,[]
get,users.getPresence,[]

twitter
get,friends/list.json,[]
facebook
get,:user_id/friendlists,[]
get,:user_id/photos,[]
get,:user_id,[fields]

補足

  • ファイル名はクラス名のダウンケース
  • 本当はもっといっぱいAPIのエンドポイントがあるのですが、時間の関係で省略しました。
    • このリストをつくるためのクローラを実装中なので、のちほど記事にしたいと思います。

APIごとのエンドポイントを設定

エンドポイントと、アクセストークンなどのデフォルトパラメータはここで打ち込みます。
アクセストークンなどは、こちらから取得できます。bashなどに設定してください

clients.rb
class Git < Api::Client
  def endpoint;       'https://api.github.com/'; end
  def default_params; { access_token: ENV['GIT_TOKEN'] }; end
end

class Slack < Api::Client
  def endpoint;       'https://slack.com/api/'; end
  def default_params; { token: ENV['SLACK_TOKEN'] }; end
end

class Twitter < Api::Client
  def endpoint;       'https://api.twitter.com/1.1/'; end
  def default_params;
    # credentialsというキーにはOAuth認証を使うための特別な意味をもたせました
    {
      credentials: {
        :consumer_key    => ENV['TWITTER_COUSUMER_KEY'],
        :consumer_secret => ENV['TWITTER_CONSUMER_SECRET'],
        :token           => ENV['TWITTER_TOKEN'],
        :token_secret    => ENV['TWITTER_TOKEN_SECRET'],
      }
    }
  end
end

class Facebook < Api::Client
  def endpoint;       'https://graph.facebook.com/v2.7/'; end
  def default_params;
    { 
      :access_token    => ENV['FACEBOOK_TOKEN']
    }
  end
end

補足

  • Twitterに関してはツイートするためにOAuth認証が必要だったので、
  • credentialsというパラメータを設定するとOAuth認証に切り替わるようにしました

APIクライアントを動的に定義

FaradayというAPIクライアントのGemを使っています。

他にも色々gemがはいっていますので、下記のコマンドでインストールしてください

コンソール(黒い画面)
gem install faraday
gem install faraday_middleware
gem install simple_oauth
gem install oauth
api.rb
require 'open-uri'
require 'json'
require 'faraday'
require 'faraday_middleware'
require 'simple_oauth'
require 'active_support/core_ext/class/subclasses'
require 'csv'
require 'OAuth'

class Hash
  # Hashにメソッドチェーンっぽくアクセスしたいので書きました
  def method_missing(name)
    return self[name].extend Hash if key? name
    self.each { |k, v| return v if k.to_s.to_sym == name }
    super.method_missing(name)
  end
end

module Api
  class Client
    # api一覧が入る配列
    attr_accessor :apis
    # APIの定義を構造体にまとめます
    SPEC = Struct.new(:request_type, :path, :params) do
      def method_name
        path.gsub(/\.json$/,'').split(/\W/).grep(/\S/).join('_')
      end

      # gitのURLの:repoとか:ownerをパラメータとして取り出す
      def path_params
        path.split('/').select{ |s| s[0] == ':' }.map { |s| s.delete(s[0]) }
      end

      # パラメータを使ってURLを置換する
      def absolute_url(optional_params)
        path_params.each do |path_param|
          params_value = (optional_params[path_param] || optional_params[path_param.to_sym]).to_s
          if params_value.nil?
            puts "Params Not Found: #{path_param}"
          else
            path.gsub!(':' + path_param, params_value)
            params.delete(path_param.to_s)
          end
        end
        path
      end
    end

    # APIメソッドの中身
    def define_api(api, params)
      puts "Params require: #{api.path_params}\nParams optional: #{params}"
      puts "#{api.request_type} #{api.absolute_url(params)}"
      send(api.request_type, api.absolute_url(params), params)
    end

    # このクラスがSlackとかTwitterに継承された時にフックされる(klassはサブクラスのSlackとかTwitterクラス)
    def self.inherited(klass)
      # Slack => slackにダウンケースして、Api.slackというメソッドでSlack.newされるようにエイリアス
      klass_name = klass.to_s.downcase
      Api.define_singleton_method klass_name do
        _sub_class_object_ = klass.new
        # _sub_class_object_に対して、CSVを読み込み定義されたAPIをメソッドにする
        api_list = CSV.read("#{Dir.pwd}/#{klass_name}")
        api_list.map{ |m| SPEC.new(*m) }.each do |api|
          # 使えるAPIメソッド名がApi.slack.apisで表示するため挿入
          _sub_class_object_.apis << api.method_name
          # サブクラスのインスタンスにAPIメソッドを定義する
          klass.send(:define_method, api.method_name) do |params = {}|
            define_api(api, params)
          end
        end
        _sub_class_object_
      end
    end

    # 主にFaradayの初期化
    def initialize(endpoint = endpoint())
      @apis = []
      @conn = Faraday.new(:url => endpoint) do |faraday|
        request = default_params.key?(:credentials) ? [:oauth, default_params[:credentials]] : [:url_encoded]
        faraday.request  *request
        faraday.adapter  Faraday.default_adapter
      end
    end

    # リクエストをさばく
    def request(method, url, ops = {})
      ops = default_params.merge!(ops)
      res = @conn.send(method, url) do |req|
        req.params = ops
        req.headers['Content-Type'] = 'application/json'
        req.options.timeout = 5
        req.options.open_timeout = 5
      end
      json_to_hash res.body
    rescue => e
      res || e
    end

    def get(url, ops = {});    request(:get, url, ops);    end
    def post(url, ops = {});   request(:post, url, ops);   end
    def put(url, ops = {});    request(:put, url, ops);    end
    def patch(url, ops = {});  request(:patch, url, ops);  end
    def delete(url, ops = {}); request(:delete, url, ops); end
    def json_to_hash(json_str); JSON.parse(json_str); end
  end
end

使い方

# それぞれのAPIクライアントのインスタンス作成
Api.slack
Api.git
Api.twitter
Api.facebook
Api.slack.apis # Slackでリクエストできるメソッドを表示
=> ["channels_list", "users_getPresence"]

# Slackにのチャンネル一覧を取得
Api.slack.channels_list => "レスポンスはハッシュ"

コンソールで実行

Api.slack
=> #<Slack:0x007ffa4308f468 @apis=["channels_list", "users_getPresence"], @conn=#<Faraday::Connection:0x007ffa4308f0a8 @parallel_manager=nil, @headers={"User-Agent"=>"Faraday v0.9.2"}, @params={}, @options=#<Faraday::RequestOptions (empty)>, @ssl=#<Faraday::SSLOptions (empty)>, @default_parallel_manager=nil, @builder=#<Faraday::RackBuilder:0x007ffa4308eb58 @handlers=[Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]>, @url_prefix=#<URI::HTTPS https://slack.com/api/>, @proxy=nil>>

Api.slack.channels_list
get channels.list
=> {"ok"=>true, "channels"=>[{"id"=>"C29GZBRML", "name"=>"general", "is_channel"=>true, "created"=>1473343855, "creator"=>"U29GZBJFQ", "is_archived"=>false, "is_general"=>true, "is_member"=>true, "members"=>["U29GZBJFQ", "U29RJ3P7G"], "topic"=>{"value"=>"Company-wide announcements and work-based matters", "creator"=>"", "last_set"=>0}, "purpose"=>{"value"=>"This channel is for team-wide communication and announcements. All team members are in this channel.", "creator"=>"", "last_set"=>0}, "num_members"=>2}, {"id"=>"C29HG89DY", "name"=>"random", "is_channel"=>true, "created"=>1473343855, "creator"=>"U29GZBJFQ", "is_archived"=>false, "is_general"=>false, "is_member"=>true, "members"=>["U29GZBJFQ", "U29RJ3P7G"], "topic"=>{"value"=>"Non-work banter and water cooler conversation", "creator"=>"", "last_set"=>0}, "purpose"=>{"value"=>"A place for non-work-related flimflam, faffing, hodge-podge or jibber-jabber you'd prefer to keep out of more focused work-related channels.", "creator"=>"", "last_set"=>0}, "num_members"=>2}]}
Api.slack.channels_list.channels.first.members
=> ["U29GZBJFQ", "U29RJ3P7G"]

Api.slack.users_getPresence(user_id: "U29GZBJFQ")
=> {"ok"=>true, "presence"=>"active", "online"=>true, "auto_away"=>false, "manual_away"=>false, "connection_count"=>1, "last_activity"=>1474713399}

Futeare work

  • APIの仕様変更をフックして、
  • CSVのAPI定義が常に最新になれば便利
  • そのためのクローラをつくろうとおもいます
5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5