easy_fabricator とか作ってみた。

あるいは。

Ruby で動的に class を定義したかった。

twitter 上で何人かにそんなんできるよって言われたんだけど、どうにも伝わらなかった部分。

class を生成したいんじゃなくて、class 構文を使わずに class を定義したい。

なんでかというと

Fabricator は Model 相当の class の名前を Symbol で与えて使うから

ゴニョゴニョしてできたものはこれ。

#! /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

って書き方になるんだけど、これは通常のメソッド呼び出しになるので定義できない。

回避方法あるのかなぁ。あったら嬉しいんだけど。ダミーデータ用のツールのために元の名前に制限があったらちょっと本末転倒だよね。

More