きっかけ
- 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/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
ではなくてこちらに書かないといけないらしいので注意
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"
という風にランダムに名前を生成できるようになる。