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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +111 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +445 -0
  5. data/app/assets/stylesheets/factory_seeder.css +637 -0
  6. data/app/controllers/factory_seeder/application_controller.rb +8 -0
  7. data/app/controllers/factory_seeder/custom_seeds_controller.rb +134 -0
  8. data/app/controllers/factory_seeder/dashboard_controller.rb +36 -0
  9. data/app/controllers/factory_seeder/factory_controller.rb +70 -0
  10. data/app/views/factory_seeder/custom_seeds/index.html.erb +51 -0
  11. data/app/views/factory_seeder/custom_seeds/show.html.erb +113 -0
  12. data/app/views/factory_seeder/dashboard/index.html.erb +99 -0
  13. data/app/views/factory_seeder/factory/index.html.erb +71 -0
  14. data/app/views/factory_seeder/factory/show.html.erb +108 -0
  15. data/app/views/factory_seeder/seeds/show.html.erb +2 -0
  16. data/app/views/layouts/factory_seeder/application.html.erb +25 -0
  17. data/bin/factory_seeder +27 -0
  18. data/config/factory_seeder.rb +24 -0
  19. data/config/routes.rb +20 -0
  20. data/lib/factory_seeder/asset_helper.rb +34 -0
  21. data/lib/factory_seeder/cli.rb +352 -0
  22. data/lib/factory_seeder/configuration.rb +32 -0
  23. data/lib/factory_seeder/custom_seed_loader.rb +39 -0
  24. data/lib/factory_seeder/engine.rb +16 -0
  25. data/lib/factory_seeder/execution_log_store.rb +48 -0
  26. data/lib/factory_seeder/factory_scanner.rb +149 -0
  27. data/lib/factory_seeder/loader.rb +26 -0
  28. data/lib/factory_seeder/rails_integration.rb +29 -0
  29. data/lib/factory_seeder/seed.rb +102 -0
  30. data/lib/factory_seeder/seed_builder.rb +67 -0
  31. data/lib/factory_seeder/seed_generator.rb +305 -0
  32. data/lib/factory_seeder/seed_manager.rb +128 -0
  33. data/lib/factory_seeder/seeder.rb +41 -0
  34. data/lib/factory_seeder/version.rb +5 -0
  35. data/lib/factory_seeder/web_interface.rb +119 -0
  36. data/lib/factory_seeder.rb +209 -0
  37. data/templates/seed_template.rb +84 -0
  38. 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