簡易版Fakerを作ってGemの作り方の手法を学ぶ


# きっかけ
- Gemを自作してみたいと考えてはいるものの、どのような作り方が綺麗なのかがまだよくわかっていない。
- なので、今回はGemの中でも比較的シンプルな **Faker**を真似た、 **MyFaker**を作ることで、Gem構造などのテクニックを学ぶことにした

# 完成形
- `MyFaker::Name.name`と書くことで、ランダムな名前を生成できる。
- `MyFaker::Config.locale = :en`もしくは`MyFaker::Config.locale = :ja`と打つことで、日本語名と英語名を切り替えられる（デフォルトは日本語）
- [完成形](https://github.com/DialBird/my_faker)

# Fakerとは？
- Faker([github](https://github.com/stympy/faker))
- ランダムに人名や地名を作成してくれるGem。サンプルデータを一気に作りたいときに役立つ

```ruby
# 使い方(db/seeds.rbで実行)

# localeを日本に設定
Faker::Config.locale = :ja

# 100個Userモデルを作成する
100.times do
  User.create(name: Faker::Name.name,
               address: Faker::Address.city
               email: Faker::Internet.email)
end
```

# Fakerの構造（重要な部分を抜粋）
- Gemはlib内にロジックが詰まっており、最初に`lib/faker.rb`が実行されるようになっている
- `lib/faker.rb`内で`lib/faker`と`lib/helpers`、`lib/extentions`ディレクトリを読み込んで使っている
- `lib/locales`ディレクトリ内に`I18n`用の翻訳ファイル群が入っている

```
faker
├── Gemfile
├── Gemfile.lock
├── faker.gemspec
└── lib
    ├── extensions（機能拡張ライブラリが入っている。複雑な処理には使うが今回は使わない）
    ├── faker（ここを介してymlファイルから翻訳情報を得ている）
    ├── faker.rb（最初に実行されるファイル）
    ├── helpers（ヘルパーメソッドが入っている。複雑な処理には使うが今回は使わない）
    └── locales（ここにja.yml,en.ymlなどの言語別の翻訳が入っている）
```

# MyFakerの自作
## 構造について
- まずはgem開発のために`bundle gem my_faker -t`を実行すると以下のようなファイルができる

```
my_faker/
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── my_faker.gemspec
├── lib
│   ├── my_faker
│   │   └── version.rb
│   └── my_faker.rb
└── spec
    ├── my_faker_spec.rb
    └── spec_helper.rb
```

- 作成したら、まずは`my_faker.gemspec`の中の以下の部分を編集する（これをやっておかないとGemが実行できない）
- なお`I18n`のgemをインストールするためには、`Gemfile`ではなくてこちらに書かないといけないらしいので注意

```ruby
spec.authors       = ["自分の名前"]
spec.email         = ["メールアドレス"]

spec.summary       = %q{簡単な要約（hogeでOK）}
spec.description   = %q{詳細な説明（fugaでOK）}
spec.homepage      = "http://test.example.com"

# i18nを使うので、ここでインストールして置く
spec.add_runtime_dependency('i18n')
```

- 後は`bundle install`を実行すれば準備完了

## 最初に呼び出されるメインファイル`lib/my_faker.rb`の編集
- gemを実行すると、lib直下のgemファイル名と同じファイルが実行される
- 以下のように書き換える（大部分は本家のGemのコピペ。）

```ruby
require 'i18n'

if I18n.respond_to?(:enforce_available_locales=)
  I18n.enforce_available_locales = true
end

I18n.available_locales = [:en, :ja]
# デフォルトを日本語に設定する
I18n.default_locale = :ja

# 現在のファイルのmy_faker.rbファイル（__FILE__）のあるディレクトリ内にあるlocalsファイル内のymlファイルをまとめて読み込む
mydir = File.expand_path(File.dirname(__FILE__))
I18n.load_path += Dir[File.join(mydir, 'locales', '*.yml')]
I18n.reload! if I18n.backend.initialized?

# メインロジック
module MyFaker
  class Config
    @locale = nil
    @random = nil

    class << self
      attr_writer :locale
      attr_writer :random

      def locale
        @locale || I18n.locale
      end

      def own_locale
        @locale
      end

      def random
        @random || Random::DEFAULT
      end
    end
  end

  class Base
    Numbers = Array(0..9)
    ULetters = Array('A'..'Z')
    Letters = ULetters + Array('a'..'z')

    class << self
      def numerify(number_string)
        number_string.sub(/#/) { (rand(9)+1).to_s }.gsub(/#/) { rand(10).to_s }
      end

      def letterify(letter_string)
        letter_string.gsub(/\?/) { sample(ULetters) }
      end

      def bothify(string)
        letterify(numerify(string))
      end

      def regexify(re)
        re = re.source if re.respond_to?(:source)
        re.
          gsub(/^\/?\^?/, '').gsub(/\$?\/?$/, '').
          gsub(/\{(\d+)\}/, '{\1,\1}').gsub(/\?/, '{0,1}').
          gsub(/(\[[^\]]+\])\{(\d+),(\d+)\}/) {|match| $1 * sample(Array(Range.new($2.to_i, $3.to_i))) }.
          gsub(/(\([^\)]+\))\{(\d+),(\d+)\}/) {|match| $1 * sample(Array(Range.new($2.to_i, $3.to_i))) }.
          gsub(/(\\?.)\{(\d+),(\d+)\}/) {|match| $1 * sample(Array(Range.new($2.to_i, $3.to_i))) }.
          gsub(/\((.*?)\)/) {|match| sample(match.gsub(/[\(\)]/, '').split('|')) }.
          gsub(/\[([^\]]+)\]/) {|match| match.gsub(/(\w\-\w)/) {|range| sample(Array(Range.new(*range.split('-')))) } }.
          gsub(/\[([^\]]+)\]/) {|match| sample($1.split('')) }.
          gsub('\d') {|match| sample(Numbers) }.
          gsub('\w') {|match| sample(Letters) }
      end

      def fetch(key)
        fetched = sample(translate("my_faker.#{key}"))
        if fetched && fetched.match(/^\//) and fetched.match(/\/$/)
          regexify(fetched)
        else
          fetched
        end
      end

      def fetch_all(key)
        fetched = translate("my_faker.#{key}")
        fetched = fetched.last if fetched.size <= 1
        if !fetched.respond_to?(:sample) && fetched.match(/^\//) and fetched.match(/\/$/)
          regexify(fetched)
        else
          fetched
        end
      end

      def parse(key)
        fetched = fetch(key)
        parts = fetched.scan(/(\(?)#\{([A-Za-z]+\.)?([^\}]+)\}([^#]+)?/).map {|prefix, kls, meth, etc|
          cls = kls ? MyFaker.const_get(kls.chop) : self

          text = prefix

          text += cls.respond_to?(meth) ? cls.send(meth) : fetch("#{(kls || self).to_s.split('::').last.downcase}.#{meth.downcase}")
          text += etc.to_s
        }
        parts.any? ? parts.join : numerify(fetched)
      end

      def translate(*args)
        opts = args.last.is_a?(Hash) ? args.pop : {}
        opts[:locale] ||= MyFaker::Config.locale
        opts[:raise] = true
        I18n.translate(*(args.push(opts)))
      rescue I18n::MissingTranslationData
        opts = args.last.is_a?(Hash) ? args.pop : {}
        opts[:locale] = :en
        I18n.translate(*(args.push(opts)))
      end

      def flexible(key)
        @flexible_key = key
      end

      def sample(list)
        list.respond_to?(:sample) ? list.sample(random: MyFaker::Config.random) : list
      end

      def rand_in_range(from, to)
        from, to = to, from if to < from
        rand(from..to)
      end

      def rand(max = nil)
        max ? MyFaker::Config.random.rand(max) : MyFaker::Config.random.rand
      end
    end
  end
end

# faker上ので定義した関数を使う
Dir.glob(File.join(File.dirname(__FILE__), 'my_faker','*.rb')).each {|f| require f }
```

## ランダムに出す名前一覧を作る
- `lib/locales/`ディレクトリ配下に、日本語用の名前一覧を格納する`ja.yml`と、英語用の`en.yml`を配置する

```YAML
# ja.yml

ja:
  my_faker:
    name:
      last_name: ["佐藤", "鈴木", "高橋"]
      first_name: ["翔太", "蓮", "翔"]
      name:
        - "#{last_name} #{first_name}"
```

```YAML
# en.yml

en:
  my_faker:
    name:
      last_name: ["Jordan", "John", "Smith"]
      first_name: ["Bob", "Kile", "Mike"]
      name:
        - "#{last_name} #{first_name}"
```

## `my_faker`フォルダ内に`name.rb`ファイルを設置する
- `my_faker.rb`ファイルから直接読み込む対象は、基本同じファイル名を持つ`my_faker`フォルダ内に入れるらしい
- 流れとしては、`name.rb`ファイルを呼び出し、先ほど作ったymlファイル内からランダムに名前を選んで返すというものである。
- 今回は
	- フルネームをランダム生成
	- 苗字だけ生成
	- 名前だけ生成

する関数を付与するので

```ruby
# /lib/myfaker/name.rb

module MyFaker
  class Name < Base
    flexible :name

    class << self
      def name
        parse('name.name')
      end

      def first_name
        fetch('name.first_name')
      end

      def last_name 
        fetch('name.last_name')
      end
    end
  end
end
```

とする。

- ここまでくれば完成である

# 作成したGemを使ってみる
- 新たに別のディレクトリにRailsアプリを作成して、以下のようにGemfile内に記入する
- もしmy_fakerの置いてあるディレクトリにRailsアプリを作った場合

```ruby
gem 'my_faker', path: '../my_faker'
```

と入力する。

- 後は`bundle install`をして、読み込まれたのを確認したら

```ruby
MyFaker::Name.name
# => "鈴木 蓮"

MyFaker::Config.locale = :en

MyFaker::Name.name
# => "John Bob"
```

という風にランダムに名前を生成できるようになる。