この冬はじめよう! 5分で理解するGemのコードリーディング(1) settingslogic

## <i class="fa fa-cubes" style="font-size:1em;"></i> 概要

Gemのコードリーディングで、広く使われている高品質なコードからテクニックや定石を学んでいきます。

## <i class="fa fa-cube" style="font-size:1em;"></i> はじめに

いきなりRailsとかRspecを読みにいくのもツラそうなので、ここでは初心者向けとウワサの[settingslogic](https://github.com/binarylogic/settingslogic)を読んでいくことにします。Gemを読む際の難易度の目安としては、 [初めてのGemの読み方](http://www.slideshare.net/babatakao/gem-release)を参考にさせて頂きました. :bow: 

私自身は、いくつかのOSSで、放置されているissue(テストやドキュメンテーション)をお片付けしたことはありますが、普段、そんなにOSSにガッツリcommitしている訳でもありません。そんな私にとって、

- メソッドの命名規則
- メモ化、method chainの実現方法
- コメントの書き方

など、Rubyのお作法を学ぶ題材として、またOSSのコードリーディングのきっかけとして、非常に良い題材だったと思います。

## <i class="fa fa-cube" style="font-size:1em;"></i> version

2.0.9を使用します。

## <i class="fa fa-cube" style="font-size:1em;"></i> settingslogicの使い方

ソースコードを読む前に、Settingslogicの使い方を確認しておきましょう。

### 設定ファイルの作成

まずは、管理する設定ファイルをYAML形式で記述します。

```settings.yml
language:
  haskell:
    paradigm: functional
  smalltalk:
    paradigm: object oriented
```

### 設定内容にアクセスするクラスの定義

次に、設定ファイルの内容にアクセスするためのクラスを定義します。

```settings.rb
class Settings < Settingslogic
  source "#{File.dirname(__FILE__)}/settings.yml"
end
```

### 設定内容にアクセス

設定内容にアクセスするには、以下の様に記述します。

```
>Settings.language.haskell.paradigm
=> 'functional'

```

ただのHashよりも、扱いやすいインタフェースで設定内容にアクセスすることができます。
どうして、こんな事が可能になるのでしょうか?

上記のコードの裏側を理解することが今回のゴールです。


## <i class="fa fa-tree" style="font-size:1em;"></i> 構成

ここは、軽く眺めるくらいで良いと思います。

~~~zsh

$ tree -L 2                                                                                 .
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.rdoc
├── Rakefile
├── lib
│   └── settingslogic.rb
├── settingslogic.gemspec
├── spec
│   ├── settings.rb
│   ├── settings.yml
│   ├── settings2.rb
│   ├── settings3.rb
│   ├── settings4.rb
│   ├── settings_empty.rb
│   ├── settings_empty.yml
│   ├── settingslogic_spec.rb
│   └── spec_helper.rb
└── vendor
    └── bundle
~~~

## <i class="fa fa-cubes" style="font-size:1em;"></i> コードリーディング 

### 設定ファイルのパスを設定

まずは設定ファイル(YAML)のパス設定を行っている部分のソースコードをみてみます。

```settings.rb
class Settings < Settingslogic
  source "#{File.dirname(__FILE__)}/settings.yml"
end

class SettingsInst < Settingslogic
end
```

```settingslogic.rb
def source(value = nil)
  @source ||= value
end
```    
 
sourceメソッドの中で、``@source``にYAMLファイルのパスが格納されます。
``@source``はメモ化されているようです。
この辺は読みやすいですね。


### 単なるHashよりも扱いやすいインタフェースで設定内容にアクセスする

では、いよいよ本題の部分の処理をみていきましょう。


#### Settings.languageメソッドが指定され、method_missingに処理が移る。

```
Settings.language.haskell.paradigm
>> 'functional'
```

上記のコードが実行されたとき、実際にはSettings.languageメソッドは存在しないので、method_missingが呼び出されます。
method_missingの引数であるnameには``:language``が渡されます。


```
def method_missing(name, *args, &block)
  instance.send(name, *args, &block)
end
```

次はinstanceメソッドの中身を見てみましょう。

#### YAMLファイルに記述された内容を読み込んでインスタンス作成

instanceメソッドでは、Settingsクラスのインスタンスであるオブジェクトを作成して返却します。

```
def instance
  return @instance if @instance
  @instance = new
  create_accessors!
  @instance
end
```

initializeメソッドの中身も少しのぞいてみます。

```
  def initialize(hash_or_file = self.class.source, section = nil)
    #puts "new! #{hash_or_file}"
    case hash_or_file
    when nil
      raise Errno::ENOENT, "No file specified as Settingslogic source"
    when Hash
      self.replace hash_or_file
    else
      file_contents = open(hash_or_file).read
      hash = file_contents.empty? ? {} : YAML.load(ERB.new(file_contents).result).to_hash # (1)
      if self.class.namespace
        hash = hash[self.class.namespace] or return missing_key("Missing setting '#{self.class.namespace}' in #{hash_or_file}")
      end
      self.replace hash # (2)
    end
    @section = section || self.class.source  # so end of error says "in application.yml"
    create_accessors!
  end
```  


注目するのは __(1)__ と __(2)__ の部分です。
__(1)__では、YAMLファイルに記述した内容をhashに変換しています。
そして __(2)__ ではselfの内容をhashで置き換えます。

このとき、selfの中身を確認すると、こうなっています。

``` 
{
   "language"=>{
     "haskell"=>{"paradigm"=>"functional"},
     "smalltalk"=>{"paradigm"=>"object oriented"}
   }
}
```

今度は、initializerの中で呼ばれているcreate_accessors!を詳しくみていきます。

#### 動的にアクセサを作成

このcreate_accessors!メソッドで、動的にアクセサを作成しています。


```
def create_accessors!
  self.each do |key,val|
    create_accessor_for(key)
  end
end
```

selfの実体は、こんなHashでした。

``` 
{
   "language"=>{
     "haskell"=>{"paradigm"=>"functional"},
     "smalltalk"=>{"paradigm"=>"object oriented"}
   }
}
```

ですから、create_accessors!メソッドの引数のkeyには`"language"`が渡されます。

処理内容を理解する上で、このメソッドが最も重要な部分です。

```
  def create_accessor_for(key, val=nil)
    return unless key.to_s =~ /^\w+$/  # could have "some-setting:" which blows up eval
    instance_variable_set("@#{key}", val)
    self.class.class_eval <<-EndEval
      def #{key}
        return @#{key} if @#{key}
        return missing_key("Missing setting '#{key}' in #{@section}") unless has_key? '#{key}'
        value = fetch('#{key}') # (3)
        @#{key} = if value.is_a?(Hash)
          self.class.new(value, "'#{key}' section in #{@section}") # (4)
        elsif value.is_a?(Array) && value.all?{|v| v.is_a? Hash}
          value.map{|v| self.class.new(v)}
        else
          value # (5)
        end
      end
    EndEval
  end
```
        
着目すべきはclass_evalの部分です。class_evalを用いることで、クラスメソッドの定義を動的に行っています。引数keyに'language'を当てはめて考えると、こうなります。

```
    self.class.class_eval <<-EndEval
      def language
        return @language if @language
        return missing_key("Missing setting 'language' in #{@section}") unless has_key? 'language'
        value = fetch('language')
        @lanugage = if value.is_a?(Hash)
          self.class.new(value, "'language' section in #{@section}")
        elsif value.is_a?(Array) && value.all?{|v| v.is_a? Hash}
          value.map{|v| self.class.new(v)}
        else
          value
        end
      end
    EndEval
```

Settings.languageにアクセスすると、 __(3)__  の実行結果として、valueの中には、以下のような形でlanguage配下の情報が格納されます。

``{"haskell"=>{"paradigm"=>"functional"}, "smalltalk"=>{"paradigm"=>"object oriented"}}``


これはHashですから、 __(4)__ が実行され、再びinitializeメソッドが実行され、その結果作成したインスタンスを返却します。ここで重要なことは、initializeメソッドの中では、再びHashのkey(ここでは"haskell"と"smalltalk")を元にアクセサを定義します。従って、Settings.languageで返却されるインスタンスには ``haskell``, ``smalltalk``というメソッドが定義されています。

確認してみましょう。

```
>Settings.language.methods
=>[
(略)
:haskell,
:smalltalk,
:method_missing,
```

ちゃんと定義されていますね。そして、
``Settings.language.haskell`` が返すインスタンスは、``{"paradigm"=>"functional"}`` を元に生成しますので、またまたparadigmメソッドが動的に定義されており、``Settings.language.haskell.paradigm`` を呼ぶと、今度は __(5)__ のコードが実行され、

```
>Settings.language.haskell.paradigm
=> 'functional'
```

という結果になります。


## <i class="fa fa-cube" style="font-size:1em;"></i> 総括

Hashとclass_evalを組み合わせることで、YAMLで記述した設定内容に対して、より扱いやすいインタフェースでアクセスする方法を学びとることができました。

間違いなどございましたら、ご指摘ください :bow: 

軽い気持ちで始めてみたGemのコードリーディングですが、これからも時間を見つけて投稿していきたいと思います。

## <i class="fa fa-arrow-circle-right" style="font-size:1em;"></i> 参照

- [Reading the Source of Ruby on Rails • Zack Hubert](http://www.zhubert.com/blog/2013/05/11/reading-the-source-of-ruby-on-rails/)
- [初めてのGemの読み方](http://www.slideshare.net/babatakao/gem-release)
- [Cracking Gems: Reading Ruby Code](http://www.sitepoint.com/cracking-gems-reading-ruby-code/)
[開発環境 - pryを使った快適なRubyライブラリのコードリーディング&開発方法 - Qiita](http://qiita.com/yaotti/items/b7b1a304815addf150b2)
- [太一のコードの読み方メモ](https://gist.github.com/taichi/2693387)
- [sinatraのソースコードリーディング - tobi-cの日記](http://d.hatena.ne.jp/tobi-c/20110604/1307235117)

- [ソースコード・リーディングしよう！[GemJam][ActiveSupport] - 酒と泪とRubyとRailsと](http://morizyun.github.io/blog/library-source-code-reading-acctive-support/)
- [sendagaya.rb #101 でActionDispatchを読んでみた - fukajun - 僕はvimで行きます -](http://fukajun.org/61)