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,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FactorySeeder
4
+ class CustomSeedsController < ApplicationController
5
+ def index
6
+ @seeds = FactorySeeder.list_custom_seeds
7
+ end
8
+
9
+ def show
10
+ @seed = FactorySeeder.find_custom_seed(params[:name])
11
+ if @seed.nil?
12
+ flash[:error] = "Seed '#{params[:name]}' not found"
13
+ redirect_to custom_seeds_path
14
+ return
15
+ end
16
+ @seed_name = @seed.name
17
+ @execution_logs = []
18
+
19
+ # Retrieve logs from temporary storage if available (PRG pattern)
20
+ if params[:log_id].present?
21
+ stored = ExecutionLogStore.retrieve(params[:log_id])
22
+ if stored
23
+ @execution_logs = stored[:logs] || []
24
+ flash.now[stored[:flash_type]] = stored[:flash_message] if stored[:flash_type]
25
+ end
26
+ end
27
+ end
28
+
29
+ def create
30
+ seed_name = params[:name]
31
+ seed = FactorySeeder.find_custom_seed(seed_name)
32
+
33
+ if seed.nil?
34
+ flash[:error] = "Seed '#{seed_name}' not found"
35
+ redirect_to custom_seeds_path
36
+ return
37
+ end
38
+
39
+ attributes = safe_attributes_params
40
+ result = FactorySeeder.run_custom_seed(seed_name, **attributes)
41
+ logs = result[:logs] || []
42
+
43
+ flash_type = result[:success] ? :success : :error
44
+ log_id = ExecutionLogStore.store(logs, flash_type: flash_type, flash_message: result[:message])
45
+
46
+ redirect_to custom_seed_path(seed_name, log_id: log_id)
47
+ end
48
+
49
+ def new
50
+ # For creating new seeds via web interface (future feature)
51
+ @seed = nil
52
+ end
53
+
54
+ def edit
55
+ @seed = FactorySeeder.find_custom_seed(params[:name])
56
+ return unless @seed.nil?
57
+
58
+ flash[:error] = "Seed '#{params[:name]}' not found"
59
+ redirect_to custom_seeds_path
60
+ nil
61
+ end
62
+
63
+ def update
64
+ # For updating existing seeds (future feature)
65
+ seed_name = params[:name]
66
+ # Implementation would go here
67
+ redirect_to custom_seed_path(seed_name)
68
+ end
69
+
70
+ def destroy
71
+ # For deleting seeds (future feature)
72
+ params[:name]
73
+ # Implementation would go here
74
+ redirect_to custom_seeds_path
75
+ end
76
+
77
+ private
78
+
79
+ def safe_attributes_params
80
+ if params.key?(:attributes)
81
+ # Convert string values to appropriate types based on seed parameter definitions
82
+ raw_attributes = params.require(:attributes).permit!
83
+ seed = FactorySeeder.find_custom_seed(params[:name])
84
+
85
+ if seed
86
+ convert_attributes_to_types(raw_attributes, seed)
87
+ else
88
+ raw_attributes.transform_keys(&:to_sym)
89
+ end
90
+ else
91
+ {}
92
+ end
93
+ end
94
+
95
+ def convert_attributes_to_types(raw_attributes, seed)
96
+ converted = {}
97
+
98
+ raw_attributes.each do |key, value|
99
+ param_info = seed.parameter_info(key)
100
+ converted[key.to_sym] = convert_value_to_type(value, param_info)
101
+ end
102
+
103
+ converted
104
+ end
105
+
106
+ def convert_value_to_type(value, param_info)
107
+ return value if value.blank?
108
+
109
+ case param_info&.dig(:type)
110
+ when :integer
111
+ value.to_i
112
+ when :boolean
113
+ case value.to_s.downcase
114
+ when 'true', '1', 'yes', 'on'
115
+ true
116
+ when 'false', '0', 'no', 'off'
117
+ false
118
+ else
119
+ value
120
+ end
121
+ when :symbol
122
+ value.to_sym
123
+ when :array
124
+ if value.is_a?(String)
125
+ value.split(',').map(&:strip)
126
+ else
127
+ value
128
+ end
129
+ else
130
+ value
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FactorySeeder
4
+ class DashboardController < ApplicationController
5
+ def index
6
+ @factories = FactorySeeder.scan_loaded_factories
7
+ @seeds_info = FactorySeeder.list_seeds
8
+ end
9
+
10
+ def run_seed
11
+ params[:name]
12
+
13
+ begin
14
+ flash[:info] = 'Seed system coming soon! For now, use the factory generation interface.'
15
+ redirect_to root_path
16
+ rescue StandardError => e
17
+ flash[:error] = "Error running seed: #{e.message}"
18
+ redirect_to root_path
19
+ end
20
+ end
21
+
22
+ def run_all_seeds
23
+ flash[:info] = 'Seed system coming soon! For now, use the factory generation interface.'
24
+ redirect_to root_path
25
+ rescue StandardError => e
26
+ flash[:error] = "Error running seeds: #{e.message}"
27
+ redirect_to root_path
28
+ end
29
+
30
+ private
31
+
32
+ def get_seeds_info
33
+ FactorySeeder.list_seeds.keys
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FactorySeeder
4
+ class FactoryController < ApplicationController
5
+ def index
6
+ @factories = FactorySeeder.scan_loaded_factories
7
+ end
8
+
9
+ def show
10
+ @factory_name = params[:name]
11
+ @factories = FactorySeeder.scan_loaded_factories
12
+ @factory = @factories[@factory_name]
13
+ @execution_logs = []
14
+
15
+ # Retrieve logs from temporary storage if available (PRG pattern)
16
+ if params[:log_id].present?
17
+ stored = ExecutionLogStore.retrieve(params[:log_id])
18
+ if stored
19
+ @execution_logs = stored[:logs] || []
20
+ flash.now[stored[:flash_type]] = stored[:flash_message] if stored[:flash_type]
21
+ end
22
+ end
23
+
24
+ return if @factory
25
+
26
+ redirect_to root_path, alert: "Factory '#{@factory_name}' not found"
27
+ nil
28
+ end
29
+
30
+ def generate
31
+ factory_name = params[:name]
32
+ count = (params[:count] || 1).to_i
33
+ traits = parse_traits(params[:selected_traits])
34
+
35
+ begin
36
+ generator = SeedGenerator.new
37
+ result = generator.generate(factory_name, count, traits, generate_params[:attributes].to_h.compact_blank)
38
+ logs = result[:logs] || []
39
+
40
+ if result[:errors].any?
41
+ log_id = ExecutionLogStore.store(logs, flash_type: :error,
42
+ flash_message: "Error generating seeds: #{result[:errors].join(', ')}")
43
+ else
44
+ log_id = ExecutionLogStore.store(logs, flash_type: :success,
45
+ flash_message: "Successfully generated #{result[:count]} #{factory_name} records")
46
+ end
47
+ rescue StandardError => e
48
+ log_id = ExecutionLogStore.store([], flash_type: :error, flash_message: "Error generating seeds: #{e.message}")
49
+ end
50
+
51
+ redirect_to factory_path(factory_name, log_id: log_id)
52
+ end
53
+
54
+ private
55
+
56
+ def parse_traits(traits_param)
57
+ return [] if traits_param.blank?
58
+
59
+ if traits_param.is_a?(String)
60
+ traits_param.split(',').map(&:strip).reject(&:blank?)
61
+ else
62
+ traits_param
63
+ end
64
+ end
65
+
66
+ def generate_params
67
+ params.permit(:name, :count, :selected_traits, attributes: {})
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,51 @@
1
+ <div class="page-shell">
2
+ <div class="scanlines"></div>
3
+ <div class="grid-background"></div>
4
+ <div class="page-content">
5
+ <header class="section-title">Custom Seeds (<%= @seeds.count %>)</header>
6
+
7
+ <% if flash[:success] %>
8
+ <div class="alert-message alert-message--success"><%= flash[:success] %></div>
9
+ <% end %>
10
+
11
+ <% if flash[:error] %>
12
+ <div class="alert-message alert-message--error"><%= flash[:error] %></div>
13
+ <% end %>
14
+
15
+ <% if @seeds.empty? %>
16
+ <div class="alert-message alert-message--info">
17
+ No custom seed files detected. Drop your Ruby seed scripts under <code>db/seeds</code>
18
+ or configure them via the FactorySeeder DSL.
19
+ </div>
20
+ <% else %>
21
+ <div class="seed-grid">
22
+ <% @seeds.each do |seed| %>
23
+ <% parameter_count = seed.parameter_names.count %>
24
+ <article class="seed-card">
25
+ <div class="seed-card__header">
26
+ <div>
27
+ <div class="seed-card__title"><%= seed.name.to_s.humanize %></div>
28
+ <div class="factory-card__meta"><%= seed.description %></div>
29
+ </div>
30
+ <span class="seed-card__tag"><%= pluralize(parameter_count, 'parameter') %></span>
31
+ </div>
32
+
33
+ <div class="seed-card__description">
34
+ <% if seed.has_parameters? %>
35
+ <% seed.parameter_names.each do |param_name| %>
36
+ <span class="seed-card__tag"><%= param_name.to_s.humanize %></span>
37
+ <% end %>
38
+ <% else %>
39
+ <span class="seed-card__tag">No parameters</span>
40
+ <% end %>
41
+ </div>
42
+
43
+ <div class="seed-card__actions">
44
+ <%= link_to "Inspect & Run", custom_seed_path(seed.name), class: "btn btn-accent" %>
45
+ </div>
46
+ </article>
47
+ <% end %>
48
+ </div>
49
+ <% end %>
50
+ </div>
51
+ </div>
@@ -0,0 +1,113 @@
1
+ <div class="page-shell">
2
+ <div class="scanlines"></div>
3
+ <div class="grid-background"></div>
4
+ <div class="page-content">
5
+ <header class="section-title">Custom Seed · <%= @seed.name.to_s.humanize %></header>
6
+ <div class="factory-card__label">
7
+ <%= link_to "← Back to seeds", custom_seeds_path, class: "btn btn-accent" %>
8
+ </div>
9
+ <p class="hero__subtitle"><%= @seed.description %></p>
10
+
11
+ <% if flash[:success] %>
12
+ <div class="alert-message alert-message--success"><%= flash[:success] %></div>
13
+ <% end %>
14
+
15
+ <% if flash[:error] %>
16
+ <div class="alert-message alert-message--error"><%= flash[:error] %></div>
17
+ <% end %>
18
+
19
+ <% if flash[:info] %>
20
+ <div class="alert-message alert-message--info"><%= flash[:info] %></div>
21
+ <% end %>
22
+
23
+ <div class="factory-show-grid">
24
+ <section class="panel seed-detail">
25
+ <div class="seed-detail__hero">
26
+ <h2 class="seed-detail__title"><%= @seed.name.to_s.humanize %></h2>
27
+ <p class="seed-detail__subtitle"><%= @seed.description %></p>
28
+ <div class="seed-card__tags">
29
+ <span class="seed-card__tag"><%= pluralize(@seed.parameter_names.count, 'parameter') %></span>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="seed-detail-form panel__body">
34
+ <% if @seed.has_parameters? %>
35
+ <div class="panel__title">Parameters</div>
36
+ <%= form_tag create_custom_seede_path(@seed_name), method: :post do %>
37
+ <% @seed.parameter_names.each do |param_name| %>
38
+ <% param_info = @seed.parameter_info(param_name) %>
39
+ <div class="form-group">
40
+ <label for="<%= param_name %>">
41
+ <%= param_name.to_s.humanize %>
42
+ <% if param_info[:required] %>
43
+ <span class="required">*</span>
44
+ <% end %>
45
+ </label>
46
+ <% if param_info[:description].present? %>
47
+ <small class="parameter-description"><%= param_info[:description] %></small>
48
+ <% end %>
49
+
50
+ <% case param_info[:type] %>
51
+ <% when :boolean %>
52
+ <%= select_tag "attributes[#{param_name}]", options_for_select([['Select...', ''], ['Yes', 'true'], ['No', 'false']], param_info[:default]&.to_s), class: "form-control" %>
53
+ <% when :integer %>
54
+ <%= number_field_tag "attributes[#{param_name}]", param_info[:default], class: "form-control", min: param_info[:min], max: param_info[:max], placeholder: "Enter value" %>
55
+ <% else %>
56
+ <%= text_field_tag "attributes[#{param_name}]", param_info[:default], class: "form-control", placeholder: "Enter #{param_name.to_s.humanize.downcase}" %>
57
+ <% end %>
58
+
59
+ <% if param_info[:allowed_values].present? && param_info[:type] != :symbol %>
60
+ <small class="allowed-values">Allowed: <%= param_info[:allowed_values].join(', ') %></small>
61
+ <% end %>
62
+ </div>
63
+ <% end %>
64
+
65
+ <div class="form-actions">
66
+ <%= submit_tag "Run Seed", class: "btn btn-primary" %>
67
+ </div>
68
+ <% end %>
69
+ <% else %>
70
+ <div class="alert-message alert-message--info">
71
+ This seed does not expose any parameters. It will execute with its default settings.
72
+ </div>
73
+ <div class="form-actions">
74
+ <%= form_tag create_custom_seede_path(@seed_name), method: :post do %>
75
+ <%= submit_tag "Run Seed", class: "btn btn-primary" %>
76
+ <% end %>
77
+ </div>
78
+ <% end %>
79
+ </div>
80
+ </section>
81
+
82
+ <section class="panel console">
83
+ <div class="console__header">
84
+ <span>Execution Console</span>
85
+ <span class="factory-card__meta">Logs stream here when running.</span>
86
+ </div>
87
+ <div class="console__body">
88
+ <% logs = @execution_logs || [] %>
89
+ <% if logs.any? %>
90
+ <div class="space-y-1">
91
+ <% logs.each do |log| %>
92
+ <% entry = log.is_a?(Hash) ? log : log.to_h %>
93
+ <% message = entry['message'] || entry[:message] || entry.to_s %>
94
+ <% level = (entry['level'] || entry[:level] || :info).to_s %>
95
+ <% timestamp_value = entry['timestamp'] || entry[:timestamp] || Time.now %>
96
+ <% timestamp =
97
+ timestamp_value.respond_to?(:strftime) ? timestamp_value.strftime('%H:%M:%S') : timestamp_value.to_s %>
98
+ <div class="console-log console-log--<%= level %>">
99
+ <span class="text-muted-foreground">[<%= timestamp %>]</span>
100
+ <span><%= message %></span>
101
+ </div>
102
+ <% end %>
103
+ </div>
104
+ <% else %>
105
+ <div class="console-log">
106
+ <span>[ --:--:-- ]</span>Waiting for execution...
107
+ </div>
108
+ <% end %>
109
+ </div>
110
+ </section>
111
+ </div>
112
+ </div>
113
+ </div>
@@ -0,0 +1,99 @@
1
+ <div class="page-shell">
2
+ <div class="scanlines"></div>
3
+ <div class="grid-background"></div>
4
+ <div class="page-content">
5
+ <section class="hero">
6
+ <div class="hero__icon">🛠️</div>
7
+ <h1 class="hero__title">Factory Seeder</h1>
8
+ <p class="hero__subtitle">
9
+ Industrial-grade seed generation that keeps your FactoryBot definitions
10
+ and custom scripts in sync with production-ready tooling.
11
+ </p>
12
+ <div class="hero__flair">
13
+ <span>FactoryBot</span>
14
+ <span>|</span>
15
+ <span>Custom Seeds</span>
16
+ <span>|</span>
17
+ <span>Live Logs</span>
18
+ </div>
19
+ </section>
20
+
21
+ <% if flash[:success] %>
22
+ <div class="alert-message alert-message--success">
23
+ <%= flash[:success] %>
24
+ </div>
25
+ <% end %>
26
+
27
+ <% if flash[:error] %>
28
+ <div class="alert-message alert-message--error">
29
+ <%= flash[:error] %>
30
+ </div>
31
+ <% end %>
32
+
33
+ <% if flash[:info] %>
34
+ <div class="alert-message alert-message--info">
35
+ <%= flash[:info] %>
36
+ </div>
37
+ <% end %>
38
+
39
+ <% factories_with_traits = @factories.count { |_, info| info[:traits].present? } %>
40
+ <div class="stat-row">
41
+ <div class="stat-card">
42
+ <div class="stat-card__value"><%= @factories.count %></div>
43
+ <div class="stat-card__label">Loaded Factories</div>
44
+ </div>
45
+ <div class="stat-card">
46
+ <div class="stat-card__value"><%= @seeds_info.count %></div>
47
+ <div class="stat-card__label">Custom Seeds</div>
48
+ </div>
49
+ <div class="stat-card">
50
+ <div class="stat-card__value"><%= factories_with_traits %></div>
51
+ <div class="stat-card__label">Factories w/ Traits</div>
52
+ </div>
53
+ <div class="stat-card">
54
+ <div class="stat-card__value">Ready</div>
55
+ <div class="stat-card__label">System Status</div>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="mode-grid">
60
+ <article class="mode-card">
61
+ <div class="mode-card__header">
62
+ <span class="mode-card__icon">🏭</span>
63
+ <h2 class="mode-card__title">Factories</h2>
64
+ </div>
65
+ <p class="mode-card__description">
66
+ Generate Faker data directly from your FactoryBot definitions. Tweak traits,
67
+ associations, and batch counts before execution.
68
+ </p>
69
+ <div class="mode-card__tags">
70
+ <% @factories.each_key.take(3).each do |name| %>
71
+ <span class="mode-card__tag"><%= name %></span>
72
+ <% end %>
73
+ </div>
74
+ <div class="mode-card__actions">
75
+ <%= link_to "Open factories", factory_index_path, class: "btn btn-primary" %>
76
+ </div>
77
+ </article>
78
+
79
+ <article class="mode-card">
80
+ <div class="mode-card__header">
81
+ <span class="mode-card__icon">💾</span>
82
+ <h2 class="mode-card__title">Custom Seeds</h2>
83
+ </div>
84
+ <p class="mode-card__description">
85
+ Run your bespoke Ruby seed files with the same industrial UI. Logs, progress,
86
+ and retry actions are available from the execution screen.
87
+ </p>
88
+ <div class="mode-card__tags">
89
+ <% @seeds_info.each_key.take(3).each do |seed| %>
90
+ <span class="mode-card__tag"><%= seed.to_s.humanize %></span>
91
+ <% end %>
92
+ </div>
93
+ <div class="mode-card__actions">
94
+ <%= link_to "View seeds", custom_seeds_path, class: "btn btn-accent" %>
95
+ </div>
96
+ </article>
97
+ </div>
98
+ </div>
99
+ </div>
@@ -0,0 +1,71 @@
1
+ <div class="page-shell">
2
+ <div class="scanlines"></div>
3
+ <div class="grid-background"></div>
4
+ <div class="page-content">
5
+ <header class="section-title">Available Factories (<%= @factories.count %>)</header>
6
+
7
+ <% if flash[:success] %>
8
+ <div class="alert-message alert-message--success"><%= flash[:success] %></div>
9
+ <% end %>
10
+
11
+ <% if flash[:error] %>
12
+ <div class="alert-message alert-message--error"><%= flash[:error] %></div>
13
+ <% end %>
14
+
15
+ <% if flash[:info] %>
16
+ <div class="alert-message alert-message--info"><%= flash[:info] %></div>
17
+ <% end %>
18
+
19
+ <% if @factories.empty? %>
20
+ <div class="alert-message alert-message--info">
21
+ No FactoryBot definitions were detected yet. Confirm there are files under
22
+ <code>spec/factories/</code> or <code>test/factories/</code>, then refresh.
23
+ </div>
24
+ <% else %>
25
+ <div class="factories-grid">
26
+ <% @factories.each do |name, info| %>
27
+ <% traits = Array(info[:traits]) %>
28
+ <% associations = Array(info[:associations]) %>
29
+ <article class="factory-card">
30
+ <div class="factory-card__header">
31
+ <div>
32
+ <div class="factory-card__title"><%= name %></div>
33
+ <div class="factory-card__meta"><%= info[:class_name] %></div>
34
+ </div>
35
+ <div class="factory-card__label">Live</div>
36
+ </div>
37
+
38
+ <% if traits.any? %>
39
+ <div class="factory-card__badges">
40
+ <% traits.first(4).each do |trait| %>
41
+ <span class="trait-pill"><%= trait %></span>
42
+ <% end %>
43
+ <% if traits.size > 4 %>
44
+ <span class="trait-pill">+<%= traits.size - 4 %> more</span>
45
+ <% end %>
46
+ </div>
47
+ <% end %>
48
+
49
+ <% if associations.any? %>
50
+ <div class="factory-card__badges">
51
+ <% associations.each do |assoc| %>
52
+ <span class="assoc-pill"><%= assoc[:name] %></span>
53
+ <% end %>
54
+ </div>
55
+ <% end %>
56
+
57
+ <div class="factory-card__description">
58
+ Rendered class: <%= info[:class_name] %><br>
59
+ Traits: <%= traits.present? ? traits.join(', ') : 'none' %><br>
60
+ Associations: <%= associations.present? ? associations.count : 0 %>
61
+ </div>
62
+
63
+ <div class="factory-card__footer">
64
+ <%= link_to "Generate Seeds", factory_path(name), class: "btn btn-primary w-full" %>
65
+ </div>
66
+ </article>
67
+ <% end %>
68
+ </div>
69
+ <% end %>
70
+ </div>
71
+ </div>