easy_fabricator とか作ってみた。
あるいは。
Ruby で動的に class を定義したかった。
twitter 上で何人かにそんなんできるよって言われたんだけど、どうにも伝わらなかった部分。
class を生成したいんじゃなくて、class 構文を使わずに class を定義したい。
なんでかというと
Fabricator は Model 相当の class の名前を Symbol で与えて使うから
ゴニョゴニョしてできたものはこれ。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env ruby | |
# -*- coding: utf-8 -*- | |
require 'optparse' | |
require 'singleton' | |
require 'rubygems' unless defined? ::Gem | |
require 'fabrication' | |
class EasyFabricator | |
class Configure | |
include Singleton | |
def initialize | |
@_fields = nil | |
@_fab = nil | |
end | |
def fields | |
if block_given? | |
@_fields = yield | |
else | |
@_fields | |
end | |
end | |
def fab( &block ) | |
if block_given? | |
@_fab = block | |
else | |
@_fab.call if @_fab | |
end | |
end | |
end | |
def self.configure(&block) | |
block.call(Configure.instance) | |
end | |
def initialize | |
@num = 1 | |
@format = 'csv' | |
@config = Configure.instance | |
end | |
attr_reader :config | |
def run | |
args.parse!( ARGV ) | |
if ( file = ARGV.shift and File.exist?( file ) ) | |
load file | |
Object.const_set( 'Model', Struct.new( *config.fields ) ) | |
config.fab | |
send( "output_#{@format}" ) | |
else | |
puts args | |
end | |
end | |
def output_csv | |
require 'fastercsv' if RUBY_VERSION < '1.9' | |
puts FasterCSV.generate { |csv| | |
csv << config.fields | |
} | |
puts FasterCSV.generate { |csv| | |
@num.times { | |
c = Fabricate.build( :model ) | |
csv << config.fields.map { |field| | |
c[field] | |
} | |
} | |
} | |
end | |
def output_yaml | |
$KCODE = 'u' unless defined? ::Encoding | |
puts (1..@num).to_a.map { |id| | |
record = Fabricate.build( :model ) | |
hash = {} | |
record.each_pair { |k, v| | |
hash[k.to_s] = v | |
} | |
to_yaml( id => hash ).sub( /--- \n/, '' ) | |
} | |
end | |
def args | |
OptionParser.new { |opt| | |
opt.banner << " fabricator" | |
opt.on( '-n', '--num NUM ( default 1 )' ) { |num| | |
@num = num.to_i if num =~ /\A[0-9]+\z/ | |
} | |
opt.on( '-y', '--output-yaml' ) { | |
@format = 'yaml' | |
prepare_yaml | |
} | |
opt.on( '-c', '--output-csv ( default )' ) { | |
@format = 'csv' | |
} | |
} | |
end | |
private | |
def prepare_yaml | |
begin | |
require 'ya2yaml' | |
rescue LoadError | |
require 'yaml' | |
end | |
class << self | |
if ( defined? Ya2YAML ) | |
def to_yaml( obj ) | |
obj.ya2yaml | |
end | |
else | |
def to_yaml( obj ) | |
obj.to_yaml | |
end | |
end | |
end | |
end | |
end | |
if ( __FILE__ == $0 ) | |
EasyFabricator.new.run | |
end |
具体的に困ったこと、解決したことは何か
- Object.const_set( NAME, classobj ) で好きなタイミングで class 定義できる
- Klass = な書き方はメソッドの中では不可能
- fabrication の DSL はちょっと注意が必要
例えば open や catch とかって attribute を用意してしまうと困る。
Fabricator( :model ) do
open { ... }
catch { ... }
end
って書き方になるんだけど、これは通常のメソッド呼び出しになるので定義できない。
回避方法あるのかなぁ。あったら嬉しいんだけど。ダミーデータ用のツールのために元の名前に制限があったらちょっと本末転倒だよね。