u1f419

コード読んだめも

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::BuilderDSL として処理してる(多分).

このブロック中の provider は, Rack::Builder をラップしている OmniAuth::Builder で提供されてる.

#provider

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::Strategyomniauth/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 が返される.これが認可サーバの 「 がアクセスを求めています,許可しますか?」みたいなビュー.ここでリソースオーナーが認証(ログイン)して認可すると,認可グラントとともにリソースオーナーを Client Application に返して,この認可グラントと Client Application の client id と client secret を使って認可サーバーに access token を要求するはずだけど,その部分が見られない.おもむろに omniauth/omniauth-oauth2を見てみると,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 を呼ぶっぽい.

ServerCommand#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

configyaml をロードしたハッシュを持ってきてる. 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

Rubicure::Series#find

method missing のオーバーライドはびっくりした


Cureyamlからデータを引っぱってくるところまでは 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が美味しそうで夜中に読んでたら無限にお腹がへったのがつらかった.

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.rbDSLを書けば ruby app.rb コマンドでサーバが起動する.

Sinatra::Application をみると,at_exitApplication.run! をフックしてる

at_exit { Application.run! if $!.nil? && Application.run? }

Sinatra::Base#run!

Sinatra::ApplicationSinatra::Base のサブクラス. #detect_rack_handler で ひっぱってきたRackハンドラに対して #start_serverhandler.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::BuilderSinatra::Delegator を include してる

class Rack::Builder
  include Sinatra::Delegator
end

Sinatra::DelegatorDSLのメソッドを define_method で定義して,Sinatra::Applicationsend してる.

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.rbKaminari.config.page_method_name には :page が入っているので,予想通りここで生成されてるのは page method .

(なんでわざわざconfigに入ってるんだろうと思ったけど rails g kaminari:config したときにメソッド名を指定できるようにしてるからだった.)

その他の各メソッドは,page メソッドの中で Kaminari::ActiveRecordRelationMethodsKaminari::PageScopeMethods を include して生やしてた.