factory_seeder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +111 -0
- data/LICENSE.txt +21 -0
- data/README.md +445 -0
- data/app/assets/stylesheets/factory_seeder.css +637 -0
- data/app/controllers/factory_seeder/application_controller.rb +8 -0
- data/app/controllers/factory_seeder/custom_seeds_controller.rb +134 -0
- data/app/controllers/factory_seeder/dashboard_controller.rb +36 -0
- data/app/controllers/factory_seeder/factory_controller.rb +70 -0
- data/app/views/factory_seeder/custom_seeds/index.html.erb +51 -0
- data/app/views/factory_seeder/custom_seeds/show.html.erb +113 -0
- data/app/views/factory_seeder/dashboard/index.html.erb +99 -0
- data/app/views/factory_seeder/factory/index.html.erb +71 -0
- data/app/views/factory_seeder/factory/show.html.erb +108 -0
- data/app/views/factory_seeder/seeds/show.html.erb +2 -0
- data/app/views/layouts/factory_seeder/application.html.erb +25 -0
- data/bin/factory_seeder +27 -0
- data/config/factory_seeder.rb +24 -0
- data/config/routes.rb +20 -0
- data/lib/factory_seeder/asset_helper.rb +34 -0
- data/lib/factory_seeder/cli.rb +352 -0
- data/lib/factory_seeder/configuration.rb +32 -0
- data/lib/factory_seeder/custom_seed_loader.rb +39 -0
- data/lib/factory_seeder/engine.rb +16 -0
- data/lib/factory_seeder/execution_log_store.rb +48 -0
- data/lib/factory_seeder/factory_scanner.rb +149 -0
- data/lib/factory_seeder/loader.rb +26 -0
- data/lib/factory_seeder/rails_integration.rb +29 -0
- data/lib/factory_seeder/seed.rb +102 -0
- data/lib/factory_seeder/seed_builder.rb +67 -0
- data/lib/factory_seeder/seed_generator.rb +305 -0
- data/lib/factory_seeder/seed_manager.rb +128 -0
- data/lib/factory_seeder/seeder.rb +41 -0
- data/lib/factory_seeder/version.rb +5 -0
- data/lib/factory_seeder/web_interface.rb +119 -0
- data/lib/factory_seeder.rb +209 -0
- data/templates/seed_template.rb +84 -0
- metadata +276 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactorySeeder
|
|
4
|
+
class SeedGenerator
|
|
5
|
+
def initialize
|
|
6
|
+
@generated_records = []
|
|
7
|
+
@defined_seeds = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def preview(factory_name, count = 1, traits = [], attributes = {})
|
|
11
|
+
traits = traits.map(&:to_sym)
|
|
12
|
+
filtered_attributes = filter_association_attributes(factory_name, attributes)
|
|
13
|
+
|
|
14
|
+
preview_data = []
|
|
15
|
+
count.times do |i|
|
|
16
|
+
begin
|
|
17
|
+
# Build without saving
|
|
18
|
+
record = FactoryBot.build(factory_name, *traits, filtered_attributes)
|
|
19
|
+
rescue NoMethodError => e
|
|
20
|
+
# Check if this is the specific error about calling a method on CollectionProxy
|
|
21
|
+
raise unless e.message.include?('CollectionProxy') && e.message.include?('undefined method')
|
|
22
|
+
|
|
23
|
+
method_name = e.message.match(/undefined method `(\w+)'/)&.[](1)
|
|
24
|
+
# Try to identify which attribute might be causing the issue
|
|
25
|
+
problematic_attrs = attributes.select { |k, v| v.to_s == method_name || k.to_s == method_name }
|
|
26
|
+
if problematic_attrs.any?
|
|
27
|
+
raise NoMethodError,
|
|
28
|
+
"#{e.message}. This might be caused by passing '#{problematic_attrs.keys.first}' as an attribute. Collection associations (has_many, has_and_belongs_to_many) cannot be set directly via attributes."
|
|
29
|
+
else
|
|
30
|
+
raise NoMethodError,
|
|
31
|
+
"#{e.message}. This might be caused by an attribute that matches an association name. Try removing association-related attributes from your attributes hash."
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
preview_data << {
|
|
36
|
+
index: i + 1,
|
|
37
|
+
attributes: record.attributes,
|
|
38
|
+
associations: extract_associations(record)
|
|
39
|
+
}
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
preview_data << {
|
|
42
|
+
index: i + 1,
|
|
43
|
+
error: e.message
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
factory: factory_name,
|
|
49
|
+
count: count,
|
|
50
|
+
traits: traits,
|
|
51
|
+
attributes: attributes,
|
|
52
|
+
preview: preview_data
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def generate(factory_name, count = 1, traits = [], attributes = {}, strategy = 'create')
|
|
57
|
+
traits = traits.map(&:to_sym)
|
|
58
|
+
filtered_attributes = filter_association_attributes(factory_name, attributes)
|
|
59
|
+
|
|
60
|
+
FactorySeeder.clear_execution_logs!
|
|
61
|
+
FactorySeeder.log_info("Starting #{factory_name} generation", count: count, traits: traits, strategy: strategy)
|
|
62
|
+
|
|
63
|
+
generated_count = 0
|
|
64
|
+
errors = []
|
|
65
|
+
|
|
66
|
+
count.times do |i|
|
|
67
|
+
begin
|
|
68
|
+
if strategy == 'create'
|
|
69
|
+
record = FactoryBot.create(factory_name, *traits, filtered_attributes)
|
|
70
|
+
else
|
|
71
|
+
record = FactoryBot.build(factory_name, *traits, filtered_attributes)
|
|
72
|
+
record.save! if record.respond_to?(:save!)
|
|
73
|
+
end
|
|
74
|
+
rescue NoMethodError => e
|
|
75
|
+
# Check if this is the specific error about calling a method on CollectionProxy
|
|
76
|
+
raise unless e.message.include?('CollectionProxy') && e.message.include?('undefined method')
|
|
77
|
+
|
|
78
|
+
method_name = e.message.match(/undefined method `(\w+)'/)&.[](1)
|
|
79
|
+
# Try to identify which attribute might be causing the issue
|
|
80
|
+
problematic_attrs = attributes.select { |k, v| v.to_s == method_name || k.to_s == method_name }
|
|
81
|
+
if problematic_attrs.any?
|
|
82
|
+
raise NoMethodError,
|
|
83
|
+
"#{e.message}. This might be caused by passing '#{problematic_attrs.keys.first}' as an attribute. Collection associations (has_many, has_and_belongs_to_many) cannot be set directly via attributes."
|
|
84
|
+
else
|
|
85
|
+
raise NoMethodError,
|
|
86
|
+
"#{e.message}. This might be caused by an attribute that matches an association name. Try removing association-related attributes from your attributes hash."
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@generated_records << {
|
|
91
|
+
factory: factory_name,
|
|
92
|
+
record: record,
|
|
93
|
+
traits: traits,
|
|
94
|
+
attributes: attributes,
|
|
95
|
+
strategy: strategy
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
generated_count += 1
|
|
99
|
+
|
|
100
|
+
FactorySeeder.log("✅ Generated #{factory_name} ##{i + 1}", level: :success)
|
|
101
|
+
puts "✅ Generated #{factory_name} ##{i + 1}" if FactorySeeder.configuration.verbose
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
error_msg = "Failed to generate #{factory_name} ##{i + 1}: #{e.message}"
|
|
104
|
+
errors << error_msg
|
|
105
|
+
FactorySeeder.log(error_msg, level: :error)
|
|
106
|
+
puts "❌ #{error_msg}" if FactorySeeder.configuration.verbose
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
FactorySeeder.log_info('Completed generation', generated_count: generated_count, errors: errors.count)
|
|
110
|
+
logs = FactorySeeder.normalized_logs(FactorySeeder.execution_logs)
|
|
111
|
+
FactorySeeder.clear_execution_logs!
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
factory: factory_name,
|
|
115
|
+
generated_records: @generated_records,
|
|
116
|
+
count: generated_count,
|
|
117
|
+
requested_count: count,
|
|
118
|
+
traits: traits,
|
|
119
|
+
attributes: attributes,
|
|
120
|
+
strategy: strategy,
|
|
121
|
+
errors: errors,
|
|
122
|
+
logs: logs
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def summary
|
|
127
|
+
return 'No records generated' if @generated_records.empty?
|
|
128
|
+
|
|
129
|
+
summary = "📊 Generation Summary:\n"
|
|
130
|
+
summary += "#{'=' * 50}\n"
|
|
131
|
+
|
|
132
|
+
by_factory = @generated_records.group_by { |r| r[:factory] }
|
|
133
|
+
|
|
134
|
+
by_factory.each do |factory_name, records|
|
|
135
|
+
summary += "\n🏭 #{factory_name}: #{records.count} records\n"
|
|
136
|
+
records.each_with_index do |record, index|
|
|
137
|
+
summary += " #{index + 1}. Strategy: #{record[:strategy]}"
|
|
138
|
+
summary += ", Traits: #{record[:traits].join(', ')}" if record[:traits].any?
|
|
139
|
+
summary += ", Attributes: #{record[:attributes]}" if record[:attributes].any?
|
|
140
|
+
summary += "\n"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
summary
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def define_seed(name, &block)
|
|
148
|
+
@defined_seeds[name.to_sym] = block
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def run_seed(name)
|
|
152
|
+
seed_name = name.to_sym
|
|
153
|
+
unless @defined_seeds.key?(seed_name)
|
|
154
|
+
raise "Seed '#{name}' not found. Available seeds: #{@defined_seeds.keys.join(', ')}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
@defined_seeds[seed_name].call(self)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def list_seeds
|
|
161
|
+
@defined_seeds.keys
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def has_seed?(name)
|
|
165
|
+
@defined_seeds.key?(name.to_sym)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def run_all_seeds
|
|
169
|
+
puts '🌱 Running all defined seeds...'
|
|
170
|
+
@defined_seeds.each do |name, block|
|
|
171
|
+
puts "\n--- Running seed: #{name} ---"
|
|
172
|
+
block.call(self)
|
|
173
|
+
end
|
|
174
|
+
puts "\n✅ All seeds completed successfully"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def filter_association_attributes(factory_name, attributes)
|
|
180
|
+
return attributes if attributes.empty?
|
|
181
|
+
|
|
182
|
+
begin
|
|
183
|
+
# Get the factory
|
|
184
|
+
factory = FactoryBot.factories.find { |f| f.name.to_s == factory_name.to_s }
|
|
185
|
+
return attributes unless factory
|
|
186
|
+
|
|
187
|
+
# Get associations from factory definition
|
|
188
|
+
factory_associations = []
|
|
189
|
+
factory.definition.declarations.each do |declaration|
|
|
190
|
+
factory_associations << declaration.name.to_s if declaration.is_a?(FactoryBot::Declaration::Association)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Get the model class from the factory
|
|
194
|
+
model_class = factory.build_class
|
|
195
|
+
model_associations = []
|
|
196
|
+
|
|
197
|
+
if model_class.respond_to?(:reflect_on_all_associations)
|
|
198
|
+
# Get all has_many and has_and_belongs_to_many associations from the model
|
|
199
|
+
model_associations = model_class.reflect_on_all_associations.select do |assoc|
|
|
200
|
+
%i[has_many has_and_belongs_to_many].include?(assoc.macro)
|
|
201
|
+
end.map { |assoc| assoc.name.to_s }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Combine factory and model associations
|
|
205
|
+
all_collection_associations = (factory_associations + model_associations).uniq
|
|
206
|
+
|
|
207
|
+
# Get all associations (including belongs_to and has_one) for additional safety
|
|
208
|
+
all_associations = []
|
|
209
|
+
if model_class.respond_to?(:reflect_on_all_associations)
|
|
210
|
+
all_associations = model_class.reflect_on_all_associations.map { |assoc| assoc.name.to_s }
|
|
211
|
+
end
|
|
212
|
+
all_associations = (all_associations + factory_associations).uniq
|
|
213
|
+
|
|
214
|
+
# Filter out attributes that match collection associations
|
|
215
|
+
filtered = attributes.dup
|
|
216
|
+
all_collection_associations.each do |assoc_name|
|
|
217
|
+
filtered.delete(assoc_name.to_sym)
|
|
218
|
+
filtered.delete(assoc_name.to_s)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Additional safety: filter out any attribute whose value is a symbol
|
|
222
|
+
# and whose key matches an association name (to prevent method calls on associations)
|
|
223
|
+
filtered_before_symbol_check = filtered.dup
|
|
224
|
+
filtered.delete_if do |key, value|
|
|
225
|
+
key_str = key.to_s
|
|
226
|
+
# If the value is a symbol and the key matches an association name,
|
|
227
|
+
# FactoryBot might try to call that symbol as a method on the association
|
|
228
|
+
if value.is_a?(Symbol) && all_associations.include?(key_str)
|
|
229
|
+
if FactorySeeder.configuration.verbose
|
|
230
|
+
puts "🔍 Filtering attribute '#{key_str}' (value: #{value}) - matches association name"
|
|
231
|
+
end
|
|
232
|
+
true
|
|
233
|
+
# Also filter if key looks like an association name (plural, ends with _ids, etc.)
|
|
234
|
+
elsif value.is_a?(Symbol) && (
|
|
235
|
+
key_str.end_with?('_ids') ||
|
|
236
|
+
(key_str.pluralize == key_str && key_str.singularize != key_str)
|
|
237
|
+
)
|
|
238
|
+
# Double-check if it's actually an association
|
|
239
|
+
if model_class.respond_to?(:reflect_on_all_associations)
|
|
240
|
+
association = model_class.reflect_on_association(key_str.singularize.to_sym) ||
|
|
241
|
+
model_class.reflect_on_association(key_str.to_sym)
|
|
242
|
+
if !association.nil?
|
|
243
|
+
if FactorySeeder.configuration.verbose
|
|
244
|
+
puts "🔍 Filtering attribute '#{key_str}' (value: #{value}) - detected as association"
|
|
245
|
+
end
|
|
246
|
+
true
|
|
247
|
+
else
|
|
248
|
+
false
|
|
249
|
+
end
|
|
250
|
+
else
|
|
251
|
+
false
|
|
252
|
+
end
|
|
253
|
+
else
|
|
254
|
+
false
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
if FactorySeeder.configuration.verbose && filtered_before_symbol_check != filtered
|
|
259
|
+
removed = filtered_before_symbol_check.keys - filtered.keys
|
|
260
|
+
puts "🔍 Filtered out #{removed.count} association-related attributes: #{removed.join(', ')}" if removed.any?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
filtered
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
# If we can't determine associations, try a more aggressive filter
|
|
266
|
+
# Filter out any attribute that might be problematic
|
|
267
|
+
filtered = attributes.dup
|
|
268
|
+
|
|
269
|
+
# Remove any attribute whose value is a symbol (which FactoryBot might try to call as a method)
|
|
270
|
+
# but only if we're not sure it's safe
|
|
271
|
+
filtered.delete_if do |key, value|
|
|
272
|
+
# If value is a symbol and key looks like it could be an association name
|
|
273
|
+
value.is_a?(Symbol) && (
|
|
274
|
+
key.to_s.end_with?('_ids') ||
|
|
275
|
+
key.to_s.pluralize == key.to_s ||
|
|
276
|
+
key.to_s.singularize != key.to_s
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
if FactorySeeder.configuration.verbose
|
|
281
|
+
puts "⚠️ Warning: Could not fully filter association attributes: #{e.message}"
|
|
282
|
+
end
|
|
283
|
+
filtered
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def extract_associations(record)
|
|
288
|
+
associations = {}
|
|
289
|
+
|
|
290
|
+
record.class.reflect_on_all_associations.each do |association|
|
|
291
|
+
if association.macro == :belongs_to
|
|
292
|
+
associated_record = record.send(association.name)
|
|
293
|
+
associations[association.name] = associated_record&.id if associated_record
|
|
294
|
+
elsif association.macro == :has_many
|
|
295
|
+
associated_records = record.send(association.name)
|
|
296
|
+
associations[association.name] = associated_records.map(&:id) if associated_records.any?
|
|
297
|
+
end
|
|
298
|
+
rescue StandardError => e
|
|
299
|
+
associations[association.name] = "Error: #{e.message}"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
associations
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactorySeeder
|
|
4
|
+
class SeedManager
|
|
5
|
+
attr_reader :seeds
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@seeds = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def register(seed)
|
|
12
|
+
raise ArgumentError, 'Seed must be a FactorySeeder::Seed instance' unless seed.is_a?(Seed)
|
|
13
|
+
|
|
14
|
+
@seeds[seed.name] = seed
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def define(name, builder_block = nil, &execution_block)
|
|
18
|
+
builder = SeedBuilder.new(name)
|
|
19
|
+
builder_block&.call(builder)
|
|
20
|
+
seed = builder.build(&execution_block)
|
|
21
|
+
register(seed)
|
|
22
|
+
seed
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def find(name)
|
|
26
|
+
@seeds[name.to_sym]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def exists?(name)
|
|
30
|
+
@seeds.key?(name.to_sym)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def list
|
|
34
|
+
@seeds.values
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def list_names
|
|
38
|
+
@seeds.keys
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run(name, **kwargs)
|
|
42
|
+
seed = find(name)
|
|
43
|
+
raise ArgumentError, "Seed '#{name}' not found" unless seed
|
|
44
|
+
|
|
45
|
+
FactorySeeder.clear_execution_logs!
|
|
46
|
+
FactorySeeder.log_info("Running custom seed '#{name}'", kwargs: kwargs)
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
result = seed.call(**kwargs)
|
|
50
|
+
FactorySeeder.log_success("Seed '#{name}' completed", result: result)
|
|
51
|
+
logs = FactorySeeder.normalized_logs(FactorySeeder.execution_logs)
|
|
52
|
+
FactorySeeder.clear_execution_logs!
|
|
53
|
+
{
|
|
54
|
+
success: true,
|
|
55
|
+
seed_name: name,
|
|
56
|
+
result: result,
|
|
57
|
+
message: "Seed '#{name}' executed successfully",
|
|
58
|
+
logs: logs
|
|
59
|
+
}
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
FactorySeeder.log("Seed '#{name}' failed: #{e.message}", level: :error)
|
|
62
|
+
logs = FactorySeeder.normalized_logs(FactorySeeder.execution_logs)
|
|
63
|
+
FactorySeeder.clear_execution_logs!
|
|
64
|
+
{
|
|
65
|
+
success: false,
|
|
66
|
+
seed_name: name,
|
|
67
|
+
error: e.message,
|
|
68
|
+
message: "Seed '#{name}' failed: #{e.message}",
|
|
69
|
+
logs: logs
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def run_all(**global_kwargs)
|
|
75
|
+
results = []
|
|
76
|
+
|
|
77
|
+
@seeds.each_key do |name|
|
|
78
|
+
result = run(name, **global_kwargs)
|
|
79
|
+
results << result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
total_seeds: @seeds.count,
|
|
84
|
+
successful: results.count { |r| r[:success] },
|
|
85
|
+
failed: results.count { |r| !r[:success] },
|
|
86
|
+
results: results
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_seed(name, **kwargs)
|
|
91
|
+
seed = find(name)
|
|
92
|
+
raise ArgumentError, "Seed '#{name}' not found" unless seed
|
|
93
|
+
|
|
94
|
+
seed.validate_parameters!(kwargs)
|
|
95
|
+
true
|
|
96
|
+
rescue StandardError
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def get_seed_info(name)
|
|
101
|
+
seed = find(name)
|
|
102
|
+
return nil unless seed
|
|
103
|
+
|
|
104
|
+
seed.to_h
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def search(query)
|
|
108
|
+
query = query.to_s.downcase
|
|
109
|
+
@seeds.values.select do |seed|
|
|
110
|
+
seed.name.to_s.downcase.include?(query) ||
|
|
111
|
+
seed.description.downcase.include?(query) ||
|
|
112
|
+
seed.parameter_names.any? { |param| param.to_s.downcase.include?(query) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def clear
|
|
117
|
+
@seeds.clear
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def count
|
|
121
|
+
@seeds.count
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def empty?
|
|
125
|
+
@seeds.empty?
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactorySeeder
|
|
4
|
+
class Seeder
|
|
5
|
+
attr_reader :seeds
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@seeds = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def seed(name, &block)
|
|
12
|
+
@seeds[name.to_sym] = block
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(name, **kwargs)
|
|
16
|
+
name = to_sym(name)
|
|
17
|
+
seed = @seeds[name]
|
|
18
|
+
|
|
19
|
+
raise ArgumentError, "Seed #{name} not defined" unless seed
|
|
20
|
+
|
|
21
|
+
result = @seeds[name].call(**kwargs)
|
|
22
|
+
if result
|
|
23
|
+
puts "🌱 Seed #{name} generated successfully"
|
|
24
|
+
true
|
|
25
|
+
else
|
|
26
|
+
puts "⚠️ Seed #{name} failed"
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def to_sym(name)
|
|
34
|
+
if name.is_a?(Symbol)
|
|
35
|
+
name
|
|
36
|
+
else
|
|
37
|
+
name.to_sym
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sinatra/base'
|
|
4
|
+
require 'sinatra/reloader'
|
|
5
|
+
|
|
6
|
+
module FactorySeeder
|
|
7
|
+
class WebInterface < Sinatra::Base
|
|
8
|
+
configure do
|
|
9
|
+
set :public_folder, File.join(File.dirname(__FILE__), '..', '..', 'public')
|
|
10
|
+
set :views, File.join(File.dirname(__FILE__), '..', '..', 'views')
|
|
11
|
+
enable :reloader
|
|
12
|
+
set :bind, '0.0.0.0'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
before do
|
|
16
|
+
FactorySeeder.reload!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
get '/' do
|
|
20
|
+
@factories = FactorySeeder.scan_factories
|
|
21
|
+
erb :index
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
get '/factory/:name' do
|
|
25
|
+
@factory_name = params[:name]
|
|
26
|
+
@factories = FactorySeeder.scan_factories
|
|
27
|
+
@factory = @factories[@factory_name]
|
|
28
|
+
|
|
29
|
+
if @factory
|
|
30
|
+
erb :factory_detail
|
|
31
|
+
else
|
|
32
|
+
status 404
|
|
33
|
+
'Factory not found'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
post '/generate' do
|
|
38
|
+
content_type :json
|
|
39
|
+
|
|
40
|
+
# Parse JSON body if present
|
|
41
|
+
if request.content_type == 'application/json'
|
|
42
|
+
data = JSON.parse(request.body.read)
|
|
43
|
+
factory_name = data['factory']
|
|
44
|
+
count = data['count'].to_i
|
|
45
|
+
traits = data['traits']
|
|
46
|
+
attributes = data['attributes'] || {}
|
|
47
|
+
else
|
|
48
|
+
factory_name = params[:factory]
|
|
49
|
+
count = params[:count].to_i
|
|
50
|
+
traits = params[:traits]
|
|
51
|
+
attributes = params[:attributes] || {}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Parse traits if it's a string
|
|
55
|
+
traits = if traits.is_a?(String) && !traits.empty?
|
|
56
|
+
traits.split(',').map(&:strip)
|
|
57
|
+
elsif traits.is_a?(Array)
|
|
58
|
+
traits.flatten.map(&:strip)
|
|
59
|
+
else
|
|
60
|
+
[]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
generator = SeedGenerator.new
|
|
65
|
+
result = generator.generate(factory_name, count, traits, attributes)
|
|
66
|
+
|
|
67
|
+
if result[:errors].any?
|
|
68
|
+
{ success: false, error: result[:errors].join(', ') }.to_json
|
|
69
|
+
else
|
|
70
|
+
{ success: true, message: "Created #{result[:count]} #{factory_name} records" }.to_json
|
|
71
|
+
end
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
{ success: false, error: e.message }.to_json
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
get '/api/factories' do
|
|
78
|
+
content_type :json
|
|
79
|
+
FactorySeeder.scan_factories.to_json
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
get '/api/factory/:name/preview' do
|
|
83
|
+
content_type :json
|
|
84
|
+
|
|
85
|
+
factory_name = params[:name]
|
|
86
|
+
traits = params[:traits]
|
|
87
|
+
attributes = params[:attributes] || {}
|
|
88
|
+
|
|
89
|
+
# Parse traits if it's a string
|
|
90
|
+
traits = if traits.is_a?(String)
|
|
91
|
+
traits.split(',').map(&:strip).map(&:to_sym)
|
|
92
|
+
elsif traits.is_a?(Array)
|
|
93
|
+
traits.flatten.map(&:strip).map(&:to_sym)
|
|
94
|
+
else
|
|
95
|
+
[]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Parse attributes if it's a string
|
|
99
|
+
if attributes.is_a?(String) && !attributes.empty?
|
|
100
|
+
begin
|
|
101
|
+
attributes = JSON.parse(attributes)
|
|
102
|
+
rescue JSON::ParserError
|
|
103
|
+
attributes = {}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
begin
|
|
108
|
+
sample = FactoryBot.build(factory_name, *traits, attributes)
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
success: true,
|
|
112
|
+
attributes: sample.attributes.reject { |k, _| %w[id created_at updated_at].include?(k) }
|
|
113
|
+
}.to_json
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
{ success: false, error: e.message }.to_json
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|