fixture_record 0.1.1.pre.rc → 1.0

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