LoginSignup
3
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-06-05

きっかけ

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

完成形

  • MyFaker::Name.nameと書くことで、ランダムな名前を生成できる。
  • MyFaker::Config.locale = :enもしくはMyFaker::Config.locale = :jaと打つことで、日本語名と英語名を切り替えられる(デフォルトは日本語)
  • 完成形

Fakerとは?

  • Faker(github)
  • ランダムに人名や地名を作成してくれるGem。サンプルデータを一気に作りたいときに役立つ
# 使い方(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/fakerlib/helperslib/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ではなくてこちらに書かないといけないらしいので注意
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のコピペ。)
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を配置する
# ja.yml

ja:
  my_faker:
    name:
      last_name: ["佐藤", "鈴木", "高橋"]
      first_name: ["翔太", "蓮", "翔"]
      name:
        - "#{last_name} #{first_name}"
# 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ファイル内からランダムに名前を選んで返すというものである。
  • 今回は
    • フルネームをランダム生成
    • 苗字だけ生成
    • 名前だけ生成

する関数を付与するので

# /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アプリを作った場合
gem 'my_faker', path: '../my_faker'

と入力する。

  • 後はbundle installをして、読み込まれたのを確認したら
MyFaker::Name.name
# => "鈴木 蓮"

MyFaker::Config.locale = :en

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

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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3