Skip to content

Commit 84c12fb

Browse files
committed
Replace Class.new + define_method with method_missing
Simpler DSL dispatch using standard method_missing/respond_to_missing? instead of anonymous subclasses with baked-in define_method closures.
1 parent 5faa4fe commit 84c12fb

6 files changed

Lines changed: 97 additions & 75 deletions

File tree

lib/fixturebot.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.for(schema)
19+
definition = Definition.new(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.for(schema)
26+
definition = Definition.new(schema)
2727
definition.instance_eval(content, fixtures_path, 1)
2828
FixtureSet.new(schema, definition)
2929
end

lib/fixturebot/definition.rb

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,45 @@ 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-
337
def initialize(schema)
348
@schema = schema
359
@generators = {}
3610
@rows = []
11+
@singular_to_table = {}
3712

3813
schema.tables.each_value do |table_def|
3914
@generators[table_def.name] = {}
15+
@singular_to_table[table_def.singular_name] = table_def
16+
end
17+
end
18+
19+
private
20+
21+
def method_missing(method_name, *args, &block)
22+
table_def = @singular_to_table[method_name]
23+
return super unless table_def
24+
25+
record_name = args.first
26+
27+
if record_name.nil? && block.nil?
28+
GeneratorProxy.new(table_def, @generators[table_def.name])
29+
elsif record_name
30+
row_dsl = RowDSL.new(table_def, @schema)
31+
row_dsl.instance_eval(&block) if block
32+
@rows << Row.new(
33+
table: table_def.name,
34+
name: record_name,
35+
literal_values: row_dsl.literal_values,
36+
association_refs: row_dsl.association_refs,
37+
tag_refs: row_dsl.tag_refs
38+
)
39+
else
40+
raise ArgumentError, "#{table_def.singular_name} requires a record name or no arguments"
4041
end
4142
end
43+
44+
def respond_to_missing?(method_name, include_private = false)
45+
@singular_to_table.key?(method_name) || super
46+
end
4247
end
4348
end

lib/fixturebot/generator_context.rb

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

33
module FixtureBot
44
class GeneratorContext
5-
def self.for(record_name:, table:, literal_values: {})
6-
klass = Class.new(self)
5+
def initialize(record_name:, literal_values: {})
6+
@record_name = record_name
7+
@literal_values = literal_values
8+
end
9+
10+
def name
11+
@literal_values.key?(:name) ? @literal_values[:name] : @record_name
12+
end
713

8-
klass.define_method(:name) { record_name }
14+
private
915

10-
literal_values.each do |col, val|
11-
klass.define_method(col) { val }
16+
def method_missing(method_name, *args, &block)
17+
if @literal_values.key?(method_name)
18+
@literal_values[method_name]
19+
else
20+
super
1221
end
22+
end
1323

14-
klass.new
24+
def respond_to_missing?(method_name, include_private = false)
25+
@literal_values.key?(method_name) || super
1526
end
1627
end
1728
end

lib/fixturebot/generator_proxy.rb

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

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

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
13-
end
10+
private
1411

15-
klass.new(generators)
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
18+
end
1619
end
1720

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

lib/fixturebot/record_builder.rb

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

71-
context = GeneratorContext.for(
71+
context = GeneratorContext.new(
7272
record_name: @row.name,
73-
table: @row.table,
7473
literal_values: @row.literal_values
7574
)
7675
result[col] = context.instance_eval(&block)

lib/fixturebot/row_dsl.rb

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

7-
def self.for(table_def, schema)
8-
klass = Class.new(self)
9-
10-
table_def.columns.each do |col|
11-
klass.define_method(col) do |value|
12-
@literal_values[col] = value
13-
end
14-
end
7+
def initialize(table_def, schema)
8+
@table_def = table_def
9+
@schema = schema
10+
@literal_values = {}
11+
@association_refs = {}
12+
@tag_refs = {}
13+
end
1514

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
20-
end
15+
private
2116

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
17+
def method_missing(method_name, *args, &block)
18+
if @table_def.columns.include?(method_name)
19+
@literal_values[method_name] = args.first
20+
elsif (assoc = @table_def.belongs_to_associations.find { |a| a.name == method_name })
21+
@association_refs[assoc.name] = args.first
22+
elsif (jt = find_join_table(method_name))
23+
@tag_refs[jt[:join_table].name] = { table: jt[:other_table], refs: args }
24+
else
25+
super
3226
end
27+
end
3328

34-
klass.new
29+
def respond_to_missing?(method_name, include_private = false)
30+
@table_def.columns.include?(method_name) ||
31+
@table_def.belongs_to_associations.any? { |a| a.name == method_name } ||
32+
!!find_join_table(method_name) ||
33+
super
3534
end
3635

37-
def initialize
38-
@literal_values = {}
39-
@association_refs = {}
40-
@tag_refs = {}
36+
def find_join_table(method_name)
37+
@schema.join_tables.each_value do |jt|
38+
if jt.left_table == @table_def.name && jt.right_table == method_name
39+
return { join_table: jt, other_table: jt.right_table }
40+
elsif jt.right_table == @table_def.name && jt.left_table == method_name
41+
return { join_table: jt, other_table: jt.left_table }
42+
end
43+
end
44+
nil
4145
end
4246
end
4347
end

0 commit comments

Comments
 (0)