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,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactorySeeder
|
|
4
|
+
# Temporary in-memory storage for execution logs
|
|
5
|
+
# Used to implement PRG pattern without session overflow
|
|
6
|
+
class ExecutionLogStore
|
|
7
|
+
EXPIRY_TIME = 300 # 5 minutes
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def store(logs, flash_type: nil, flash_message: nil)
|
|
11
|
+
cleanup_expired
|
|
12
|
+
id = SecureRandom.hex(8)
|
|
13
|
+
storage[id] = {
|
|
14
|
+
logs: logs,
|
|
15
|
+
flash_type: flash_type,
|
|
16
|
+
flash_message: flash_message,
|
|
17
|
+
created_at: Time.now
|
|
18
|
+
}
|
|
19
|
+
id
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def retrieve(id)
|
|
23
|
+
return nil if id.blank?
|
|
24
|
+
|
|
25
|
+
cleanup_expired
|
|
26
|
+
data = storage.delete(id)
|
|
27
|
+
return nil unless data
|
|
28
|
+
|
|
29
|
+
data
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def clear
|
|
33
|
+
@storage = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def storage
|
|
39
|
+
@storage ||= {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cleanup_expired
|
|
43
|
+
cutoff = Time.now - EXPIRY_TIME
|
|
44
|
+
storage.delete_if { |_id, data| data[:created_at] < cutoff }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactorySeeder
|
|
4
|
+
class FactoryScanner
|
|
5
|
+
def initialize
|
|
6
|
+
@factories = {}
|
|
7
|
+
@factory_paths = find_factory_paths
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def scan
|
|
11
|
+
# Setup Rails integration if available
|
|
12
|
+
FactorySeeder::RailsIntegration.setup
|
|
13
|
+
FactorySeeder::RailsIntegration.load_models
|
|
14
|
+
|
|
15
|
+
load_factories
|
|
16
|
+
analyze_factories
|
|
17
|
+
@factories
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def find_factory_paths
|
|
23
|
+
paths = []
|
|
24
|
+
|
|
25
|
+
# Rails convention
|
|
26
|
+
paths << 'spec/factories' if Dir.exist?('spec/factories')
|
|
27
|
+
paths << 'test/factories' if Dir.exist?('test/factories')
|
|
28
|
+
paths << 'factories' if Dir.exist?('factories')
|
|
29
|
+
|
|
30
|
+
# Custom paths from configuration
|
|
31
|
+
paths.concat(FactorySeeder.configuration.factory_paths)
|
|
32
|
+
|
|
33
|
+
paths.uniq
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load_factories
|
|
37
|
+
@factory_paths.each do |path|
|
|
38
|
+
Dir.glob("#{path}/**/*.rb").each do |file|
|
|
39
|
+
load file
|
|
40
|
+
rescue FactoryBot::DuplicateDefinitionError => e
|
|
41
|
+
# Skip if factory is already registered
|
|
42
|
+
puts "⚠️ Factory already registered: #{e.message}" if FactorySeeder.configuration.verbose
|
|
43
|
+
rescue NameError => e
|
|
44
|
+
# Handle uninitialized constant errors
|
|
45
|
+
puts "⚠️ Model not loaded yet: #{e.message}" if FactorySeeder.configuration.verbose
|
|
46
|
+
# Store the file to retry later
|
|
47
|
+
@retry_files ||= []
|
|
48
|
+
@retry_files << file
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
# Handle other errors that might occur during factory loading
|
|
51
|
+
puts "⚠️ Error loading factory file #{file}: #{e.message}" if FactorySeeder.configuration.verbose
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Retry loading files that failed due to missing models
|
|
56
|
+
retry_loading_factories if @retry_files&.any?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def retry_loading_factories
|
|
60
|
+
return unless @retry_files&.any?
|
|
61
|
+
|
|
62
|
+
puts '🔄 Retrying to load factories that failed...' if FactorySeeder.configuration.verbose
|
|
63
|
+
|
|
64
|
+
# Try to load models again before retrying factories
|
|
65
|
+
FactorySeeder::RailsIntegration.load_models
|
|
66
|
+
|
|
67
|
+
@retry_files.each do |file|
|
|
68
|
+
load file
|
|
69
|
+
puts "✅ Successfully loaded: #{file}" if FactorySeeder.configuration.verbose
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
puts "❌ Still failed to load #{file}: #{e.message}" if FactorySeeder.configuration.verbose
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@retry_files = []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def analyze_factories
|
|
78
|
+
FactoryBot.factories.each do |factory|
|
|
79
|
+
factory_name = factory.name.to_s
|
|
80
|
+
begin
|
|
81
|
+
# Use a safer approach to get class name without building the class
|
|
82
|
+
class_name = factory_name.classify
|
|
83
|
+
|
|
84
|
+
# Try to get the actual class name if possible, but don't fail if it doesn't work
|
|
85
|
+
begin
|
|
86
|
+
class_name = factory.build_class.name if factory.respond_to?(:build_class) && factory.build_class
|
|
87
|
+
rescue NameError, StandardError => e
|
|
88
|
+
# If we can't get the actual class name, use the inferred one
|
|
89
|
+
if FactorySeeder.configuration.verbose
|
|
90
|
+
puts "⚠️ Using inferred class name for '#{factory_name}': #{e.message}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@factories[factory_name] = {
|
|
95
|
+
name: factory_name,
|
|
96
|
+
class_name: class_name,
|
|
97
|
+
traits: extract_traits(factory),
|
|
98
|
+
associations: extract_associations(factory),
|
|
99
|
+
attributes: extract_attributes(factory)
|
|
100
|
+
}
|
|
101
|
+
rescue NameError => e
|
|
102
|
+
# Skip factories with missing model classes
|
|
103
|
+
puts "⚠️ Skipping factory '#{factory_name}': #{e.message}" if FactorySeeder.configuration.verbose
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
# Skip factories with other errors
|
|
106
|
+
puts "⚠️ Error analyzing factory '#{factory_name}': #{e.message}" if FactorySeeder.configuration.verbose
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def extract_traits(factory)
|
|
112
|
+
factory.definition.defined_traits.map(&:name).map(&:to_s)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def extract_associations(factory)
|
|
116
|
+
associations = []
|
|
117
|
+
|
|
118
|
+
factory.definition.declarations.each do |declaration|
|
|
119
|
+
next unless declaration.is_a?(FactoryBot::Declaration::Association)
|
|
120
|
+
|
|
121
|
+
# Try to get factory name from the association name
|
|
122
|
+
factory_name = declaration.name.to_s
|
|
123
|
+
|
|
124
|
+
associations << {
|
|
125
|
+
name: factory_name,
|
|
126
|
+
factory: factory_name.singularize, # Assume factory name matches association
|
|
127
|
+
strategy: 'create'
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
associations
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def extract_attributes(factory)
|
|
135
|
+
attributes = []
|
|
136
|
+
|
|
137
|
+
factory.definition.declarations.each do |declaration|
|
|
138
|
+
next if declaration.is_a?(FactoryBot::Declaration::Association)
|
|
139
|
+
|
|
140
|
+
attributes << {
|
|
141
|
+
name: declaration.name.to_s,
|
|
142
|
+
type: declaration.class.name.demodulize.downcase
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
attributes
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zeitwerk'
|
|
4
|
+
|
|
5
|
+
module FactorySeeder
|
|
6
|
+
class Loader
|
|
7
|
+
class << self
|
|
8
|
+
def setup
|
|
9
|
+
return if @loader
|
|
10
|
+
|
|
11
|
+
@loader = Zeitwerk::Loader.new
|
|
12
|
+
@loader.inflector.inflect('cli' => 'CLI')
|
|
13
|
+
@loader.push_dir(File.expand_path(__dir__), namespace: FactorySeeder)
|
|
14
|
+
@loader.ignore("#{__dir__}/version.rb")
|
|
15
|
+
@loader.enable_reloading
|
|
16
|
+
@loader.setup
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reload!
|
|
20
|
+
return unless @loader&.reloading_enabled?
|
|
21
|
+
|
|
22
|
+
@loader.reload
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactorySeeder
|
|
4
|
+
module RailsIntegration
|
|
5
|
+
def self.setup
|
|
6
|
+
return unless defined?(Rails)
|
|
7
|
+
|
|
8
|
+
# Force eager loading when Rails hasn't done it (development/test with lazy loading)
|
|
9
|
+
if Rails.respond_to?(:application) && Rails.application && !Rails.application.config.eager_load
|
|
10
|
+
Rails.application.eager_load!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Add Rails-specific factory paths
|
|
14
|
+
FactorySeeder.configuration.factory_paths << 'spec/factories' if Dir.exist?('spec/factories')
|
|
15
|
+
FactorySeeder.configuration.factory_paths << 'test/factories' if Dir.exist?('test/factories')
|
|
16
|
+
|
|
17
|
+
# Enable verbose mode in development
|
|
18
|
+
FactorySeeder.configuration.verbose = Rails.env.development? if Rails.respond_to?(:env)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.load_models
|
|
22
|
+
return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
23
|
+
return if Rails.application.config.eager_load
|
|
24
|
+
|
|
25
|
+
# Force eager loading when Rails hasn't done it (development/test with lazy loading)
|
|
26
|
+
Rails.application.eager_load!
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactorySeeder
|
|
4
|
+
class Seed
|
|
5
|
+
attr_reader :name, :description, :parameters, :block, :metadata
|
|
6
|
+
|
|
7
|
+
def initialize(name, description: nil, parameters: {}, metadata: {}, &block)
|
|
8
|
+
@name = name.to_sym
|
|
9
|
+
@description = description || "Seed for #{name}"
|
|
10
|
+
@parameters = parameters.transform_keys(&:to_sym)
|
|
11
|
+
@metadata = metadata.transform_keys(&:to_sym)
|
|
12
|
+
@block = block
|
|
13
|
+
@created_at = Time.now
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(**kwargs)
|
|
17
|
+
validate_parameters!(kwargs)
|
|
18
|
+
@block.call(**kwargs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parameter_names
|
|
22
|
+
@parameters.keys
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def has_parameters?
|
|
26
|
+
@parameters.any?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parameter_info(name)
|
|
30
|
+
@parameters[name.to_sym]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_parameters!(kwargs)
|
|
34
|
+
# Check for required parameters
|
|
35
|
+
required_params = @parameters.select { |_, info| info[:required] }.keys
|
|
36
|
+
missing_params = required_params - kwargs.keys
|
|
37
|
+
|
|
38
|
+
raise ArgumentError, "Missing required parameters: #{missing_params.join(', ')}" if missing_params.any?
|
|
39
|
+
|
|
40
|
+
# Validate parameter types and values
|
|
41
|
+
kwargs.each do |key, value|
|
|
42
|
+
param_info = @parameters[key]
|
|
43
|
+
next unless param_info
|
|
44
|
+
|
|
45
|
+
validate_parameter_type!(key, value, param_info)
|
|
46
|
+
validate_parameter_value!(key, value, param_info)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h
|
|
51
|
+
{
|
|
52
|
+
name: @name,
|
|
53
|
+
description: @description,
|
|
54
|
+
parameters: @parameters,
|
|
55
|
+
metadata: @metadata,
|
|
56
|
+
created_at: @created_at,
|
|
57
|
+
has_parameters: has_parameters?
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_json(*args)
|
|
62
|
+
to_h.to_json(*args)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def validate_parameter_type!(key, value, param_info)
|
|
68
|
+
expected_type = param_info[:type]
|
|
69
|
+
return unless expected_type
|
|
70
|
+
return unless value.present? && param_info[:required]
|
|
71
|
+
|
|
72
|
+
case expected_type
|
|
73
|
+
when :string
|
|
74
|
+
raise ArgumentError, "Parameter '#{key}' must be a string" unless value.is_a?(String)
|
|
75
|
+
when :integer
|
|
76
|
+
raise ArgumentError, "Parameter '#{key}' must be an integer" unless value.is_a?(Integer)
|
|
77
|
+
when :boolean
|
|
78
|
+
raise ArgumentError, "Parameter '#{key}' must be a boolean" unless [true, false].include?(value)
|
|
79
|
+
when :symbol
|
|
80
|
+
raise ArgumentError, "Parameter '#{key}' must be a symbol" unless value.is_a?(Symbol)
|
|
81
|
+
when :array
|
|
82
|
+
raise ArgumentError, "Parameter '#{key}' must be an array" unless value.is_a?(Array)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def validate_parameter_value!(key, value, param_info)
|
|
87
|
+
# Check allowed values
|
|
88
|
+
if param_info[:allowed_values] && !param_info[:allowed_values].include?(value)
|
|
89
|
+
raise ArgumentError, "Parameter '#{key}' must be one of: #{param_info[:allowed_values].join(', ')}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check min/max for numeric values
|
|
93
|
+
if param_info[:min] && value < param_info[:min]
|
|
94
|
+
raise ArgumentError, "Parameter '#{key}' must be >= #{param_info[:min]}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
return unless param_info[:max] && value > param_info[:max]
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, "Parameter '#{key}' must be <= #{param_info[:max]}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FactorySeeder
|
|
4
|
+
class SeedBuilder
|
|
5
|
+
def initialize(name)
|
|
6
|
+
@name = name
|
|
7
|
+
@description = nil
|
|
8
|
+
@parameters = {}
|
|
9
|
+
@metadata = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def description(text)
|
|
13
|
+
@description = text
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parameter(name, type: :string, required: false, default: nil, allowed_values: nil, min: nil, max: nil,
|
|
18
|
+
description: nil)
|
|
19
|
+
# Validation des paramètres selon le type
|
|
20
|
+
validate_parameter_options(type, allowed_values: allowed_values, min: min, max: max)
|
|
21
|
+
|
|
22
|
+
@parameters[name.to_sym] = {
|
|
23
|
+
type: type,
|
|
24
|
+
required: required,
|
|
25
|
+
default: default,
|
|
26
|
+
allowed_values: allowed_values,
|
|
27
|
+
min: min,
|
|
28
|
+
max: max,
|
|
29
|
+
description: description
|
|
30
|
+
}
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def metadata(key, value)
|
|
35
|
+
@metadata[key.to_sym] = value
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build(&block)
|
|
40
|
+
Seed.new(
|
|
41
|
+
@name,
|
|
42
|
+
description: @description,
|
|
43
|
+
parameters: @parameters,
|
|
44
|
+
metadata: @metadata,
|
|
45
|
+
&block
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def validate_parameter_options(type, allowed_values:, min:, max:)
|
|
52
|
+
case type
|
|
53
|
+
when :integer
|
|
54
|
+
raise ArgumentError, 'allowed_values is not valid for integer type' if allowed_values
|
|
55
|
+
when :string, :symbol
|
|
56
|
+
raise ArgumentError, "min and max are not valid for #{type} type" if min || max
|
|
57
|
+
when :boolean
|
|
58
|
+
if min || max || allowed_values
|
|
59
|
+
raise ArgumentError,
|
|
60
|
+
'min, max, and allowed_values are not valid for boolean type'
|
|
61
|
+
end
|
|
62
|
+
when :array
|
|
63
|
+
raise ArgumentError, 'min, max, and allowed_values are not valid for array type' if min || max || allowed_values
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|