fixtures_from_factories 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []