takoblo

コード読んだめも

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 して生やしてた.