fixtures_from_factories 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 71e768e6b219f669c71efecbe203f358e3ab3db484045d49e297c3b6b3845f47
4
+ data.tar.gz: 3a18e1072c46a3d585b954e2b259ebf2ca1fbc9710f7c9e3a7ad39cdb59d912c
5
+ SHA512:
6
+ metadata.gz: b607d62ff84ecf91222176ec430289057dc9075d4c5a102b69aa4a404687c4b1b768a32546dcb43d142c8667bff61bea86503bdd8cf9409581ac1ba825252299
7
+ data.tar.gz: 2b5fa3d3e23f2f3b1f7f67807ba52904476b66fb4f1ebcfefa50b55abf68238e43c204ed27b7b3009efe663b11dfa6934c27c5c11563c24236640f23cb6cb776
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 2.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-11-17
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in fixtures_from_factories.gemspec
6
+ gemspec
7
+
8
+ gem "sqlite3"
9
+
10
+ gem "puma"
11
+
12
+ gem "rake", "~> 13.0"
13
+
14
+ gem "minitest", "~> 5.0"
15
+
16
+ gem "standard", "~> 1.3"
data/Gemfile.lock ADDED
@@ -0,0 +1,208 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fixtures_from_factories (0.1.0)
5
+ devise
6
+ factory_bot
7
+ faker
8
+ rails (>= 6)
9
+ timecop
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ actioncable (7.0.4)
15
+ actionpack (= 7.0.4)
16
+ activesupport (= 7.0.4)
17
+ nio4r (~> 2.0)
18
+ websocket-driver (>= 0.6.1)
19
+ actionmailbox (7.0.4)
20
+ actionpack (= 7.0.4)
21
+ activejob (= 7.0.4)
22
+ activerecord (= 7.0.4)
23
+ activestorage (= 7.0.4)
24
+ activesupport (= 7.0.4)
25
+ mail (>= 2.7.1)
26
+ net-imap
27
+ net-pop
28
+ net-smtp
29
+ actionmailer (7.0.4)
30
+ actionpack (= 7.0.4)
31
+ actionview (= 7.0.4)
32
+ activejob (= 7.0.4)
33
+ activesupport (= 7.0.4)
34
+ mail (~> 2.5, >= 2.5.4)
35
+ net-imap
36
+ net-pop
37
+ net-smtp
38
+ rails-dom-testing (~> 2.0)
39
+ actionpack (7.0.4)
40
+ actionview (= 7.0.4)
41
+ activesupport (= 7.0.4)
42
+ rack (~> 2.0, >= 2.2.0)
43
+ rack-test (>= 0.6.3)
44
+ rails-dom-testing (~> 2.0)
45
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
46
+ actiontext (7.0.4)
47
+ actionpack (= 7.0.4)
48
+ activerecord (= 7.0.4)
49
+ activestorage (= 7.0.4)
50
+ activesupport (= 7.0.4)
51
+ globalid (>= 0.6.0)
52
+ nokogiri (>= 1.8.5)
53
+ actionview (7.0.4)
54
+ activesupport (= 7.0.4)
55
+ builder (~> 3.1)
56
+ erubi (~> 1.4)
57
+ rails-dom-testing (~> 2.0)
58
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
59
+ activejob (7.0.4)
60
+ activesupport (= 7.0.4)
61
+ globalid (>= 0.3.6)
62
+ activemodel (7.0.4)
63
+ activesupport (= 7.0.4)
64
+ activerecord (7.0.4)
65
+ activemodel (= 7.0.4)
66
+ activesupport (= 7.0.4)
67
+ activestorage (7.0.4)
68
+ actionpack (= 7.0.4)
69
+ activejob (= 7.0.4)
70
+ activerecord (= 7.0.4)
71
+ activesupport (= 7.0.4)
72
+ marcel (~> 1.0)
73
+ mini_mime (>= 1.1.0)
74
+ activesupport (7.0.4)
75
+ concurrent-ruby (~> 1.0, >= 1.0.2)
76
+ i18n (>= 1.6, < 2)
77
+ minitest (>= 5.1)
78
+ tzinfo (~> 2.0)
79
+ ast (2.4.2)
80
+ bcrypt (3.1.18)
81
+ builder (3.2.4)
82
+ concurrent-ruby (1.1.10)
83
+ crass (1.0.6)
84
+ devise (4.8.1)
85
+ bcrypt (~> 3.0)
86
+ orm_adapter (~> 0.1)
87
+ railties (>= 4.1.0)
88
+ responders
89
+ warden (~> 1.2.3)
90
+ erubi (1.11.0)
91
+ factory_bot (6.2.1)
92
+ activesupport (>= 5.0.0)
93
+ faker (2.21.0)
94
+ i18n (>= 1.8.11, < 2)
95
+ globalid (1.0.0)
96
+ activesupport (>= 5.0)
97
+ i18n (1.12.0)
98
+ concurrent-ruby (~> 1.0)
99
+ json (2.6.2)
100
+ loofah (2.19.0)
101
+ crass (~> 1.0.2)
102
+ nokogiri (>= 1.5.9)
103
+ mail (2.7.1)
104
+ mini_mime (>= 0.1.1)
105
+ marcel (1.0.2)
106
+ method_source (1.0.0)
107
+ mini_mime (1.1.2)
108
+ minitest (5.16.3)
109
+ net-imap (0.3.1)
110
+ net-protocol
111
+ net-pop (0.1.2)
112
+ net-protocol
113
+ net-protocol (0.1.3)
114
+ timeout
115
+ net-smtp (0.3.3)
116
+ net-protocol
117
+ nio4r (2.5.8)
118
+ nokogiri (1.13.9-x86_64-darwin)
119
+ racc (~> 1.4)
120
+ orm_adapter (0.5.0)
121
+ parallel (1.22.1)
122
+ parser (3.1.2.1)
123
+ ast (~> 2.4.1)
124
+ puma (6.0.0)
125
+ nio4r (~> 2.0)
126
+ racc (1.6.0)
127
+ rack (2.2.4)
128
+ rack-test (2.0.2)
129
+ rack (>= 1.3)
130
+ rails (7.0.4)
131
+ actioncable (= 7.0.4)
132
+ actionmailbox (= 7.0.4)
133
+ actionmailer (= 7.0.4)
134
+ actionpack (= 7.0.4)
135
+ actiontext (= 7.0.4)
136
+ actionview (= 7.0.4)
137
+ activejob (= 7.0.4)
138
+ activemodel (= 7.0.4)
139
+ activerecord (= 7.0.4)
140
+ activestorage (= 7.0.4)
141
+ activesupport (= 7.0.4)
142
+ bundler (>= 1.15.0)
143
+ railties (= 7.0.4)
144
+ rails-dom-testing (2.0.3)
145
+ activesupport (>= 4.2.0)
146
+ nokogiri (>= 1.6)
147
+ rails-html-sanitizer (1.4.3)
148
+ loofah (~> 2.3)
149
+ railties (7.0.4)
150
+ actionpack (= 7.0.4)
151
+ activesupport (= 7.0.4)
152
+ method_source
153
+ rake (>= 12.2)
154
+ thor (~> 1.0)
155
+ zeitwerk (~> 2.5)
156
+ rainbow (3.1.1)
157
+ rake (13.0.6)
158
+ regexp_parser (2.6.0)
159
+ responders (3.0.1)
160
+ actionpack (>= 5.0)
161
+ railties (>= 5.0)
162
+ rexml (3.2.5)
163
+ rubocop (1.39.0)
164
+ json (~> 2.3)
165
+ parallel (~> 1.10)
166
+ parser (>= 3.1.2.1)
167
+ rainbow (>= 2.2.2, < 4.0)
168
+ regexp_parser (>= 1.8, < 3.0)
169
+ rexml (>= 3.2.5, < 4.0)
170
+ rubocop-ast (>= 1.23.0, < 2.0)
171
+ ruby-progressbar (~> 1.7)
172
+ unicode-display_width (>= 1.4.0, < 3.0)
173
+ rubocop-ast (1.23.0)
174
+ parser (>= 3.1.1.0)
175
+ rubocop-performance (1.15.0)
176
+ rubocop (>= 1.7.0, < 2.0)
177
+ rubocop-ast (>= 0.4.0)
178
+ ruby-progressbar (1.11.0)
179
+ sqlite3 (1.5.4-x86_64-darwin)
180
+ standard (1.18.0)
181
+ rubocop (= 1.39.0)
182
+ rubocop-performance (= 1.15.0)
183
+ thor (1.2.1)
184
+ timecop (0.9.5)
185
+ timeout (0.3.0)
186
+ tzinfo (2.0.5)
187
+ concurrent-ruby (~> 1.0)
188
+ unicode-display_width (2.3.0)
189
+ warden (1.2.9)
190
+ rack (>= 2.0.9)
191
+ websocket-driver (0.7.5)
192
+ websocket-extensions (>= 0.1.0)
193
+ websocket-extensions (0.1.5)
194
+ zeitwerk (2.6.6)
195
+
196
+ PLATFORMS
197
+ x86_64-darwin-21
198
+
199
+ DEPENDENCIES
200
+ fixtures_from_factories!
201
+ minitest (~> 5.0)
202
+ puma
203
+ rake (~> 13.0)
204
+ sqlite3
205
+ standard (~> 1.3)
206
+
207
+ BUNDLED WITH
208
+ 2.3.19
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Stephen Ierodiaconou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # FixturesFromFactories
2
+
3
+ A tool to help build a set of Fixtures for your Rails app, using your test suite's FactoryBot factories.
4
+
5
+ ## Why?
6
+
7
+ So you can build a full set of "fake data" for your development environment or for your QA/demo or test environments.
8
+
9
+ Instead of manually maintaining a set of fixtures you can write a script which uses you existing Factories to
10
+ build out the data set. Ie we generate fixture files from a script which uses FactoryBot factories to define the setup.
11
+
12
+ `FixturesFromFactories` sets up a clean DB, runs your setup script, and then dumps records to fixture YAML files!
13
+
14
+ Your Fixtures can then be very quickly loaded into the database to setup a new dev env with data to work with, or
15
+ reset a demo environment between demos to prospective clients.
16
+
17
+ Features:
18
+ - TODO
19
+
20
+ ## Prior art (`fixture-builder` gem)
21
+
22
+ The logic to dump the entities to YAML is based partly on [fixture-builder](https://github.com/rdy/fixture_builder).
23
+
24
+ Big thanks to the many contributors to that project.
25
+
26
+
27
+ ## Installation
28
+
29
+ Install the gem and add to the application's Gemfile by executing:
30
+
31
+ $ bundle add fixtures_from_factories
32
+
33
+ If bundler is not being used to manage dependencies, install the gem by executing:
34
+
35
+ $ gem install fixtures_from_factories
36
+
37
+ ## Usage
38
+
39
+ Write a script (rake task or otherwise) which calls `GenerateSet` and gives it the name of a class which exposes
40
+ a "generate" method. This class should be a subclass of `FixturesFromFactories::BaseBuilder` and should
41
+
42
+ ```ruby
43
+ FixturesFromFactories::GenerateSet.call(
44
+ DemoFixturesBuilder, # The custom class which will setup the data
45
+ fixtures_path, # path to the directory where the fixtures will be written, eg Rails.root.join("demos", "fixtures")
46
+ time_cop_now: [Time.zone.now], # The time to freeze "now" to
47
+ faker_seed: 42, # A seed value for faker to ensure consistent data between runs
48
+ options: { # Any options to pass to the data builder class, available as `options` in the builder class
49
+ # ...
50
+ }
51
+ )
52
+ ```
53
+
54
+ The `DemoFixturesBuilder` class should look something like this:
55
+
56
+ ```ruby
57
+ class DemoFixturesBuilder < FixturesFromFactories::BaseBuilder
58
+ def generate
59
+ # Add data that was seeded (already in newly created database) to fixtures
60
+ # Here the Category records will be added with names "category_<id>"
61
+ generator.add_collection(Category.all)
62
+ # Here the tag records will be added with names "tag_<name attribute parameterized and underscored>"
63
+ # eg a Tag(name: "Foo Bar") will be added as "tag_foo_bar"
64
+ generator.add_collection(Tag.all) { |t| t.name.parameterize.underscore }
65
+
66
+ # Setup for tables with no primary key ID (eg joins tables), note must have an AR model class
67
+ generator.configure_name(CategoriesPosts, :category_id, :post_id)
68
+
69
+ # Create an author
70
+ author = generator.create(
71
+ :first_author, # name of the fixture
72
+ :user, # factory name
73
+ :with_comments, # optional traits
74
+ first_name: "John", # optional attributes
75
+ last_name: "Doe"
76
+ )
77
+
78
+ # Create 6 blog posts for the author - will be named "first_author_post_1", "first_author_post_2", etc
79
+ generator.create_multiple(:first_author_post, :post, 1, 6) do |post_index|
80
+ # from the block return the attributes for the factory
81
+ {
82
+ author: author, # or generator.get(:first_author)
83
+ text: Faker::Lorem.paragraphs(number: 3).join("\n\n"),
84
+ published_at: generator.make_fake_time(post_index.days.ago, (post_index - 1).days.ago)
85
+ }
86
+ end
87
+
88
+ generator.create(
89
+ :category_to_post,
90
+ :categories_post,
91
+ category: generator.get(:category_12), # You can get a previously created record using its name
92
+ post: generator.get(:first_author_post_1)
93
+ )
94
+
95
+ # ... etc
96
+ end
97
+ end
98
+ ```
99
+
100
+
101
+ To load the generated fixtures to your DB
102
+
103
+ rake db:fixtures:load FIXTURES_PATH=demos/fixtures
104
+
105
+ *NOTE:* if you are adding something where the records have no primary 'id' key (eg on HABTM joins table) you must
106
+ specify the columns to use when comparing records in the generator. Ie you must specify a set of attributes
107
+ which uniquely identify each row (for example in a join table the pair of IDs in the row). This is done in
108
+ `TestFixturesGenerators::Index` at the start of the model generation process.
109
+
110
+
111
+
112
+ ## Development
113
+
114
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
115
+
116
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
117
+
118
+ ## Contributing
119
+
120
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/fixtures_from_factories.
121
+
122
+ ## License
123
+
124
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "standard/rake"
13
+
14
+ task default: %i[test standard]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FixturesFromFactories
4
+ class BaseBuilder
5
+ def initialize(generator, rand_seed, options)
6
+ @generator = generator
7
+ @rand_seed = rand_seed
8
+ @options = options
9
+ end
10
+
11
+ attr_reader :generator, :options
12
+
13
+ def generate
14
+ raise NotImplementedError
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "factory_bot"
4
+ require "faker"
5
+
6
+ module FixturesFromFactories
7
+ class FixtureGenerator
8
+ attr_reader :model_cache, :name_config, :excluded_tables, :output_path
9
+
10
+ def initialize(
11
+ output_path,
12
+ excluded_tables = FixturesFromFactories.configuration.excluded_tables
13
+ )
14
+ @model_cache = {}
15
+ @name_config = {}
16
+ @output_path = output_path
17
+ @excluded_tables = excluded_tables
18
+ end
19
+
20
+ def generate(&blk)
21
+ instance_eval(&blk)
22
+ dump_tables
23
+ dump_record_index
24
+ end
25
+
26
+ # Record creation DSL
27
+
28
+ # Create a single named record using FactoryBot
29
+ def create(name_or_prefix, factory_or_index, *bot_args)
30
+ item_name, factory =
31
+ if factory_or_index.is_a?(Symbol) || factory_or_index.is_a?(Array)
32
+ [name_or_prefix, factory_or_index]
33
+ else
34
+ ["#{name_or_prefix}_#{factory_or_index}".to_sym, bot_args.shift]
35
+ end
36
+
37
+ Rails.logger.info "Creating record: #{item_name}"
38
+ record = FactoryBot.create(*(Array.wrap(factory) + bot_args))
39
+ if record.nil? || !record.persisted?
40
+ raise StandardError, "A record failed to save, #{record.class.name}, #{bot_args}"
41
+ end
42
+ add_record(item_name, record)
43
+ record
44
+ end
45
+
46
+ # Create multiple records from factory within index range, where the name has the index appended
47
+ def create_multiple(name, factory, start_index, end_index, items = nil)
48
+ (start_index..end_index).map.with_index do |instance_num, index|
49
+ bot_args = block_given? ? yield(instance_num) : items&.at(index)
50
+ create(name, instance_num, factory, bot_args)
51
+ end
52
+ end
53
+
54
+ def create_multiple_with_names(name_prefix, factory, names, items = nil)
55
+ names.map.with_index do |config, index|
56
+ parts = config.is_a?(Hash) ? config[:names] : config
57
+ n_index = Array.wrap(parts).map { |s| s.to_s.parameterize.underscore }.join("_").to_sym
58
+ n = "#{name_prefix}_#{n_index}".to_sym
59
+ bot_args = block_given? ? yield(n, n_index, config, index) : items&.at(index)
60
+ create(n, factory, bot_args)
61
+ end
62
+ end
63
+
64
+ # Get an existing record
65
+ def get(name, index = nil)
66
+ model_cache.fetch(index.present? ? "#{name}_#{index}".to_sym : name)[:record]
67
+ end
68
+
69
+ def generate_name_from_humanized(model, *parts)
70
+ "#{model}_#{parts.map { |s| s.to_s.parameterize.underscore }.join("_")}".to_sym
71
+ end
72
+
73
+ # Setup naming logic for a specific table
74
+ def configure_name(model_klass, *attrs)
75
+ name_config[model_klass.name] = {klass: model_klass, attrs: attrs}
76
+ name_config[model_klass.name] = ->(record, other) do
77
+ attrs.all? { |attr| record.attributes[attr.to_s] == other[attr.to_s] }
78
+ end
79
+ end
80
+
81
+ def add_record(item_name, record)
82
+ raise "Adding nil record! #{item_name}" if record.nil?
83
+ Rails.logger.info "Adding record #{item_name}..."
84
+ model_cache.merge!({item_name => cached_record(record)}) do |key|
85
+ raise "Duplicate key: #{key}"
86
+ end
87
+ end
88
+
89
+ def add_collection(collection, collection_name: nil)
90
+ c_name = collection_name || collection.name.underscore
91
+ added_count = 0
92
+ collection.find_in_batches do |group|
93
+ group.each do |model|
94
+ id = block_given? ? yield(model) : model.id.to_s
95
+ name = "#{c_name}_#{id}".to_sym
96
+ add_record(name, model)
97
+ added_count += 1
98
+ end
99
+ end
100
+ added_count
101
+ end
102
+
103
+ def make_fake_time(from, to)
104
+ Faker::Time.between(from: from.utc, to: to.utc).utc
105
+ end
106
+
107
+ private
108
+
109
+ def dump_record_index
110
+ path = File.join(output_path, "record_index.txt")
111
+ File.open(File.expand_path(path), "w") do |file|
112
+ file.write "# ----------------------------------------------------------------------------\n"
113
+ file.write "# Do not edit this file manually! It is generated by ./db/generate_fixtures.rb\n"
114
+ file.write "# ----------------------------------------------------------------------------\n"
115
+ file.write model_cache.keys.join("\n")
116
+ end
117
+ end
118
+
119
+ def cached_record(record)
120
+ {
121
+ record: record,
122
+ klass: record.class,
123
+ comparison: ->(record, other) do
124
+ if name_config[record.class.name]
125
+ name_config[record.class.name].call(record, other)
126
+ elsif record.id.present?
127
+ record.id.to_s == other["id"]&.to_s
128
+ else
129
+ raise StandardError,
130
+ "You maybe missing a call to `configure_name` for a table without a primary id key"
131
+ end
132
+ end
133
+ }
134
+ end
135
+
136
+ def class_from_table(table_name)
137
+ table_name.classify.constantize
138
+ rescue
139
+ nil
140
+ end
141
+
142
+ def get_record_name(needle, table_name)
143
+ generate_record_name(needle, table_name, nil)
144
+ end
145
+
146
+ # Based on https://github.com/rdy/fixture_builder
147
+ def generate_record_name(needle, table_name, row_index)
148
+ table_klass = class_from_table(table_name)
149
+ generated_name =
150
+ if table_klass
151
+ generate_record_name_with_model(needle, table_klass, row_index)
152
+ elsif row_index
153
+ [table_name, row_index.succ!].join("_")
154
+ end
155
+
156
+ generated_name&.to_s
157
+ end
158
+
159
+ def generate_record_name_with_model(needle, table_klass, row_index)
160
+ found_name = get_record_name_from_cache(needle, table_klass)
161
+ if found_name.present?
162
+ found_name
163
+ elsif row_index
164
+ [table_klass.name, row_index.succ!].join("_")
165
+ end
166
+ end
167
+
168
+ def get_record_name_from_cache(needle, table_klass)
169
+ # Check if the model is in the cache and get its name, otherwise generate a name from a sequence
170
+ # Note if row_index is not set and the model isnt in the cache (ie it was not explicitly created by us) then
171
+ # nil is returned
172
+ klasses = [table_klass]
173
+ klasses.concat(table_klass.subclasses) if table_klass.subclasses.present?
174
+ found_name =
175
+ model_cache.find do |_key, cached|
176
+ klasses.include?(cached[:klass]) && cached[:comparison].call(cached[:record], needle)
177
+ end
178
+ found_name&.first
179
+ end
180
+
181
+ # Taken from https://github.com/rdy/fixture_builder
182
+ def tables
183
+ ActiveRecord::Base.connection.tables - excluded_tables
184
+ end
185
+
186
+ # Based on https://github.com/rdy/fixture_builder but we replace references with a named reference and let Rails
187
+ # generate stable IDs. In cases where a record has been created indirectly by a factory (and not explicity by this
188
+ # generator) it will stay as a reference with a hardcoded ID
189
+ def dump_tables
190
+ default_date_format = Date::DATE_FORMATS[:default]
191
+ Date::DATE_FORMATS[:default] = Date::DATE_FORMATS[:db]
192
+ begin
193
+ fixtures =
194
+ tables.inject([]) do |files, table_name|
195
+ rows = process_table(table_name)
196
+ next files if rows.empty?
197
+
198
+ fixture_data = prepare_fixture_data(rows, table_name)
199
+ write_fixture_file(fixture_data, table_name)
200
+ files + [File.basename(fixture_file(table_name))]
201
+ end
202
+ ensure
203
+ Date::DATE_FORMATS[:default] = default_date_format
204
+ end
205
+ Rails.logger.info "Built #{fixtures.to_sentence}"
206
+ end
207
+
208
+ def process_table(table_name)
209
+ table_klass = class_from_table(table_name)
210
+ if table_klass && table_klass < ActiveRecord::Base
211
+ process_table_with_model(table_klass)
212
+ else
213
+ process_table_without_model(table_name)
214
+ end
215
+ end
216
+
217
+ def process_table_with_model(table_klass)
218
+ table_klass.unscoped do
219
+ table_klass.all.map do |obj|
220
+ attrs = obj.attributes.select { |attr_name| table_klass.column_names.include?(attr_name) }
221
+ attrs.each_with_object({}) do |(attr_name, value), hash|
222
+ replace_association_with_named(table_klass, attrs, hash, attr_name, value)
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ # Some records are
229
+ def replace_association_with_named(table_klass, attrs, hash, attr_name, value)
230
+ # If the record attribute is a relation, then get the other model name and use it instead of a ID value
231
+ # but only if there is a AR model for the table
232
+ attr_name_without_id = attr_name.sub(/_id$/, "")
233
+ reflection = table_klass.reflect_on_association(attr_name_without_id)
234
+ if reflection&.kind_of?(ActiveRecord::Reflection::AssociationReflection) && value.present?
235
+ related_klass_name =
236
+ begin
237
+ # Polymorphic
238
+ association_type = attrs[attr_name_without_id + "_type"]
239
+ association_type.presence || reflection.class_name
240
+ end
241
+ other_name = get_record_name({"id" => value}, related_klass_name.constantize.table_name)
242
+ if other_name
243
+ hash[attr_name_without_id] = other_name.to_s
244
+ return hash
245
+ end
246
+ end
247
+ hash[attr_name] = serialized_value_if_needed(table_klass, attr_name, value)
248
+ end
249
+
250
+ # Some tables have no model (eg joins) and should be dumped.
251
+ def process_table_without_model(table_name)
252
+ ActiveRecord::Base
253
+ .connection
254
+ .select_all(
255
+ "SELECT * FROM %<table>s" % {
256
+ table: ActiveRecord::Base.connection.quote_table_name(table_name)
257
+ }
258
+ )
259
+ .map do |row|
260
+ row.each_with_object({}) do |(attr_name, value), hash|
261
+ # If the record attribute is a relation, then get the other model name and use it instead of a ID value
262
+ if attr_name.end_with?("_id") && value.present?
263
+ attr_name_without_id = attr_name.sub(/_id$/, "")
264
+ klass = class_from_table(attr_name_without_id)
265
+ if klass
266
+ other_name = get_record_name({"id" => value}, klass.table_name)
267
+
268
+ # Note when there is no model for the table then retain '_id' attr name, as the fixtures setup code
269
+ # doesnt know about a relation named with the value of <attr_name_without_id> since there is no Model class
270
+ hash[attr_name] = other_name ? other_name.to_s : value
271
+ else
272
+ hash[attr_name] = value
273
+ end
274
+ else
275
+ hash[attr_name] = value
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ def prepare_fixture_data(rows, table_name)
282
+ # This will be mutated by calls to generate_record_name in cases where the index is needed
283
+ row_index = +"000"
284
+ rows.inject({}) do |hash, record|
285
+ # If this was created by us, dont store the ID, use the rails stable ID
286
+ obj_name = get_record_name(record, table_name)
287
+ record_name = generate_record_name(record, table_name, row_index)
288
+ if obj_name
289
+ Rails.logger.debug "Writing #{obj_name} (#{table_name})"
290
+ record.delete("id")
291
+ else
292
+ Rails
293
+ .logger.debug "Writing a record which was created indirectly by a factory (#{table_name})"
294
+ end
295
+ hash.merge(record_name => record)
296
+ end
297
+ end
298
+
299
+ # Taken from https://github.com/rdy/fixture_builder
300
+ def serialized_value_if_needed(table_klass, attr_name, value)
301
+ if table_klass.respond_to?(:type_for_attribute)
302
+ serialize_according_to_type(table_klass, attr_name, value)
303
+ elsif table_klass.serialized_attributes.has_key? attr_name
304
+ table_klass.serialized_attributes[attr_name].dump(value)
305
+ else
306
+ value
307
+ end
308
+ end
309
+
310
+ def serialize_according_to_type(klass, attr_name, value)
311
+ attr_type = klass.type_for_attribute(attr_name)
312
+ if attr_type.type == :jsonb || attr_type.type == :json
313
+ value
314
+ elsif attr_type.type == :time
315
+ # Serialise as an ActiveSupport::TimeWithZone
316
+ value
317
+ elsif attr_type.respond_to?(:serialize)
318
+ attr_type.serialize(value)
319
+ elsif attr_type.respond_to?(:type_cast_for_database)
320
+ attr_type.type_cast_for_database(value)
321
+ else
322
+ attr_type.type_cast_for_schema(value)
323
+ end
324
+ end
325
+
326
+ def write_fixture_file(fixture_data, table_name)
327
+ File.open(fixture_file(table_name), "w") do |file|
328
+ file.write "# ----------------------------------------------------------------------------\n"
329
+ file.write "# Do not edit this file manually! It is generated by ./db/generate_fixtures.rb\n"
330
+ file.write "# ----------------------------------------------------------------------------\n"
331
+ file.write fixture_data.to_yaml
332
+ end
333
+ end
334
+
335
+ def fixture_file(table_name)
336
+ File.expand_path(File.join(output_path, "#{table_name}.yml"))
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./fixture_generator"
4
+ require "timecop"
5
+
6
+ module FixturesFromFactories
7
+ class GenerateSet
8
+ class << self
9
+ def call(generator_index_klass, output_path, time_cop_now:, faker_seed:, options: {})
10
+ raise "Do not run fixture generation in a production environment" if ::Rails.env.production?
11
+
12
+ # Seed with known value to always end up with same result
13
+ ::Faker::Config.random = Random.new(faker_seed)
14
+ ::Faker::Config.locale = "en"
15
+
16
+ # Freeze the time to avoid having changing timestamps when regenerating
17
+ ::Timecop.freeze(*time_cop_now)
18
+
19
+ # TODO: what is a better solution here?
20
+ # Monkey patch Devise encryptor so that we generate stable password hashes
21
+ if defined?(Devise)
22
+ ::Devise::Encryptor.define_singleton_method(:digest) do |_klass, _password|
23
+ FixturesFromFactories.configuration.devise_password_hash
24
+ end
25
+ end
26
+
27
+ database_env_setup = "RAILS_ENV=#{ENV["RAILS_ENV"]}"
28
+ puts "Setup database for fixtures #{database_env_setup}"
29
+ system("#{database_env_setup} bin/rails db:environment:set RAILS_ENV=development")
30
+ # TODO: should we just use db:reset here?
31
+ system("#{database_env_setup} rake db:drop")
32
+ system("#{database_env_setup} rake db:create")
33
+ system("#{database_env_setup} rake db:migrate")
34
+ system("#{database_env_setup} rake db:seed")
35
+
36
+ if FixturesFromFactories.configuration.factory_bot_definition_file_paths
37
+ FactoryBot.definition_file_paths = FixturesFromFactories.configuration.factory_bot_definition_file_paths
38
+ end
39
+ FactoryBot.find_definitions
40
+
41
+ # Prepare the fixture files
42
+ FixtureGenerator.new(output_path).generate do
43
+ generator_index_klass.new(self, faker_seed, options).generate
44
+ end
45
+
46
+ # After fixtures are build the Rails env set in the DB is lost, so set it manually
47
+ system("#{database_env_setup} bin/rails db:environment:set RAILS_ENV=development")
48
+
49
+ ::Timecop.return
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ module FixturesFromFactories
2
+ class Railtie < ::Rails::Railtie
3
+ # rake_tasks do
4
+ # load "tasks/fixtures_from_factories.rake"
5
+ # end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FixturesFromFactories
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fixtures_from_factories/version"
4
+ require_relative "fixtures_from_factories/base_builder"
5
+ require_relative "fixtures_from_factories/fixture_generator"
6
+ require_relative "fixtures_from_factories/generate_set"
7
+ require_relative "fixtures_from_factories/railtie"
8
+
9
+ module FixturesFromFactories
10
+ class << self
11
+ def configuration
12
+ @configuration ||= Configuration.new
13
+ end
14
+
15
+ def configure
16
+ yield(configuration) if block_given?
17
+ configuration
18
+ end
19
+ end
20
+
21
+ # Configuration class for initializer
22
+ class Configuration
23
+ # @dynamic devise_password_hash
24
+ attr_accessor :devise_password_hash, :excluded_tables, :factory_bot_definition_file_paths
25
+
26
+ def initialize
27
+ @devise_password_hash = "$2a$11$M8e7PEPxv4JEx2JHLw4XnuvfVVafJjFb6DfMnODxSUy5WhIgfbF1y" # 'password' TODO: make this not use the hashed value but let it be hashed at creation time
28
+ @excluded_tables = %w[schema_migrations spatial_ref_sys ar_internal_metadata]
29
+ @factory_bot_definition_file_paths = nil
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ module FixturesFromFactories
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fixtures_from_factories
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen Ierodiaconou
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-11-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: factory_bot
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faker
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: devise
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: timecop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: A tool to help build a set of Fixtures for your Rails app, using your
84
+ test suite's FactoryBot factories. .
85
+ email:
86
+ - stevegeek@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".standard.yml"
92
+ - CHANGELOG.md
93
+ - Gemfile
94
+ - Gemfile.lock
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - lib/fixtures_from_factories.rb
99
+ - lib/fixtures_from_factories/base_builder.rb
100
+ - lib/fixtures_from_factories/fixture_generator.rb
101
+ - lib/fixtures_from_factories/generate_set.rb
102
+ - lib/fixtures_from_factories/railtie.rb
103
+ - lib/fixtures_from_factories/version.rb
104
+ - sig/fixtures_from_factories.rbs
105
+ homepage: https://github.com/stevegeek/fixtures_from_factories
106
+ licenses:
107
+ - MIT
108
+ metadata:
109
+ homepage_uri: https://github.com/stevegeek/fixtures_from_factories
110
+ source_code_uri: https://github.com/stevegeek/fixtures_from_factories
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 2.7.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.3.7
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Build a set of Fixtures for your Rails app, using your test suite's FactoryBot
130
+ factories.
131
+ test_files: []