< Back

rspecのdescribeとitを自作する / ついでにGrapeのexposeのソースコードを読む

rspecのdescribeとitを自作してみる

読書コード対象者

Rubyのコード少しでも読める方なら最後まで読了できるかと思います。

  • Rubyの引数について詳しくなります。
  • GrapeというAPIを簡単に作れるOSSのソースコードを少しだけ一緒に読んで行きたいと思うので毎日使っているコードが親近感でます

最終的には、下記の文法が何しているかわかります。

describe "RspecでこちらのコードがRuby内部でどういう文法で動いているか" do

  puts "分かるようになります"

end

class Status < Grape::Entity
    # またGrapeのこのあたりのコードにも詳しくなります
    expose :style
    expose :html, documentation: { type: String, desc: 'html' } do |f|
        f.body
    end
end

またこの do end{ } みたいなブロックと呼ばれるものへの理解が少し広がるかと思います。

背景

こんにちは僕です。この話は、Vue.jsからAPIを叩こう思い、Rails側にGrapeによるAPIを作っていたことから始まります。そのときに Grapeエラーでるよ〜〜あああああ〜〜 って思いソースコードまで追ってみたのでその時の話を順番にしていきます。( ドキュメント見てから実装開始しろってツッコミはなしで )

Grapeのコードを追ってみる

Entityのサンプル

Grape::EntityのGitHub

class Status < Grape::Entity
    expose :style
    expose :html, documentation: { type: String, desc: 'html' } do |f|
        f.body
    end
end

と書いておくと

present s, with: Status

みたいな感じでJSONの返却をぬるっとやってくれるのがGrapeです。

今回はこの expose の引数に何を渡せばいいのか全くわからなかったので、そのあたりのコードを追っていきます。

Rubyの引数の振り返り


def x1(text)
    puts text
end

def x2 text
    puts text
end

x1("it calls x1") # it calls x1
x2 "it calls x2"  # it calls x2

おもむろにTerminalでirbと打つとRubyが対話モードで使えるので(MacUser)上のコードは適当に自分で流し込んで試してください。!!

$ irb
$ irb(main):001:0>

スクリーンショット 2018-06-21 0.43.36.png

こんな感じでRubyは () が省略できたりします。

参考: Ruby のコーディングスタイル

Grapeの expose の使い方をもう一度見る

前回はRubyの()の省略について学んだので次のコードも理解できるはずです。

expose :user_name

つまり expose はメソッドで、引数に :user_name というシンボルを渡している、という風に見えて来ましたね。(間違ってたら偉い人が教えてくれるのでコメント欄参照!!)

ここまでで、ちょっとRubyに詳しくなった気がします。

なので次は expose というメソッドを見に行きます。

Grapeの expose のソースコードを見る

ソースコードはこちら lib/grape_entity/entity.rb

def self.expose(*args, &block)
    options = merge_options(args.last.is_a?(Hash) ? args.pop : {})

    if args.size > 1
        raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as]
        raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
        raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
    end

    raise ArgumentError, 'You may not use block-setting when also using format_with' if block_given? && options[:format_with].respond_to?(:call)

    if block_given?
        if block.parameters.any?
            options[:proc] = block
        else
            options[:nesting] = true
        end
    end

    @documentation = nil
    @nesting_stack ||= []
    args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) }
end

20行くらいなのでみんなすぐ理解できますよね(ちなみに私理解できなかったのでこの記事書きながら理解しています)

Grapeの expose のソースコードを適当に理解する

def self.expose(*args, &block)
    options = "引数の *args からオプションをいい感じに整形する"

    if args.size > 1
        "問題あったらエラーだす"
    end

    raise ArgumentError, '問題ないよね…?'

    if block_given?
        "引数として &block が渡されてるとごにょごにょ"
    end

    @documentation = nil
    @nesting_stack ||= []
    args.each { |attribute| "いい感じにゴニョゴニョしてデータ作り出す" }
end

正直なんかよくわからないけど def self.expose(*args, &block) これの *args引数はたくさん渡せるよ!で、 最後の &block引数の最後にブロック渡せるぜ!!!! って意味だと思ったらいいと思います。(強引)

※ブロックとは do end とか {} のことで、ブロックが引数として渡されたのを判定するのは Rubyの block_given? というメソッドが使えます。

Grapeの expose の使い方をもう一度見る(3度目)

なので、こちらももう読めるようになったはずです。。

expose :html, documentation: { type: String, desc: 'html' } do |f|
    f.body
end

exposeというメソッドの引数は

  1. :html
  2. documentation: { type: String, desc: 'html' })
  3. do |f| f.body; end

の3つで、最後の3つ目はブロック要素として認識されています。

def self.expose(*args, &block)
    options = merge_options(args.last.is_a?(Hash) ? args.pop : {})
end

そして、もう一度exposeメソッドを見ると、1と2は(*args)に入って、3のブロックは(&block)に入っていくことがわかります。

args.last.is_a?(Hash)*args の最後がハッシュなら という意味ですが、おそらくですが、この *args の最後には documentation のドキュメントをいれないとエラー起きるような気がします(今回はそこは追わない)

ブロックが渡されたか判断する (&block)

Rubyのメソッドにblock_given?で渡されたかチェックできます。

def self.expose(*args, &block)
    if block_given?
        "引数として &block が渡されてるとごにょごにょ"
    end
end

なのでexposeには2つの書き方があります。

class Status < Grape::Entity
    expose :style
    expose :html, documentation: { type: String, desc: 'html' } do |f|
        f.body
    end
end

do; end;をつける書き方、何もつけない書き方、どちらも文法的に正しくなるように expose は設計されています。

普段なんとなく使っているコードをしっかり追ってみましたが、個人的にはRubyをちゃんと知らなくなんとなく使っているのはエンジニアとして成長するためにもだめだなあと思いました。

block_given? のドキュメント

Rspecのコードは追わないが作ってみる

Rspec序文

ここまでで expose のコードを読んでrubyの使い方への理解が深まったと思います。

次は本題ですが Rspecの describe を自分で書いてみようと思います。

describe "xxxをテストする" do
  puts "これで合ってるかな…"
end

ここまで読んだ読者はもうわかると思いますが、describe というメソッドは引数は (text, block) テキストとブロックだと判断がつきます。

Rspec作成編

# encoding: utf-8

def describe(msg, &block)
    if block_given?
        block.call
    else
        puts msg
    end
end

puts "~~~~~~~~~~~~~~~~~~~~~~~~~~"

describe "こちらテストですよ!!!!!!"


puts "~~~~~~~~~~~~~~~~~~~~~~~~~~"

describe "こちらテストです" do
  puts "ブロック渡したらどうなるかテスト"
end

# 返ってくる値
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~
# こちらテストですよ!!!!!!
# ~~~~~~~~~~~~~~~~~~~~~~~~~~
# ブロック渡したらどうなるかテスト

今回作成した describe はブロックを渡したかどうかで処理を分岐させてみました。

  1. ブロックを渡した場合

    • block_given?true になる
    • block.callを実行させる
  2. ブロックを渡さない場合

    • ただ第一引数で渡したmsgをputsするだけになります。

Paiza.ioでの動作サンプル

ここまで自分で作れるようになったら、Rubyの引数マスターだと思います

block.call

渡したブロックを実行させられます。

block.call

Rspecのソースコードについて

たぶんレポジトリはこちらですが describe とかどこで実装されているか正直読んでないです。分かる方は記事を書いてくれると楽しんで読みたいと思うのでコメント欄でお願いします〜〜

最後に

自分が普段使っているソースコードを、気になったところだけでもいいので読んで見る。これだけでもOSS使ってるっぽいし(形から入る人)、ドキュメントにしっかりかかれていないこともなんとなく分かるのがすごくいいですね。(今回は expose の最後の引数はdocumentじゃね?って考えれたこととか)

Ruby詳しいですとは口がさけても言えないので詳しい方PRとかコメント欄でご指導いただけると幸いです。最後まで読んでいただきありがとうございました〜!!