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定義が常に最新になれば便利
- そのためのクローラをつくろうとおもいます