Skip to content

Commit 4ef0640

Browse files
committed
Replace method_missing with Class.new + define_method
DSL classes now use .for() factories that build anonymous subclasses with real methods via define_method. Better stack traces, faster dispatch, and visible via .methods introspection.
1 parent 465dc4a commit 4ef0640

File tree

6 files changed

+73
-102
lines changed

6 files changed

+73
-102
lines changed

lib/fixture_bot.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ module FixtureBot
1616
class Error < StandardError; end
1717

1818
def self.define(schema, &block)
19-
definition = Definition.new(schema)
19+
definition = Definition.for(schema)
2020
definition.instance_eval(&block)
2121
FixtureSet.new(schema, definition)
2222
end
2323

2424
def self.define_from_file(schema, fixtures_path)
2525
content = File.read(fixtures_path)
26-
definition = Definition.new(schema)
26+
definition = Definition.for(schema)
2727
definition.instance_eval(content, fixtures_path, 1)
2828
FixtureSet.new(schema, definition)
2929
end

lib/fixture_bot/definition.rb

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@ module FixtureBot
44
class Definition
55
attr_reader :generators, :rows
66

7+
def self.for(schema)
8+
klass = Class.new(self)
9+
10+
schema.tables.each_value do |table_def|
11+
klass.define_method(table_def.singular_name) do |record_name = nil, &block|
12+
if record_name.nil? && block.nil?
13+
GeneratorProxy.for(table_def, @generators[table_def.name])
14+
elsif record_name
15+
row_dsl = RowDSL.for(table_def, @schema)
16+
row_dsl.instance_eval(&block) if block
17+
@rows << Row.new(
18+
table: table_def.name,
19+
name: record_name,
20+
literal_values: row_dsl.literal_values,
21+
association_refs: row_dsl.association_refs,
22+
tag_refs: row_dsl.tag_refs
23+
)
24+
else
25+
raise ArgumentError, "#{table_def.singular_name} requires a record name or no arguments"
26+
end
27+
end
28+
end
29+
30+
klass.new(schema)
31+
end
32+
733
def initialize(schema)
834
@schema = schema
935
@generators = {}
@@ -13,37 +39,5 @@ def initialize(schema)
1339
@generators[table_def.name] = {}
1440
end
1541
end
16-
17-
private
18-
19-
def method_missing(method_name, *args, &block)
20-
table_def = find_table(method_name)
21-
return super unless table_def
22-
23-
if args.empty? && block.nil?
24-
GeneratorProxy.new(table_def, @generators[table_def.name])
25-
elsif args.first
26-
record_name = args.first
27-
row_dsl = RowDSL.new(table_def, @schema)
28-
row_dsl.instance_eval(&block) if block
29-
@rows << Row.new(
30-
table: table_def.name,
31-
name: record_name,
32-
literal_values: row_dsl.literal_values,
33-
association_refs: row_dsl.association_refs,
34-
tag_refs: row_dsl.tag_refs
35-
)
36-
else
37-
raise ArgumentError, "#{method_name} requires a record name or no arguments"
38-
end
39-
end
40-
41-
def respond_to_missing?(method_name, include_private = false)
42-
find_table(method_name) || super
43-
end
44-
45-
def find_table(singular_name)
46-
@schema.tables.values.find { |t| t.singular_name.to_s == singular_name.to_s }
47-
end
4842
end
4943
end

lib/fixture_bot/generator_context.rb

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,16 @@
22

33
module FixtureBot
44
class GeneratorContext
5-
def initialize(record_name:, table:, literal_values: {})
6-
@record_name = record_name
7-
@table = table
8-
@literal_values = literal_values
9-
end
5+
def self.for(record_name:, table:, literal_values: {})
6+
klass = Class.new(self)
107

11-
private
8+
klass.define_method(:name) { record_name }
129

13-
def method_missing(method_name, *args)
14-
if @literal_values.key?(method_name)
15-
@literal_values[method_name]
16-
elsif method_name == :name
17-
@record_name
18-
else
19-
super
10+
literal_values.each do |col, val|
11+
klass.define_method(col) { val }
2012
end
21-
end
2213

23-
def respond_to_missing?(method_name, include_private = false)
24-
@literal_values.key?(method_name) || method_name == :name || super
14+
klass.new
2515
end
2616
end
2717
end

lib/fixture_bot/generator_proxy.rb

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,21 @@
22

33
module FixtureBot
44
class GeneratorProxy
5-
def initialize(table_def, generators)
6-
@table_def = table_def
7-
@generators = generators
8-
end
9-
10-
private
5+
def self.for(table_def, generators)
6+
klass = Class.new(self)
117

12-
def method_missing(method_name, *args, &block)
13-
if @table_def.columns.include?(method_name)
14-
raise ArgumentError, "#{method_name} requires a block" unless block
15-
@generators[method_name] = block
16-
else
17-
super
8+
table_def.columns.each do |col|
9+
klass.define_method(col) do |&block|
10+
raise ArgumentError, "#{col} requires a block" unless block
11+
@generators[col] = block
12+
end
1813
end
14+
15+
klass.new(generators)
1916
end
2017

21-
def respond_to_missing?(method_name, include_private = false)
22-
@table_def.columns.include?(method_name) || super
18+
def initialize(generators)
19+
@generators = generators
2320
end
2421
end
2522
end

lib/fixture_bot/record_builder.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def generated_values
6868
next if @row.literal_values.key?(col)
6969
next if foreign_key_values.key?(col)
7070

71-
context = GeneratorContext.new(
71+
context = GeneratorContext.for(
7272
record_name: @row.name,
7373
table: @row.table,
7474
literal_values: @row.literal_values

lib/fixture_bot/row_dsl.rb

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,40 @@ module FixtureBot
44
class RowDSL
55
attr_reader :literal_values, :association_refs, :tag_refs
66

7-
def initialize(table_def, schema)
8-
@table_def = table_def
9-
@schema = schema
10-
@literal_values = {}
11-
@association_refs = {}
12-
@tag_refs = {}
13-
@join_table_map = build_join_table_map
14-
end
7+
def self.for(table_def, schema)
8+
klass = Class.new(self)
159

16-
private
10+
table_def.columns.each do |col|
11+
klass.define_method(col) do |value|
12+
@literal_values[col] = value
13+
end
14+
end
1715

18-
def method_missing(method_name, *args)
19-
if @table_def.columns.include?(method_name)
20-
@literal_values[method_name] = args.first
21-
elsif (assoc = find_association(method_name))
22-
@association_refs[assoc.name] = args.first
23-
elsif (jt_info = @join_table_map[method_name])
24-
@tag_refs[jt_info[:join_table]] = { table: jt_info[:other_table], refs: args }
25-
else
26-
super
16+
table_def.belongs_to_associations.each do |assoc|
17+
klass.define_method(assoc.name) do |ref|
18+
@association_refs[assoc.name] = ref
19+
end
2720
end
28-
end
2921

30-
def respond_to_missing?(method_name, include_private = false)
31-
@table_def.columns.include?(method_name) ||
32-
find_association(method_name) ||
33-
@join_table_map.key?(method_name) ||
34-
super
35-
end
22+
schema.join_tables.each_value do |jt|
23+
if jt.left_table == table_def.name
24+
klass.define_method(jt.right_table) do |*refs|
25+
@tag_refs[jt.name] = { table: jt.right_table, refs: refs }
26+
end
27+
elsif jt.right_table == table_def.name
28+
klass.define_method(jt.left_table) do |*refs|
29+
@tag_refs[jt.name] = { table: jt.left_table, refs: refs }
30+
end
31+
end
32+
end
3633

37-
def find_association(name)
38-
@table_def.belongs_to_associations.find { |a| a.name == name }
34+
klass.new
3935
end
4036

41-
def build_join_table_map
42-
map = {}
43-
@schema.join_tables.each_value do |jt|
44-
if jt.left_table == @table_def.name
45-
map[jt.right_table] = { join_table: jt.name, other_table: jt.right_table }
46-
elsif jt.right_table == @table_def.name
47-
map[jt.left_table] = { join_table: jt.name, other_table: jt.left_table }
48-
end
49-
end
50-
map
37+
def initialize
38+
@literal_values = {}
39+
@association_refs = {}
40+
@tag_refs = {}
5141
end
5242
end
5343
end

0 commit comments

Comments
 (0)