omniauth(と,少しomniauth-oauth2) を読む
前にも一回読んだけどあんまり理解できなかった.最近RackとかOAuthについてわかりを得てきたのでもう一度読んでみようと思った.気づいたら rails server
command を読んでいたりして(rails s を読む)少し遠回り気味だったけど,OAuth2.0の認証フローに合わせてomniauthの挙動をたどってみた.
Rackに関してはRackとは何か - Qiitaが,OAuthについては色々な OAuth のフローと doorkeeper gem での実装 - Qiitaの認可コードの項がとても参考になった.
Client Application で /app/:provider にアクセスがあると,omniauth が Rack middleware としてこのリクエストを拾い,認可フローが開始される.
middleware の使用は config/initializers/omniauth.rb
で宣言する.
Rails.application.config.middleware.use OmniAuth::Builder do provider :developer unless Rails.env.production? provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET'] end
Rails.application.config.middleware.use
の引数に OmniAuth::Builder
を指定することで,Blockの中身を Rack::Builder
ではなく OmniAuth::Builder
の DSL として処理してる(多分).
このブロック中の provider
は, Rack::Builder
をラップしている OmniAuth::Builder
で提供されてる.
def provider(klass, *args, &block) # ... begin middleware = OmniAuth::Strategies.const_get(OmniAuth::Utils.camelize(klass.to_s).to_s) rescue NameError raise(LoadError.new("Could not find matching strategy for #{klass.inspect}. You may need to install an additional gem (such as omniauth-#{klass}).")) end args.last.is_a?(Hash) ? args.push(options.merge(args.pop)) : args.push(options) use middleware, *args, &block end
例えば
provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
みたいに書くと,
twitter strategy を探しにいって,あれば use
してmiddleware の stack につんでくれる.
omniauth では, omniauth-twitter など,omniauth にのせる OAuth 用の middleware を strategy とよぶ.strategyを作る時は, OmniAuth::Strategies
namespace 以下に定義することになっていて,これによって OmniAuth::Strategies.const_get
で持ってこれるようになってる.
omniauth には 開発時の placeholder としてシンプルな developer strategy が用意されている.
strategy の本体 OminAuth::Strategy
は omniauth/omniauth.rbで autoload されてる.Rack middleware なのでおもむろに call
を見に行く.Omniauth::Strategy#call!
def call!(env) # rubocop:disable CyclomaticComplexity, PerceivedComplexity # ... @env = env @env['omniauth.strategy'] = self if on_auth_path? return mock_call!(env) if OmniAuth.config.test_mode return options_call if on_auth_path? && options_request? return request_call if on_request_path? && OmniAuth.config.allowed_request_methods.include?(request.request_method.downcase.to_sym) return callback_call if on_callback_path? return other_phase if respond_to?(:other_phase) @app.call(env) end
色々やってるけどOAuthの本筋部分をたどると,
return request_call if on_request_path? && OmniAuth.config.allowed_request_methods.include?
細かい条件は省くけど /auth/twitter
みたいなrequestが来た時, request_call
が呼ばれて Rack::Response
Object が返される.これが認可サーバの 「callback_phase
(後述)がオーバーライドされてた.omniauth-oauth2/oauth2.rb
def callback_phase # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength, PerceivedComplexity error = request.params["error_reason"] || request.params["error"] if error #... else self.access_token = build_access_token self.access_token = access_token.refresh! if access_token.expired? super end # ... end
build_access_token
で認可サーバにアクセストークンを要求してる
def build_access_token verifier = request.params["code"] client.auth_code.get_token(verifier, {:redirect_uri => callback_url}.merge(token_params.to_hash(:symbolize_keys => true)), deep_symbolize(options.auth_token_params)) end
callback_phase
は, callback path に返って来た時に callback_call
がよばれて,その中で呼ばれてる.
def callback_call # ... callback_phase end
super
で呼ばれてる OmniAuth::Strategy#callback_pase
はここ
def callback_phase env['omniauth.auth'] = auth_hash call_app! end
env['omniauth.auth']
に認可サーバからの情報をいれて Client Application を call して auth/twitter/callback
みたいなところに戻ってくる.
Client Applocation では, SessionController
とかで
class SessionsController < ApplicationController def create @user = User.find_or_create_from_auth_hash(auth_hash) self.current_user = @user redirect_to '/' end protected def auth_hash request.env['omniauth.auth'] end end
みたいにして,access_token とか access_token_secret をもった User record を find_or_create する.
これで, 取得した tokenを使ってリソースサーバにリクエストが送れるようになる 🎉
rails s を読む
Omniauth読むはずが,Rackを理解したい気持ちになって(n回目)気づいたら Rails c
command を読んでた.
Guides rails/initialization.md
When one types a Rails command, invoke tries to lookup a command for the given namespace and executes the command if found.
railsコマンドを実行すると rails/command.rb がコマンド探して実行してくれる.
<command_namespace>_command.rb
の <command_namespace>Command#perform
を呼ぶっぽい.
def perform set_application_directory! prepare_restart Rails::Server.new(server_options).tap do |server| # Require application after server sets environment to propagate # the --environment option. require APP_PATH Dir.chdir(Rails.application.root) server.start end end
Rails::Server.new
して #start
を呼んでるだけ.
set_application_directory!
は config.ru
がないディレクトリでもよしなに動くようにしてる.rails/actions.rb
Rails::Server
のコンストラクタrails/server_command.rb で,PortやHostなどの設定をして(server_options
),それを使って Rack::Server
を初期化してる.
RailsServer#start
def start print_boot_information trap(:INT) { exit } create_tmp_directories setup_dev_caching log_to_stdout if options[:log_stdout] super ensure # The '-h' option calls exit before @options is set. # If we call 'options' with it unset, we get double help banners. puts "Exiting" unless @options && options[:daemonize] end
cacheの設定などして,Rack::Server#start
してる.
よだん
-p 3001
みたいな option は ServerCommand
Classが継承している Base
Class で Thor を継承することで実現してる.(rails/base.rb)
rubicure
最近ちまちまこっそりコードリーディングをしてる. 自分が普段かかないような構文がみられるのは楽しい.
今回は特に目的を決めないで目についた面白そうなところから読んでみる
rubicure
All about Japanese battle heroine "Pretty Cure (Precure)".
プリキュアのruby実装. ちなみにたこはプリキュアは無印しかしらない.
よんでみる
執筆時点の最新v1.0.4をよむ.
とりあえずreadmeの一番頭を見てみる
Precure.title #=> "ふたりはプリキュア" Precure.unmarked.title #=> "ふたりはプリキュア" Precure.max_heart.title #=> "ふたりはプリキュア Max Heart" # ...
title
メソッドとかは,yamlファイルにかかれたハッシュから引かれてるみたい.(というかキーそれぞれにメソッド生やしてたら必要以上に冗長になりそう)この辺のメソッドを定義してる部分から読んでみる.
unmarked: &unmarked series_name: unmarked title: ふたりはプリキュア started_date: 2004-02-01 ended_date: 2005-01-30 girls: - cure_black - cure_white futari_wa_pretty_cure: <<: *unmarked # ...
Precure
module は method_missing
がオーバーライトされてた.
module Precure def self.method_missing(name, *args, &block) Rubicure.core.send(name, *args, &block) end end
Rubicure.core
で method を実行させているけど,ここでも method_missing
が上書きされていて, Rubicure::Series#find
でハッシュの中身をもってきてる
def method_missing(name, *args) unmarked_precure = Rubicure::Series.find(:unmarked) if Rubicure::Series.valid?(name) Rubicure::Series.find(name) elsif unmarked_precure.respond_to?(name) unmarked_precure.send(name, *args) else super end end
https://github.com/sue445/rubicure/blob/v1.0.4/lib/rubicure/core.rb#L10-L20
config
で yaml をロードしたハッシュを持ってきてる.
https://github.com/sue445/rubicure/blob/master/lib/rubicure/series.rb#L84-L91
def find(series_name) raise UnknownSeriesError, "unknown series: #{series_name}" unless valid?(series_name) @cache ||= {} unless @cache[series_name] series_config = config[series_name] || {} series_config.reject! { |_k, v| v.nil? } @cache[series_name] = Rubicure::Series[series_config] end @cache[series_name] end
method missing のオーバーライドはびっくりした
Cure
もyamlからデータを引っぱってくるところまでは Series
とほぼほぼ同じ
Cure.lemonade #=> {:girl_name=>"cure_lemonade", :human_name=>"春日野うらら", :precure_name=>"キュアレモネード", :cast_name=>"伊瀬茉莉也", :created_date=>Sun, 18 Feb 2007, :color=>"yellow", :transform_message=>"プリキュア!メタモルフォーゼ!\nはじけるレモンの香り、キュアレモネード!\n希望の力と未来の光!\n華麗に羽ばたく5つの心!\nYes!プリキュア5!", :extra_names=>nil, :attack_messages=>["輝く乙女のはじける力、受けてみなさい!\nプリキュア!プリズム・チェーン!"], :transform_calls=>["metamorphose"]} Cure.pine #=> {:girl_name=>"cure_pine", :human_name=>"山吹祈里", :precure_name=>"キュアパイン", :cast_name=>"中川亜紀子", :created_date=>Sun, 15 Feb 2009, :color=>"yellow", :transform_message=>"チェインジ!プリキュア・ビートアップ!\nイエローハートは祈りのしるし!\nとれたてフレッシュ、キュアパイン!\nレッツプリキュア!", :extra_names=>["キュアエンジェルパイン"], :attack_messages=>["悪いの悪いの飛んでいけ!\nプリキュア!ヒーリングプレアーフレッシュ!"], :transform_calls=>["change_precure_beat_up", "change", "beat_up"]} Cure.sunshine #=> {:girl_name=>"cure_sunshine", :human_name=>"明堂院いつき", :precure_name=>"キュアサンシャイン", :cast_name=>"桑島法子", :created_date=>Sun, 18 Jul 2010, :color=>"yellow", :transform_message=>"(プリキュアの種、いくですぅ!)\nプリキュア!オープンマイハート!\n陽の光浴びる一輪の花! キュアサンシャイン!\nハートキャッチ、プリキュア!", :extra_names=>["スーパーキュアサンシャイン"], :attack_messages=>["花よ、舞い踊れ!\nプリキュア!ゴールドフォルテバースト!!", "花よ、咲き誇れ!\nプリキュア・ハートキャッチ・オーケストラ!!"], :transform_calls=>["open_my_heart"]} # ...
だけど,プリキュアは変身すると攻撃できるようになる.
yayoi = Precure.smile.girls[2] yayoi.name #=> "黄瀬やよい" yayoi.cast_name #=> "金元寿子" yayoi.attack! #=> RuntimeError: require transform yayoi.transform! (レディ?) プリキュア・スマイルチャージ! (ゴー!ゴー!レッツ・ゴー!ピース!!) ピカピカピカリンジャンケンポン! キュアピース! 5つの光が導く未来! 輝け!スマイルプリキュア! # 1st transform yayoi.name #=> "キュアピース" yayoi.attack! プリキュア!ピースサンダー!!
def attack! raise RequireTransformError, "require transform" if current_attack_message.blank? print_by_line current_attack_message current_attack_message end
@current_state
って変数に整数値で現在の状態をもたせてた.0だったら変身前,1だったら1段階変身..みたいな. これをcurrent_attack_message
で変身後かどうか(@current_state
が0でないか)を判定して変身の台詞を持ってきてる. print_by_line
はいい感じにdelayをつけてメッセージを標準出力するやつ.
yamlには attack_messages
に台詞が配列で入ってて @current_state
でインデックスを指定できるようになってる.
cure_peace: &cure_peace # ... extra_names: - プリンセスピース - ウルトラピース attack_messages: - |- プリキュア!ピースサンダー!! - |- 開け、ロイヤルクロック! (みんなの力を1つにするクル!) 届け、希望の光! はばたけ!光り輝く未来へ! プリキュア!ロイヤルレインボーバースト! - |- (みんなの力を1つにするクル!) プリキュア!ミラクルレインボーバースト! 輝けー!! スマイルプリキュア!! transform_calls: - smile_charge
raise RequireTransformError, "require transform"
がかっこいい.
lib/rubicure/errors.rb
で定義してる
transform!
, name
もおなじように @current_state
でひっぱってきてる
maho_girls
だけは transform_styles
ってのがあって,それぞれに girl_name
, human_name
... てのがあるらしく,それは define_method
で定義されてた.
https://github.com/sue445/rubicure/blob/v1.0.4/lib/rubicure/girl.rb#L95-L103
まなび
- yamlのloadとか,
|-
記法(配列の最後の改行を無視する)とか include Enumerable
で each メソッドを定義できる https://github.com/sue445/rubicure#enumerablemethod_missing
はインパクトあった.多分しばらくわすれない
かんそう
最新シリーズのアラモードってやつのyamlが美味しそうで夜中に読んでたら無限にお腹がへったのがつらかった.
REF
rubicure: All about Japanese battle heroine "Pretty Cure (Precure)"
sinatra
1ヶ月くらい前に7年ぶりのメジャーアップデートリリースがあった.rack2.0, ruby 2.2+ に対応したり rails5 との互換性が実装されたらしい.
コードリーディングは, sinatra の classic app の
rackup
コマンドなしでサーバーが起動する部分- DSLの実装
を読んでいく.
Rack
Rackに関してここでは説明しないが前提知識としてあったほうがいい. k0kubunさんの記事がとてもわかりやすかったです.
http://qiita.com/k0kubun/items/248395f68164b52aec4a
server の起動
entry point
require './app' run Sinatra::Application
これだけで app.rb
でDSLを書けば ruby app.rb
コマンドでサーバが起動する.
Sinatra::Application
をみると,at_exit
で Application.run!
をフックしてる
at_exit { Application.run! if $!.nil? && Application.run? }
Sinatra::Base#run!
Sinatra::Application
は Sinatra::Base
のサブクラス.
#detect_rack_handler
で
ひっぱってきたRackハンドラに対して #start_server
で handler.run
してサーバを起動してる
def run!(options = {}, &block) return if running? set options handler = detect_rack_handler handler_name = handler.name.gsub(/.*::/, '') server_settings = settings.respond_to?(:server_settings) ? settings.server_settings : {} server_settings.merge!(:Port => port, :Host => bind) begin start_server(handler, server_settings, handler_name, &block) rescue Errno::EADDRINUSE $stderr.puts "== Someone is already performing on port #{port}!" raise ensure quit! end end
DSLの実装
Rack::Builder
で Sinatra::Delegator
を include してる
class Rack::Builder include Sinatra::Delegator end
Sinatra::Delegator
はDSLのメソッドを define_method
で定義して,Sinatra::Application
に send
してる.
module Delegator #:nodoc: def self.delegate(*methods) methods.each do |method_name| define_method(method_name) do |*args, &block| return super(*args, &block) if respond_to? method_name Delegator.target.send(method_name, *args, &block) end private method_name end end delegate :get, :patch, :put, :post, :delete, :head, :options, :link, :unlink, :template, :layout, :before, :after, :error, :not_found, :configure, :set, :mime_type, :enable, :disable, :use, :development?, :test?, :production?, :helpers, :settings, :register class << self attr_accessor :target end self.target = Application end
Rack application
main.rb
で フックしてるのは Application.run
なので,Sinatra::Application#call
ではなく Sinatra::Application.call
が呼ばれる(実装は両方ある)
def call(env) synchronize { prototype.call(env) } end
prototypeにはrack middlewareでラップされたrack application が入ってるっぽいので結局 #call
がよばれるみたいだけどちょっと自信ない.
def call!(env) # :nodoc: @env = env @request = Request.new(env) @response = Response.new template_cache.clear if settings.reload_templates @response['Content-Type'] = nil invoke { dispatch! } invoke { error_block!(response.status) } unless @env['sinatra.error'] unless @response['Content-Type'] if Array === body and body[0].respond_to? :content_type content_type body[0].content_type else content_type :html end end @response.finish end
dispatch!
で #route!
がよばれてその中でroutingしてるっぽいけどこの辺はあんまりちゃんとよんでない.
かんそう
rackに関してちょっと勉強になった. at_exit
でフックしてるのはだいぶアクロバティックに見えるけどどうなんだろ.
とりあえず1984行の base.rb
は迫力満点なのでファイル分割してほしい
kaminariのactiverecord拡張部分を読む
Query Basics - kaminari/kaminiari にあるようなActiveRecordのモデルに生えるscope等がどうやって定義されているか調べる
REF
よんでみる
kaminari/kaminari-activerecord/lib/kaminari/activerecord/active_record_model_extension.rb
# frozen_string_literal: true require 'kaminari/activerecord/active_record_relation_methods' module Kaminari module ActiveRecordModelExtension extend ActiveSupport::Concern included do include Kaminari::ConfigurationMethods # Fetch the values at the specified page number # Model.page(5) eval <<-RUBY, nil, __FILE__, __LINE__ + 1 def self.#{Kaminari.config.page_method_name}(num = nil) per_page = max_per_page && (default_per_page > max_per_page) ? max_per_page : default_per_page limit(per_page).offset(per_page * ((num = num.to_i - 1) < 0 ? 0 : num)).extending do include Kaminari::ActiveRecordRelationMethods include Kaminari::PageScopeMethods end end RUBY end end end
いかにも page
メソッドを生成してそう.
included を辿っていくと active_record_extension -> active_record へたどりつく
kaminari-activerecord/lib/kaminari/activerecord.rb
ActiveSupport.on_load :active_record do require 'kaminari/activerecord/active_record_extension' ::ActiveRecord::Base.send :include, Kaminari::ActiveRecordExtension end
ActiveSupport が load されたときに, ActiveRecord::Base
を継承したクラスに対して上のevalが実行される.(kaminari-activerecord/lib/kaminari/activerecord/active_record_extension.rb で親クラスをみてる)
kaminari/kaminari-core/lib/kaminari/config.rbで Kaminari.config.page_method_name
には :page
が入っているので,予想通りここで生成されてるのは page
method .
(なんでわざわざconfigに入ってるんだろうと思ったけど rails g kaminari:config
したときにメソッド名を指定できるようにしてるからだった.)
その他の各メソッドは,page メソッドの中で Kaminari::ActiveRecordRelationMethods
と Kaminari::PageScopeMethods
を include して生やしてた.