fixture_record 0.1.1.pre.rc → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6720df231526a9d9119a2146337d2c029e5c1479eaea6f88b17df9ce8e1222fc
4
- data.tar.gz: '0579da7648101dbdf1f3ca02add8ac87a23192fb2ba635ac6749f87e394d062e'
3
+ metadata.gz: 725ff327350c904ec886ba1833e976a798e47d564aac8539dec6f2614d86f298
4
+ data.tar.gz: d820511bb21ee20a76c5e12109030e56bde60e3eab69c100248285d59dbb9047
5
5
  SHA512:
6
- metadata.gz: 1de630e483468ae70fb9e41351f5607fee4516ffea36f9008860a801c7a95c2347d608bf2c6d50c559d302eac7eb0deb2d606cd71e4914e8c8728e09d3bb60d1
7
- data.tar.gz: 6d21e6b3cd38b97122fdf567df30b9d657df3a7787e481ed79343dd691edfd48882b353f96fb3edc2693b7336b67e5c2448ec75bd324dd61c0453708ad5ea7e4
6
+ metadata.gz: 14a8530fe32a301bdc5bc4182da67fd8751823d0d43f3d9cd708e23a4c2d99c9b292f77c47043d1ddb79b4eb75f0326382f59425b37d3986e5143328a8ebd046
7
+ data.tar.gz: 90bf683053f43a2e0f7f73acb7b6a260ac2ae6748fbffb197fefc7c6fb5cdf1c7d0e6822baa558cdba82f4fbc7a7ab587882d841566a5833d5bcfb10fe379c80
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
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.
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_fixture_record` 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
3
 
4
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 `_`.
5
+ `to_fixture_record` 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
6
 
7
7
  ```ruby
8
8
  user = User.find(1)
9
- user.to_test_fixture
9
+ user.to_fixture_record
10
10
 
11
11
  # creates test/fixtures/users.yml if it does not exists
12
12
  ```
@@ -41,7 +41,7 @@ Let's say an edge case bug has been found with a particular post and it's relate
41
41
 
42
42
  ```ruby
43
43
  edge_case_post = Post.find ...
44
- edge_case_post.to_test_fixture(:author, comments: :user)
44
+ edge_case_post.to_fixture_record(:author, comments: :user)
45
45
  ```
46
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
47
  and the post has `Comment#27` the fixture records might look like:
@@ -63,7 +63,7 @@ comment_27:
63
63
  commentable: post_12 (Post)
64
64
  ```
65
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:
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_fixture_record`. So taking the same example as above, `edge_case_post.to_fixture_record` would yield the following:
67
67
  ```yaml
68
68
  post_12:
69
69
  author_id: 49
@@ -71,8 +71,8 @@ post_12:
71
71
 
72
72
  Currently, `FixtureRecord` will also not attempt to already existing fixtures to newly created data.
73
73
  ```ruby
74
- User.find(49).to_test_fixture
75
- Post.find(12).to_test_fixture
74
+ User.find(49).to_fixture_record
75
+ Post.find(12).to_fixture_record
76
76
  ```
77
77
  The above would yield fixtures that are not associated to one another.
78
78
  ```yaml
@@ -97,19 +97,19 @@ end
97
97
  ```
98
98
  Because of through association infilling the following 3 lines will produce identical results:
99
99
  ```ruby
100
- user.to_test_fixture(posts: [comments: :users])
100
+ user.to_fixture_record(posts: [comments: :users])
101
101
 
102
- user.to_test_fixture(:posts, :post_comments, :commenting_users)
102
+ user.to_fixture_record(:posts, :post_comments, :commenting_users)
103
103
 
104
104
  user.to_test_fixutre(:commenting_users)
105
105
  ```
106
106
  The reason the third example will infill the other associations is because those associations are required to create a clear path between the originating record and the final records. Without those intermediary associations, the `:commenting_users` would be orphaned from the `user` record.
107
107
 
108
108
  ### FixtureRecord::Naming
109
- 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`.
109
+ 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_fixture_record`.
110
110
  ```ruby
111
111
  user.test_fixture_prefix = :foo
112
- user.to_test_fixture(:posts)
112
+ user.to_fixture_record(:posts)
113
113
 
114
114
  # users.yml
115
115
 
@@ -124,6 +124,86 @@ foo_post_49:
124
124
  ...
125
125
  ```
126
126
 
127
+ ## Data Sanitizing and Obfuscation
128
+ `FixtureRecord` supports mutating data before writing the data. By default, `FixtureRecord` has a built in sanitizer that is used for `created_at` and `updated_at` fields. The reason for the `simple_timestamp` is that Rails will turn a timestamp into a complex object when calling `to_yaml`.
129
+ ```ruby
130
+ user.attributes.to_yaml
131
+ # =>
132
+ id: ...
133
+ created_at: !ruby/object:ActiveSupport::TimeWithZone
134
+ utc: 2024-03-17 23:11:31.329037000 Z
135
+ zone: &1 !ruby/object:ActiveSupport::TimeZone
136
+ name: Etc/UTC
137
+ time: 2024-03-17 23:11:31.329037000 Z
138
+ updated_at: !ruby/object:ActiveSupport::TimeWithZone
139
+ utc: 2024-03-17 23:11:31.329037000 Z
140
+ zone: *1
141
+ time: 2024-03-17 23:11:31.329037000 Z
142
+ ```
143
+ This type of timestamp structure isn't needed and can simply clutter up a fixture file. So instead, `FixtureRecord` comes with a sanitizer to clean this up.
144
+ ```ruby
145
+ # lib/fixture_record/sanitizers/simple_timestamp
146
+ module FixtureRecord
147
+ module Sanitizers
148
+ class SimpleTimestamp < FixtureRecord::Sanitizer
149
+ def cast(value)
150
+ value.to_s
151
+ end
152
+ end
153
+ FixtureRecord.registry.register_sanitizer(FixtureRecord::Sanitizers::SimpleTimestamp, as: :simple_timestamp)
154
+ end
155
+ end
156
+
157
+ # fixture_record/initializer.rb (created using the install script)
158
+ FixtureRecord.configure do |config|
159
+ ...
160
+
161
+ config.sanitize_column_regex /created_at$|updated_at$/, with: :simple_timestamp
162
+
163
+ ...
164
+ end
165
+ ```
166
+
167
+ In this case, any column the regex pattern of 'created_at' or 'updated_at' will have its value passed to the registered sanitizer class.
168
+
169
+ ### Creating a Custom Sanitizer
170
+ Step one, create the custom class. Inheriting from `FixtureRecord::Sanitizer` is not a requirement currently, but there might be some nice-to-have features as part of that class in the future. At minimum, your custom class needs to have at minimum a `#cast` instance method that will receive the value that is to be sanitized and returns the newly converted value. Currently, `#cast` will be called whether the value is `nil` or not, so be sure your method can handle the `nil` scenario.
171
+ ```ruby
172
+ class MyReverseSanitizer < FixtureRecord::Sanitizer
173
+ def cast(value)
174
+ value&.reverse
175
+ end
176
+ end
177
+ ```
178
+
179
+ ### Registering the Sanitizer
180
+ In your custom class, or in the initializer, register the new sanitizer.
181
+ ```ruby
182
+ class MyReverseSanitizer < FixtureRecord::Sanitizer
183
+ ...
184
+ end
185
+
186
+ FixtureRecord.registry.register_sanitizer MyReverseSanitizer, :reverse
187
+ ```
188
+ ### Assiging the Sanitizer to a Pattern
189
+ In the fixture record initializer, use `#sanitize_column_regex` to assign the registered sanitizer to a regex pattern. In the following example code, any column that matches `email` would be sent through the reverse sanitizer, this would include `email`, `user_email`, `primary_email`, etc.
190
+ ```ruby
191
+ # fixture_record/initializer.rb
192
+ FixtureRecord.configure do |config|
193
+ ...
194
+
195
+ config.sanitize_column_regex /email/, with: :reverse
196
+
197
+ ...
198
+ end
199
+ ```
200
+
201
+ The pattern that is used for comparison is inclusive of the class name as well. So if you need a sanitizer to be scoped to a specific class you can use the class name in the regex pattern. Taking the example above:
202
+ ```ruby
203
+ config.sanitize_column_regex /User.email/, with: :reverse
204
+ ```
205
+ Now columns on other classes that include `email` in their name won't be passed to the sanitizer. Also keep in mind the mechanism being used here is basic regex pattern matching, so `User.primary_email` wouldn't match in this case and would not be sent to the sanitizer.
206
+
127
207
  ## Installation
128
208
  `FixtureRecord` is only needed as a development dependency.
129
209
  ```ruby
@@ -2,7 +2,10 @@ module FixtureRecord
2
2
  module AssociationTraversal
3
3
  class UnrecognizedAssociationError < StandardError; end
4
4
 
5
+ attr_accessor :_traversed_fixture_record_associations
6
+
5
7
  def traverse_fixture_record_associations(*associations)
8
+ self._traversed_fixture_record_associations = []
6
9
  associations.each do |association|
7
10
  Builder.new(self, association).build
8
11
  end
@@ -36,18 +39,34 @@ module FixtureRecord
36
39
  def build
37
40
  raise UnrecognizedAssociationError.new(
38
41
  "#{@symbol} is not a recognized association or method on #{@source_record.class}. Is it misspelled?"
39
- ) unless @source_record.respond_to?(@symbol)
42
+ ) unless klass_association.present?
43
+
44
+ if through_assoc_option && @source_record._traversed_fixture_record_associations.exclude?(through_assoc_option)
45
+ infill_through
46
+ end
40
47
 
48
+ @source_record._traversed_fixture_record_associations << @symbol
41
49
  built_records = Array.wrap(@source_record.send(@symbol)).compact_blank
42
50
  return unless built_records.present?
43
51
 
44
52
  built_records.each do |record|
45
53
  record.fixture_record_prefix = @source_record.fixture_record_prefix
46
54
  record.fixture_record_suffix = @source_record.fixture_record_suffix
47
- record.to_test_fixture(*@next_associations)
55
+ record.to_fixture_record(*@next_associations)
48
56
  end
49
57
  end
50
58
 
59
+ def infill_through
60
+ SymbolBuilder.new(@source_record, through_assoc_option).build
61
+ end
62
+
63
+ def through_assoc_option
64
+ klass_association.options[:through]
65
+ end
66
+
67
+ def klass_association
68
+ @source_record.class.reflect_on_association(@symbol)
69
+ end
51
70
  end
52
71
 
53
72
  class HashBuilder
@@ -2,7 +2,7 @@ module FixtureRecord
2
2
  module BelongsToUpdation
3
3
  extend ActiveSupport::Concern
4
4
 
5
- def update_belongs_to_test_fixture_associations
5
+ def update_belongs_to_fixture_record_associations
6
6
  self.class.reflect_on_all_associations(:belongs_to).each do |assoc|
7
7
  klass_name = assoc.options[:polymorphic] ? send(assoc.foreign_type) : assoc.class_name
8
8
  next unless klass_name.nil? || FixtureRecord.cache.contains_class?(klass_name)
@@ -24,9 +24,9 @@ module FixtureRecord
24
24
 
25
25
  def prepare!
26
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)
27
+ .each(&:filter_attributes_for_fxiture_record)
28
+ .each(&:sanitize_attributes_for_fxiture_record)
29
+ .each(&:update_belongs_to_fixture_record_associations)
30
30
  end
31
31
  end
32
32
  end
@@ -14,14 +14,18 @@ class FixtureRecord::Data < Hash
14
14
  end
15
15
 
16
16
  def write!
17
- FileUtils.mkdir_p(Rails.root.join('test/fixtures'))
17
+ FileUtils.mkdir_p(FixtureRecord.base_path)
18
18
  self.each do |klass, data|
19
19
  File.open(fixture_path_for(klass), 'w') { |f| f.write data.to_yaml }
20
20
  end
21
21
  end
22
22
 
23
23
  def fixture_path_for(klass)
24
- Rails.root.join('test/fixtures', klass.table_name + '.yml')
24
+ if FixtureRecord.base_path.is_a?(String)
25
+ [FixtureRecord.base_path, klass.table_name + '.yml'].join('/')
26
+ else
27
+ FixtureRecord.base_path.join(klass.table_name + '.yml')
28
+ end
25
29
  end
26
30
 
27
31
  def merge_record(record)
@@ -1,6 +1,6 @@
1
1
  module FixtureRecord
2
2
  module FilterableAttributes
3
- def filter_attributes_for_test_fixture
3
+ def filter_attributes_for_fxiture_record
4
4
  self._fixture_record_attributes = FilteredAttributes.new(self).cast
5
5
  end
6
6
 
@@ -3,8 +3,15 @@ module FixtureRecord
3
3
  attr_accessor :fixture_record_prefix
4
4
  attr_accessor :fixture_record_suffix
5
5
 
6
+ class Base
7
+ def call(record)
8
+
9
+ [record.fixture_record_prefix, record.class.model_name.param_key, record.id || 'new', record.fixture_record_suffix].compact_blank.join('_')
10
+ end
11
+ end
12
+
6
13
  def test_fixture_name
7
- [fixture_record_prefix, self.class.model_name.param_key, (self.id || 'new'), fixture_record_suffix].compact_blank.join '_'
14
+ FixtureRecord.name_handler.call(self)
8
15
  end
9
16
  end
10
17
  end
@@ -1,10 +1,7 @@
1
1
  module FixtureRecord::Sanitizable
2
2
  extend ActiveSupport::Concern
3
3
 
4
- class_methods do
5
- end
6
-
7
- def sanitize_attributes_for_test_fixture
4
+ def sanitize_attributes_for_fxiture_record
8
5
  _fixture_record_attributes.each do |attr, value|
9
6
  registry_key = [self.class.name, attr.to_s].join('.')
10
7
  _fixture_record_attributes[attr] = sanitize_value_for_test_fixture(registry_key, value)
@@ -24,24 +21,27 @@ module FixtureRecord::Sanitizable
24
21
  end
25
22
 
26
23
  class Registry
27
- @_fixture_record_sanitizer_pattern_registry = {}
24
+ @_fixture_record_sanitizer_pattern_registry = []
28
25
  @_fixture_record_sanitizer_name_registry = {}
29
26
 
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
27
+ def self.[](...)
28
+ @_fixture_record_sanitizer_name_registry.send(:[], ...)
29
+ end
30
+
31
+ def self.deregister(klass_or_symbol)
32
+ @_fixture_record_sanitizer_pattern_registry.delete_if { |pattern, with| with == klass_or_symbol }
33
+ end
34
+
35
+ def self.sanitize_pattern(pattern, with:)
36
+ @_fixture_record_sanitizer_pattern_registry << [pattern, with]
37
+ end
38
+
39
+ def self.register_sanitizer(klass, as: nil)
40
+ @_fixture_record_sanitizer_name_registry[as.to_sym] = klass.new
37
41
  end
38
42
 
39
43
  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
44
+ @_fixture_record_sanitizer_pattern_registry.select { |pattern, value| to_be_matched.match(pattern) }.map(&:last)
45
45
  end
46
46
  end
47
47
  end
@@ -5,7 +5,6 @@ module FixtureRecord
5
5
  value.to_s
6
6
  end
7
7
  end
8
- FixtureRecord.registry.register_sanitizer(FixtureRecord::Sanitizers::SimpleTimestamp, /created_at$|updated_at$/, as: :simple_timestamp)
8
+ FixtureRecord.registry.register_sanitizer(FixtureRecord::Sanitizers::SimpleTimestamp, as: :simple_timestamp)
9
9
  end
10
10
  end
11
-
@@ -1,3 +1,3 @@
1
1
  module FixtureRecord
2
- VERSION = "0.1.1-rc"
2
+ VERSION = "1.0"
3
3
  end
@@ -10,7 +10,13 @@ require "fixture_record/sanitizable"
10
10
 
11
11
  module FixtureRecord
12
12
  extend ActiveSupport::Concern
13
- mattr_accessor :_locked_by, :cache, :data
13
+ mattr_accessor :_locked_by,
14
+ :cache,
15
+ :data
16
+
17
+ mattr_accessor :name_handler, default: FixtureRecord::Naming::Base.new
18
+ mattr_accessor :sanitizers, default: []
19
+ mattr_accessor :base_path, default: -> { Rails.root.join('test/fixtures') }
14
20
 
15
21
  included do
16
22
  attr_accessor :_fixture_record_attributes
@@ -24,7 +30,7 @@ module FixtureRecord
24
30
 
25
31
  Sanitizer = FixtureRecord::Sanitizable::Base
26
32
 
27
- def to_test_fixture(*associations)
33
+ def to_fixture_record(*associations)
28
34
  FixtureRecord.lock!(self)
29
35
  FixtureRecord.cache[self.test_fixture_name] ||= self
30
36
  traverse_fixture_record_associations(*associations)
@@ -33,7 +39,27 @@ module FixtureRecord
33
39
  FixtureRecord.release!(self)
34
40
  end
35
41
 
42
+
36
43
  class << self
44
+ def configure
45
+ yield self
46
+ end
47
+
48
+ def base_path
49
+ @@base_path.is_a?(String) ? @@base_path : @@base_path.call
50
+ end
51
+
52
+ def name_handler=(proc_or_klass)
53
+ @@name_handler = proc_or_klass.is_a?(Class) ? proc_or_klass.new : proc_or_klass
54
+ end
55
+
56
+ def sanitize_column_regex(col_regex, with:)
57
+ registry_name_or_klass = with
58
+ klass = registry_name_or_klass.is_a?(Symbol) ? FixtureRecord.registry[registry_name_or_klass] : registry_name_or_klass
59
+ klass_instance = klass.is_a?(Class) ? klass.new : klass
60
+ FixtureRecord.registry.sanitize_pattern col_regex, with: klass_instance
61
+ end
62
+
37
63
  def lock!(owner)
38
64
  return if locked?
39
65
 
@@ -4,10 +4,6 @@ module FixtureRecord::Generators
4
4
  class InstallGenerator < ::Rails::Generators::Base
5
5
  source_root File.expand_path("templates", __dir__)
6
6
 
7
- def create_fixture_record_schema
8
- # TODO - implement the generator
9
- end
10
-
11
7
  def create_initializer
12
8
  template "initializer.rb", Rails.root.join("config/initializers/fixture_record.rb")
13
9
  end
@@ -1,9 +1,23 @@
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
1
  if Rails.env.development?
5
- # Inject FixtureRecord after active_record loads
6
- ActiveSupport.on_load(:active_record) do
2
+ FixtureRecord.configure do |config|
3
+ # To customize how fixtures are named, provide a class the responds to #call or a Proc.
4
+ # The name handler object will receive the record and should return a String
5
+ # config.name_handler = FixtureRecord::Naming::Base
6
+
7
+ # base_path represents the base folder that FixureRecord will search for existing yml files
8
+ # to merge new fixture data with and it serves as the path for where FixtureRecord will output the
9
+ # new yml files as needed. To override, provide a String, Pathname object, or Proc/ambda to be evaluated at runtime
10
+ # config.base_path = -> { Rails.root.join('test/fixtures') }
11
+
12
+ # Create and register custom sanitizers to format, sanitiize, obfuscate, etc. the data before it is
13
+ # turned into a test fixture. Regex patterns are used to determine if a column should be passed to a
14
+ # sanitizer. The regex pattern that is tested is Classname.column_name - so if a sanitizer needs to be
15
+ # scoped to a specific class only, simply add the classname to the pattern, for example /User.phone_number/
16
+ # would sanitize the phone_number field for a User but not the phone_number field for a Customer.
17
+ # If there are other timestamp columns being used throughout your application, you can added them to this list.
18
+ config.sanitize_column_regex /created_at$|updated_at$/, with: :simple_timestamp
19
+
20
+ # Inject FixtureRecord concern into ActiveRecord
7
21
  ActiveRecord::Base.include(FixtureRecord)
8
22
  end
9
23
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fixture_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1.pre.rc
4
+ version: '1.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brad Schrag
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-28 00:00:00.000000000 Z
11
+ date: 2024-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails