fixture_record 0.1.0.pre.rc

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: f064d7bf54652f52a22d8cd269857e20a5b1478d62e56bfc9caf8b49b9835bfe
4
+ data.tar.gz: 169cec159195200f218601686768081c1f8097f2a29bb84ccc39a69d08fb93a1
5
+ SHA512:
6
+ metadata.gz: 5829f715c8d443a626cb5752b5f269ef6d8effbc2e8f34af7dcdcb847d9aac419876a282610802d8fb8ed9b25b4a63d3264bb8dbf717ee5c03451692663d6dfd
7
+ data.tar.gz: 85fa9ff11cf929a973ad4aa1b575003e2832d1bf1cbe49676eafee9753d05c86664907d1733b914caefd9ef7dc76ae2282b348036301be541fe5c6076a8da831
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Brad Schrag
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # FixtureRecord
2
+ When it comes to testing, ActiveRecrod::Fixtures provide a huge performance benefit over FactoryBot but at the expense of setting up the necessary test data. For complex associations and relationships, a large amount of time might be spent simply trying to setup the data. FixtureRecord provides a `to_test_fixture` method that accepts a chain of associations as an argument that allows you to quickly turn a large collection of existing records into test fixtures.
3
+
4
+ ## Usage
5
+ `to_test_fixture` is a method that will turn the record into a test fixture. By default, the name of the fixture record will be `param_key` of the record's class and the record's id joined with `_`.
6
+
7
+ ```ruby
8
+ user = User.find(1)
9
+ user.to_test_fixture
10
+
11
+ # creates test/fixtures/users.yml if it does not exists
12
+ ```
13
+ ```yaml
14
+ # users.yml
15
+
16
+ user_1:
17
+ email: the-user-email@domain.com
18
+ first_name: Foo
19
+ last_name: Bar
20
+ ```
21
+
22
+ ### Associations
23
+ Let's take a basic Blog app with the classes User, Post, and Comment:
24
+ ```ruby
25
+ class User
26
+ has_many :posts, foreign_key: :author_id
27
+ has_many :comments
28
+ end
29
+
30
+ class Post
31
+ has_many :comments
32
+ belongs_to :author, class_name: 'User'
33
+ end
34
+
35
+ class Comment
36
+ belongs_to :commentable, polymorphic: true
37
+ belongs_to :user
38
+ end
39
+ ```
40
+ Let's say an edge case bug has been found with a particular post and it's related comments. To write the test, you need the post record, its authors, the comments, and the user for each of the comments to properly replicate the edge case.
41
+
42
+ ```ruby
43
+ edge_case_post = Post.find ...
44
+ edge_case_post.to_test_fixture(:author, comments: :user)
45
+ ```
46
+ This would create a test fixture for the post, its author, all the comments on the post and their respective users. This will also change the `belongs_to` relationships in the yaml files to reflect their respective fixture counterparts. For example, if `Post#12` author is `User#49`,
47
+ and the post has `Comment#27` the fixture records might look like:
48
+ ```yaml
49
+ # users.yml
50
+
51
+ user_49:
52
+ email: user-49@tester.com
53
+ ...
54
+
55
+ # posts.yml
56
+ post_12:
57
+ author: user_49
58
+ ...
59
+
60
+ # comments.yml
61
+ comment_27:
62
+ user: user_1
63
+ commentable: post_12 (Post)
64
+ ```
65
+
66
+ Note that these changes to the `belongs_to` associations is only applicable to records that are part of the associations that are being passed into `to_test_fixture`. So taking the same example as above, `edge_case_post.to_test_fixture` would yield the following:
67
+ ```yaml
68
+ post_12:
69
+ author_id: 49
70
+ ```
71
+
72
+ Currently, `FixtureRecord` will also not attempt to already existing fixtures to newly created data.
73
+ ```ruby
74
+ User.find(49).to_test_fixture
75
+ Post.find(12).to_test_fixture
76
+ ```
77
+ The above would yield fixtures that are not associated to one another.
78
+ ```yaml
79
+ # users.yml
80
+ user_49:
81
+ ...
82
+
83
+
84
+ # posts.yml
85
+ post_12:
86
+ author_id: 49
87
+ ```
88
+
89
+ ### FixtureRecord::Naming
90
+ There might be instances where a record was used for a particular test fixture and you want to use this same record again for a different test case but want to keep the data isolated. `FixtureRecord::Naming` (automatically included with FixtureRecord) provides`fixture_record_prefix` and `fixture_record_suffix`. These values are propagated to the associated records when calling `to_test_fixture`.
91
+ ```ruby
92
+ user.test_fixture_prefix = :foo
93
+ user.to_test_fixture(:posts)
94
+
95
+ # users.yml
96
+
97
+ foo_user_12:
98
+ ...
99
+
100
+
101
+ # posts.yml
102
+
103
+ foo_post_49:
104
+ author: foo_user_12
105
+ ...
106
+ ```
107
+
108
+ ## Installation
109
+ `FixtureRecord` is only needed as a development dependency.
110
+ ```ruby
111
+ bundle add fixture_record --group development
112
+ ```
113
+
114
+ Or add directly to your Gemfile:
115
+ ```ruby
116
+ # Gemfile
117
+
118
+ group :development do
119
+ ...
120
+ gem 'fixture_record'
121
+ end
122
+ ```
123
+
124
+ And then execute:
125
+ ```bash
126
+ $ bundle install
127
+ ```
128
+
129
+ Finally, run the installer:
130
+ ```bash
131
+ $ rails g fixture_record:install
132
+ ```
133
+
134
+ ## Contributing
135
+ Contribution directions go here.
136
+
137
+ ## License
138
+ 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,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,79 @@
1
+ module FixtureRecord
2
+ module AssociationTraversal
3
+ class UnrecognizedAssociationError < StandardError; end
4
+
5
+ def traverse_fixture_record_associations(*associations)
6
+ associations.each do |association|
7
+ Builder.new(self, association).build
8
+ end
9
+ end
10
+
11
+ class Builder
12
+ def initialize(source_record, association)
13
+ @source_record = source_record
14
+ @association = association
15
+ end
16
+
17
+ def build
18
+ case @association
19
+ when Array then ArrayBuilder.new(@source_record, @association).build
20
+ when Hash then HashBuilder.new(@source_record, @association).build
21
+ when Symbol then SymbolBuilder.new(@source_record, @association).build
22
+ else raise UnrecognizedAssociationTypeError.new(
23
+ "Unrecognized association type of #{@association.class}. Valid types are Hash, Array, or Symbol"
24
+ )
25
+ end
26
+ end
27
+ end
28
+
29
+ class SymbolBuilder
30
+ def initialize(source_record, symbol, *next_associations)
31
+ @source_record = source_record
32
+ @symbol = symbol
33
+ @next_associations = next_associations
34
+ end
35
+
36
+ def build
37
+ raise UnrecognizedAssociationError.new(
38
+ "#{@symbol} is not a recognized association or method on #{@source_record.class}. Is it misspelled?"
39
+ ) unless @source_record.respond_to?(@symbol)
40
+
41
+ built_records = Array.wrap(@source_record.send(@symbol)).compact_blank
42
+ return unless built_records.present?
43
+
44
+ built_records.each do |record|
45
+ record.fixture_record_prefix = @source_record.fixture_record_prefix
46
+ record.fixture_record_suffix = @source_record.fixture_record_suffix
47
+ record.to_test_fixture(*@next_associations)
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ class HashBuilder
54
+ def initialize(source_record, hash)
55
+ @source_record = source_record
56
+ @hash = hash
57
+ end
58
+
59
+ def build
60
+ @hash.each do |symbol, next_associations|
61
+ SymbolBuilder.new(@source_record, symbol, *next_associations).build
62
+ end
63
+ end
64
+ end
65
+
66
+ class ArrayBuilder
67
+ def initialize(source_record, array)
68
+ @source_record = source_record
69
+ @array = array
70
+ end
71
+
72
+ def build
73
+ @array.each do |entry|
74
+ Builder.new(@source_record, entry).build
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,23 @@
1
+ module FixtureRecord
2
+ module BelongsToUpdation
3
+ extend ActiveSupport::Concern
4
+
5
+ def update_belongs_to_test_fixture_associations
6
+ self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
7
+ klass_name = assoc.options[:polymorphic] ? send(assoc.foreign_type) : assoc.class_name
8
+ next unless klass_name.nil? || FixtureRecord.cache.contains_class?(klass_name)
9
+
10
+ belongs_to = send(assoc.name)
11
+ if FixtureRecord.cache.contains_record? belongs_to
12
+ _fixture_record_attributes.delete assoc.foreign_key
13
+ foreign_key_value = belongs_to.test_fixture_name
14
+ if assoc.options[:polymorphic]
15
+ _fixture_record_attributes.delete assoc.foreign_type
16
+ foreign_key_value = "#{foreign_key_value} (#{belongs_to.class})"
17
+ end
18
+ _fixture_record_attributes[assoc.name.to_s] = foreign_key_value
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ module FixtureRecord
2
+ class Cache < Hash
3
+
4
+ def dump!
5
+ prepare!
6
+ merge_data!
7
+ FixtureRecord.data.write!
8
+ end
9
+
10
+ def contains_class?(klass_or_string)
11
+ klass = klass_or_string.is_a?(String) ? klass_or_string.constantize : klass_or_string
12
+ values.map(&:class).include?(klass)
13
+ end
14
+
15
+ def contains_record?(record)
16
+ invert.key? record
17
+ end
18
+
19
+ def merge_data!
20
+ self.each do |fixture_id, record|
21
+ FixtureRecord.data.merge_record(record)
22
+ end
23
+ end
24
+
25
+ def prepare!
26
+ self.values
27
+ .each(&:filter_attributes_for_test_fixture)
28
+ .each(&:sanitize_attributes_for_test_fixture)
29
+ .each(&:update_belongs_to_test_fixture_associations)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ class FixtureRecord::Data < Hash
2
+
3
+ def initialize(...)
4
+ super(...)
5
+ self.default_proc = Proc.new { |hash, klass| hash[klass] = load_fixture_for(klass) }
6
+ end
7
+
8
+ def load_fixture_for(klass)
9
+ if File.exist?(fixture_path_for(klass))
10
+ YAML.load_file(fixture_path_for(klass))
11
+ else
12
+ {}
13
+ end
14
+ end
15
+
16
+ def write!
17
+ FileUtils.mkdir_p(Rails.root.join('test/fixtures'))
18
+ self.each do |klass, data|
19
+ File.open(fixture_path_for(klass), 'w') { |f| f.write data.to_yaml }
20
+ end
21
+ end
22
+
23
+ def fixture_path_for(klass)
24
+ Rails.root.join('test/fixtures', klass.table_name + '.yml')
25
+ end
26
+
27
+ def merge_record(record)
28
+ key = record.test_fixture_name
29
+
30
+ self[record.class].merge!(key => record._fixture_record_attributes) unless self[record.class].key?(key)
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ module FixtureRecord
2
+ module FilterableAttributes
3
+ def filter_attributes_for_test_fixture
4
+ self._fixture_record_attributes = FilteredAttributes.new(self).cast
5
+ end
6
+
7
+ class FilteredAttributes
8
+ attr_reader :record
9
+ def initialize(record)
10
+ @record = record
11
+ end
12
+
13
+ def db_columns
14
+ record.class.columns.map(&:name)
15
+ end
16
+
17
+ def excluded_columns
18
+ %w( id )
19
+ end
20
+
21
+ def applicable_columns
22
+ db_columns - excluded_columns
23
+ end
24
+
25
+ def cast
26
+ record.attributes.slice(*applicable_columns)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ module FixtureRecord
2
+ module Naming
3
+ attr_accessor :fixture_record_prefix
4
+ attr_accessor :fixture_record_suffix
5
+
6
+ def test_fixture_name
7
+ [fixture_record_prefix, self.class.model_name.param_key, (self.id || 'new'), fixture_record_suffix].compact_blank.join '_'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module FixtureRecord
2
+ class Railtie < ::Rails::Railtie
3
+ # generators do
4
+ # require "lib/fixture_record/generators/install"
5
+ # end
6
+ end
7
+ end
@@ -0,0 +1,47 @@
1
+ module FixtureRecord::Sanitizable
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ end
6
+
7
+ def sanitize_attributes_for_test_fixture
8
+ _fixture_record_attributes.each do |attr, value|
9
+ registry_key = [self.class.name, attr.to_s].join('.')
10
+ _fixture_record_attributes[attr] = sanitize_value_for_test_fixture(registry_key, value)
11
+ end
12
+ end
13
+
14
+ def sanitize_value_for_test_fixture(registry_key, value)
15
+ FixtureRecord.registry.fetch(registry_key).inject(value) do |agg, sanitizer|
16
+ agg = sanitizer.cast(agg)
17
+ end
18
+ end
19
+
20
+ class Base
21
+ def cast(value)
22
+ raise NotImplementedError.new "#cast must be defined in #{self.class.name} and it is not"
23
+ end
24
+ end
25
+
26
+ class Registry
27
+ @_fixture_record_sanitizer_pattern_registry = {}
28
+ @_fixture_record_sanitizer_name_registry = {}
29
+
30
+ def self.register_sanitizer(klass, *patterns, as: nil)
31
+ if as
32
+ @_fixture_record_sanitizer_name_registry[as.to_sym] = klass.new
33
+ end
34
+ patterns.each do |pattern|
35
+ @_fixture_record_sanitizer_pattern_registry[pattern] = klass.new
36
+ end
37
+ end
38
+
39
+ def self.fetch(to_be_matched)
40
+ if @_fixture_record_sanitizer_name_registry.key?(to_be_matched)
41
+ @_fixture_record_sanitizer_name_registry[to_be_matched]
42
+ else
43
+ @_fixture_record_sanitizer_pattern_registry.select { |pattern, value| to_be_matched.match(pattern) }.values
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,11 @@
1
+ module FixtureRecord
2
+ module Sanitizers
3
+ class SimpleTimestamp < FixtureRecord::Sanitizer
4
+ def cast(value)
5
+ value.to_s
6
+ end
7
+ end
8
+ FixtureRecord.registry.register_sanitizer(FixtureRecord::Sanitizers::SimpleTimestamp, /created_at$|updated_at$/, as: :simple_timestamp)
9
+ end
10
+ end
11
+
@@ -0,0 +1,3 @@
1
+ module FixtureRecord
2
+ VERSION = "0.1.0-rc"
3
+ end
@@ -0,0 +1,69 @@
1
+ require "fixture_record/version"
2
+ require "fixture_record/railtie"
3
+ require "fixture_record/cache"
4
+ require "fixture_record/data"
5
+ require "fixture_record/naming"
6
+ require "fixture_record/association_traversal"
7
+ require "fixture_record/filterable_attributes"
8
+ require "fixture_record/belongs_to_updation"
9
+ require "fixture_record/sanitizable"
10
+
11
+ module FixtureRecord
12
+ extend ActiveSupport::Concern
13
+ mattr_accessor :_locked_by, :cache, :data
14
+
15
+ included do
16
+ attr_accessor :_fixture_record_attributes
17
+ end
18
+
19
+ include FixtureRecord::Naming
20
+ include FixtureRecord::AssociationTraversal
21
+ include FixtureRecord::FilterableAttributes
22
+ include FixtureRecord::Sanitizable
23
+ include FixtureRecord::BelongsToUpdation
24
+
25
+ Sanitizer = FixtureRecord::Sanitizable::Base
26
+
27
+ def to_test_fixture(*associations)
28
+ FixtureRecord.lock!(self)
29
+ FixtureRecord.cache[self.test_fixture_name] ||= self
30
+ traverse_fixture_record_associations(*associations)
31
+ FixtureRecord.cache.dump! if FixtureRecord.locked_by?(self)
32
+ ensure
33
+ FixtureRecord.release!(self)
34
+ end
35
+
36
+ class << self
37
+ def lock!(owner)
38
+ return if locked?
39
+
40
+ @@_locked_by = owner
41
+ @@cache = FixtureRecord::Cache.new
42
+ @@data = FixtureRecord::Data.new
43
+ end
44
+
45
+ def locked?
46
+ @@_locked_by.present?
47
+ end
48
+
49
+ def release!(owner)
50
+ if locked_by?(owner)
51
+ @@_locked_by = nil
52
+ @@_cache = nil
53
+ true
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def locked_by?(owner)
60
+ @@_locked_by == owner
61
+ end
62
+
63
+ def registry
64
+ FixtureRecord::Sanitizable::Registry
65
+ end
66
+ end
67
+ end
68
+
69
+ require "fixture_record/sanitizers/simple_timestamp"
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Installs FxitureRecord
3
+
4
+ Example:
5
+ rails generate fixture_record:install
6
+
7
+ This will create:
8
+ config/initializers/fixture_record.rb
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FixtureRecord::Generators
4
+ class InstallGenerator < ::Rails::Generators::Base
5
+ source_root File.expand_path("templates", __dir__)
6
+
7
+ def create_fixture_record_schema
8
+ # TODO - implement the generator
9
+ end
10
+
11
+ def create_initializer
12
+ template "initializer.rb", Rails.root.join("config/initializers/fixture_record.rb")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # FixtureRecord is currently only intended to be loaded in a development environment.
2
+ # Change this conditional at your own risk if you want it to load in production.
3
+
4
+ if Rails.env.development?
5
+ # Inject FixtureRecord after active_record loads
6
+ ActiveSupport.on_load(:active_record) do
7
+ ActiveRecord::Base.include(FixtureRecord)
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :fixture_record do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fixture_record
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.rc
5
+ platform: ruby
6
+ authors:
7
+ - Brad Schrag
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-03-24 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
+ description: Helper library for generating test fixtures for existing records and
28
+ their associated associations and relationships.
29
+ email:
30
+ - brad.schrag@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - MIT-LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - lib/fixture_record.rb
39
+ - lib/fixture_record/association_traversal.rb
40
+ - lib/fixture_record/belongs_to_updation.rb
41
+ - lib/fixture_record/cache.rb
42
+ - lib/fixture_record/data.rb
43
+ - lib/fixture_record/filterable_attributes.rb
44
+ - lib/fixture_record/naming.rb
45
+ - lib/fixture_record/railtie.rb
46
+ - lib/fixture_record/sanitizable.rb
47
+ - lib/fixture_record/sanitizers/simple_timestamp.rb
48
+ - lib/fixture_record/version.rb
49
+ - lib/generators/fixture_record/install/USAGE.md
50
+ - lib/generators/fixture_record/install/install_generator.rb
51
+ - lib/generators/fixture_record/install/templates/initializer.rb
52
+ - lib/tasks/fixture_record_tasks.rake
53
+ homepage: https://github.com/bschrag620/fixture_record
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ homepage_uri: https://github.com/bschrag620/fixture_record
58
+ source_code_uri: https://github.com/bschrag620/fixture_record
59
+ changelog_uri: https://github.com/bschrag620/fixture_record
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.5.6
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Helper library for generating test fixtures.
79
+ test_files: []