fixture_kit 0.13.0 → 0.15.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: 109e5fa3e9da5e57574f2942265e0a175d7eb7790b9958b645ac32c64384d08b
4
- data.tar.gz: 8082b4b0f1d2c39353aad564ffb4c1b918308b31675f847fc2cf93a6f0020ab2
3
+ metadata.gz: abc9fb9ae8fd65e1d8a6e3ecc6ddb588b57c528424d521a5710998526df5a490
4
+ data.tar.gz: 0c706753a232664188d72ff1caf456ebb18fa38002016f4a266566cf04944820
5
5
  SHA512:
6
- metadata.gz: dfb07fe354ed0a7eb8df7e52fd5807e994a7165579247ab73eebea1c30efe6d505ec7b86c6331c1144aaac652d9521e683fec5ed6a7f2c55c2b0bd83d308184b
7
- data.tar.gz: a1affc9ef20e4bdf533d51614e61ec85d56fd5f429606e83fc417611023bfd2511983a814a55f06c5c3396dd4a07de956b3e123e81cca536620da61797e23ded
6
+ metadata.gz: 2af988212619c3a3ac196f808d53aacf77192a4039f21d70b0cc1fd3592eb171ff485e35ee4925931418850688e1c1491d84266ab35bcfa84e38d918db8bc37f
7
+ data.tar.gz: dcfd8912c6f5dea4a01482623a5b2f9f8f7d187e7d684ef04c2c755c830f15858307dc4917b45ecba164d48c43f24463fb0100676db89c5969a300e303cb0321
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/array/wrap"
4
-
5
3
  module FixtureKit
6
4
  class Cache
7
5
  ANONYMOUS_DIRECTORY = "_anonymous"
8
6
 
9
7
  include ConfigurationHelper
10
8
 
11
- attr_reader :fixture, :data
9
+ attr_reader :fixture, :content
12
10
 
13
11
  def initialize(fixture)
14
12
  @fixture = fixture
@@ -30,11 +28,11 @@ module FixtureKit
30
28
  end
31
29
 
32
30
  def exists?
33
- data || file_cache.exists?
31
+ content || file_cache.exists?
34
32
  end
35
33
 
36
34
  def clear_memory
37
- @data = nil
35
+ @content = nil
38
36
  end
39
37
 
40
38
  def load
@@ -42,88 +40,47 @@ module FixtureKit
42
40
  raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.identifier}'"
43
41
  end
44
42
 
45
- @data ||= file_cache.read
46
- statements_by_connection(data.records).each do |connection, statements|
47
- connection.disable_referential_integrity do
48
- # execute_batch is private in current supported Rails versions.
49
- # This should be revisited when Rails 8.2 makes it public.
50
- connection.__send__(:execute_batch, statements, "FixtureKit Load")
51
- end
43
+ @content ||= file_cache.read
44
+
45
+ FixtureKit.runner.coders.each do |coder|
46
+ coder.mount(content.data_for(coder.class))
52
47
  end
53
48
 
54
- Repository.new(data.exposed)
49
+ Repository.new(content.exposed)
55
50
  end
56
51
 
57
52
  def save
58
53
  FixtureKit.runner.adapter.execute do |context|
59
- captured_models = SqlSubscriber.capture do
60
- fixture.definition.evaluate(context, parent: fixture.parent&.mount)
61
- end
62
-
63
- if fixture.parent
64
- captured_models.concat(fixture.parent.cache.data.records.keys)
65
- end
66
-
67
- @data = MemoryCache.new(
68
- records: generate_statements(captured_models),
54
+ @content = MemoryCache.new(
55
+ data: evaluate(FixtureKit.runner.coders, context),
69
56
  exposed: file_cache.serialize_exposed(fixture.definition.exposed)
70
57
  )
71
58
  end
72
59
 
73
- file_cache.write(data)
60
+ file_cache.write(content)
74
61
  end
75
62
 
76
63
  private
77
64
 
78
- def file_cache
79
- @file_cache ||= FileCache.new(
80
- File.join(configuration.cache_path, "#{identifier}.json")
81
- )
82
- end
65
+ def evaluate(coders, context, data = {}, &block)
66
+ if coders.empty?
67
+ fixture.definition.evaluate(context, parent: fixture.parent&.mount)
68
+ else
69
+ coder, *remaining_coders = coders
83
70
 
84
- def generate_statements(models)
85
- models.uniq.each_with_object({}) do |model, statements|
86
- columns = model.column_names
87
-
88
- rows = []
89
- model.unscoped.order(:id).find_each do |record|
90
- row_values = columns.map do |col|
91
- value = record.read_attribute_before_type_cast(col)
92
- model.connection.quote(value)
93
- end
94
- rows << "(#{row_values.join(", ")})"
71
+ parent_data = fixture.parent ? fixture.parent.cache.content.data_for(coder.class) : nil
72
+ data[coder.class] = coder.generate(parent_data: parent_data) do
73
+ evaluate(remaining_coders, context, data, &block)
95
74
  end
96
-
97
- sql = rows.empty? ? nil : build_insert_sql(model.table_name, columns, rows, model.connection)
98
- statements[model] = sql
99
75
  end
100
- end
101
-
102
- def build_delete_sql(model)
103
- "DELETE FROM #{model.quoted_table_name}"
104
- end
105
-
106
- def build_insert_sql(table_name, columns, rows, connection)
107
- quoted_table = connection.quote_table_name(table_name)
108
- quoted_columns = columns.map { |c| connection.quote_column_name(c) }
109
76
 
110
- "INSERT INTO #{quoted_table} (#{quoted_columns.join(", ")}) VALUES #{rows.join(", ")}"
77
+ data
111
78
  end
112
79
 
113
- def statements_by_connection(records)
114
- deleted_tables = Set.new
115
-
116
- records.each_with_object({}) do |(model, sql), grouped|
117
- connection = model.connection
118
- grouped[connection] ||= []
119
-
120
- table_key = [connection, model.table_name]
121
- if deleted_tables.add?(table_key)
122
- grouped[connection] << build_delete_sql(model)
123
- end
124
-
125
- grouped[connection] << sql if sql
126
- end
80
+ def file_cache
81
+ @file_cache ||= FileCache.new(
82
+ File.join(configuration.cache_path, "#{identifier}.json")
83
+ )
127
84
  end
128
85
  end
129
86
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FixtureKit
4
+ class Coder
5
+ def generate(parent_data: nil, &block)
6
+ raise NotImplementedError, "#{self.class} must implement #generate"
7
+ end
8
+
9
+ def mount(data)
10
+ raise NotImplementedError, "#{self.class} must implement #mount"
11
+ end
12
+
13
+ def encode(data)
14
+ data
15
+ end
16
+
17
+ def decode(data)
18
+ data
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+ require "active_support/inflector"
5
+
6
+ module FixtureKit
7
+ class ActiveRecordCoder < FixtureKit::Coder
8
+ EVENT = "sql.active_record"
9
+ NAME_PATTERN = /\A(?<model_name>.+?) (?:(?:Bulk )?(?:Insert|Upsert)|Create|Destroy|(?:Update|Delete)(?: All)?)\z/
10
+
11
+ def generate(parent_data: nil, &block)
12
+ captured_models = Set.new
13
+ subscriber = lambda do |_event_name, _start, _finish, _id, payload|
14
+ name = payload[:name].to_s
15
+ model_name = name[NAME_PATTERN, :model_name]
16
+ next unless model_name
17
+
18
+ captured_models.add(ActiveSupport::Inflector.constantize(model_name))
19
+ end
20
+
21
+ ActiveSupport::Notifications.subscribed(subscriber, EVENT, monotonic: true, &block)
22
+
23
+ captured_models.map! { |model| base_table_model(model) }
24
+ captured_models.merge(parent_data.keys) if parent_data
25
+
26
+ generate_statements(captured_models)
27
+ end
28
+
29
+ def mount(data)
30
+ statements_by_connection(data).each do |connection, statements|
31
+ connection.disable_referential_integrity do
32
+ # execute_batch is private in current supported Rails versions.
33
+ # This should be revisited when Rails 8.2 makes it public.
34
+ connection.__send__(:execute_batch, statements, "FixtureKit Load")
35
+ end
36
+ end
37
+ end
38
+
39
+ def decode(data)
40
+ data.transform_keys do |model_name|
41
+ ActiveSupport::Inflector.constantize(model_name)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def base_table_model(model)
48
+ model = model.superclass until model.superclass == ActiveRecord::Base || model.superclass.abstract_class?
49
+ model
50
+ end
51
+
52
+ def generate_statements(models)
53
+ models.each_with_object({}) do |model, statements|
54
+ columns = model.column_names
55
+
56
+ rows = []
57
+ model.unscoped.order(:id).find_each do |record|
58
+ row_values = columns.map do |col|
59
+ value = record.read_attribute_before_type_cast(col)
60
+ model.connection.quote(value)
61
+ end
62
+ rows << "(#{row_values.join(", ")})"
63
+ end
64
+
65
+ sql = rows.empty? ? nil : build_insert_sql(model.table_name, columns, rows, model.connection)
66
+ statements[model] = sql
67
+ end
68
+ end
69
+
70
+ def build_delete_sql(model)
71
+ "DELETE FROM #{model.quoted_table_name}"
72
+ end
73
+
74
+ def build_insert_sql(table_name, columns, rows, connection)
75
+ quoted_table = connection.quote_table_name(table_name)
76
+ quoted_columns = columns.map { |c| connection.quote_column_name(c) }
77
+
78
+ "INSERT INTO #{quoted_table} (#{quoted_columns.join(", ")}) VALUES #{rows.join(", ")}"
79
+ end
80
+
81
+ def statements_by_connection(records)
82
+ deleted_tables = Set.new
83
+
84
+ records.each_with_object({}) do |(model, sql), grouped|
85
+ connection = model.connection
86
+ grouped[connection] ||= []
87
+
88
+ table_key = [connection, model.table_name]
89
+ if deleted_tables.add?(table_key)
90
+ grouped[connection] << build_delete_sql(model)
91
+ end
92
+
93
+ grouped[connection] << sql if sql
94
+ end
95
+ end
96
+ end
97
+ end
@@ -4,14 +4,19 @@ module FixtureKit
4
4
  class Configuration
5
5
  attr_accessor :fixture_path
6
6
  attr_accessor :cache_path
7
- attr_reader :adapter_options, :callbacks
7
+ attr_reader :adapter_options, :callbacks, :coders
8
8
 
9
9
  def initialize
10
10
  @fixture_path = "fixture_kit"
11
11
  @cache_path = "tmp/cache/fixture_kit"
12
- @adapter_class = FixtureKit::MinitestAdapter
12
+ @adapter_class = MinitestAdapter
13
13
  @adapter_options = {}
14
14
  @callbacks = Callbacks.new
15
+ @coders = Set.new([ActiveRecordCoder])
16
+ end
17
+
18
+ def register(coder)
19
+ @coders.add(coder)
15
20
  end
16
21
 
17
22
  def adapter(adapter_class = nil, **options)
@@ -10,6 +10,10 @@ module FixtureKit
10
10
  @extends = extends
11
11
  end
12
12
 
13
+ def path
14
+ @definition.source_location.first
15
+ end
16
+
13
17
  def evaluate(context, parent: nil)
14
18
  context.singleton_class.prepend(mixin(parent))
15
19
  context.instance_exec(&@definition)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FixtureKit
4
+ class Event
5
+ attr_reader :fixture
6
+
7
+ def initialize(fixture)
8
+ @fixture = fixture
9
+ end
10
+
11
+ def identifier
12
+ fixture.cache.identifier
13
+ end
14
+
15
+ def path
16
+ fixture.definition.path
17
+ end
18
+ end
19
+ end
@@ -17,12 +17,14 @@ module FixtureKit
17
17
  end
18
18
 
19
19
  def read
20
- file_data = JSON.parse(File.read(path))
21
- records = file_data.fetch("records").transform_keys do |model_name|
22
- ActiveSupport::Inflector.constantize(model_name)
20
+ content = JSON.parse(File.read(path))
21
+
22
+ data = content.fetch("data").to_h do |coder_name, coder_data|
23
+ coder = coder_for(coder_name)
24
+ [coder.class, coder.decode(coder_data)]
23
25
  end
24
26
 
25
- exposed = file_data.fetch("exposed").each_with_object({}) do |(name, value), hash|
27
+ exposed = content.fetch("exposed").each_with_object({}) do |(name, value), hash|
26
28
  if value.is_a?(Array)
27
29
  hash[name.to_sym] = value.map { |r| { ActiveSupport::Inflector.constantize(r.keys.first) => r.values.first } }
28
30
  else
@@ -30,12 +32,20 @@ module FixtureKit
30
32
  end
31
33
  end
32
34
 
33
- MemoryCache.new(records: records, exposed: exposed)
35
+ MemoryCache.new(data: data, exposed: exposed)
34
36
  end
35
37
 
36
38
  def write(data)
39
+ content = {
40
+ data: data.data.to_h do |coder_class, coder_data|
41
+ coder = coder_for(coder_class.name)
42
+ [coder.class, coder.encode(coder_data)]
43
+ end,
44
+ exposed: data.exposed,
45
+ }
46
+
37
47
  FileUtils.mkdir_p(File.dirname(path))
38
- File.write(path, JSON.pretty_generate(data.to_h))
48
+ File.write(path, JSON.pretty_generate(content))
39
49
  end
40
50
 
41
51
  def serialize_exposed(exposed)
@@ -47,5 +57,12 @@ module FixtureKit
47
57
  end
48
58
  end
49
59
  end
60
+
61
+ private
62
+
63
+ def coder_for(class_name)
64
+ @coder_for ||= FixtureKit.runner.coders.index_by { |c| c.class.name }
65
+ @coder_for.fetch(class_name)
66
+ end
50
67
  end
51
68
  end
@@ -46,9 +46,10 @@ module FixtureKit
46
46
  !identifier.is_a?(String)
47
47
  end
48
48
 
49
- def emit(event)
49
+ def emit(event_name)
50
+ fixture_event = Event.new(self)
50
51
  unless block_given?
51
- configuration.callbacks.run(event, @cache)
52
+ configuration.callbacks.run(event_name, fixture_event)
52
53
  return
53
54
  end
54
55
 
@@ -56,7 +57,7 @@ module FixtureKit
56
57
  elapsed = Benchmark.realtime do
57
58
  value = yield
58
59
  end
59
- configuration.callbacks.run(event, @cache, elapsed)
60
+ configuration.callbacks.run(event_name, fixture_event, elapsed)
60
61
  value
61
62
  end
62
63
  end
@@ -2,16 +2,16 @@
2
2
 
3
3
  module FixtureKit
4
4
  class MemoryCache
5
- attr_reader :records, :exposed
5
+ attr_reader :data, :exposed
6
6
 
7
- def initialize(records:, exposed:)
8
- @records = records
7
+ def initialize(data:, exposed:)
8
+ @data = data
9
9
  @exposed = exposed
10
10
  freeze
11
11
  end
12
12
 
13
- def to_h
14
- { records: records, exposed: exposed }
13
+ def data_for(coder_class)
14
+ data.fetch(coder_class)
15
15
  end
16
16
  end
17
17
  end
@@ -30,6 +30,10 @@ module FixtureKit
30
30
  @adapter ||= configuration.adapter.new(configuration.adapter_options)
31
31
  end
32
32
 
33
+ def coders
34
+ @coders ||= configuration.coders.map(&:new)
35
+ end
36
+
33
37
  def started?
34
38
  @started
35
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FixtureKit
4
- VERSION = "0.13.0"
4
+ VERSION = "0.15.0"
5
5
  end
data/lib/fixture_kit.rb CHANGED
@@ -15,11 +15,11 @@ module FixtureKit
15
15
  autoload :Callbacks, File.expand_path("fixture_kit/callbacks", __dir__)
16
16
  autoload :ConfigurationHelper, File.expand_path("fixture_kit/configuration_helper", __dir__)
17
17
  autoload :Singleton, File.expand_path("fixture_kit/singleton", __dir__)
18
+ autoload :Event, File.expand_path("fixture_kit/event", __dir__)
18
19
  autoload :Fixture, File.expand_path("fixture_kit/fixture", __dir__)
19
20
  autoload :Definition, File.expand_path("fixture_kit/definition", __dir__)
20
21
  autoload :Registry, File.expand_path("fixture_kit/registry", __dir__)
21
22
  autoload :Repository, File.expand_path("fixture_kit/repository", __dir__)
22
- autoload :SqlSubscriber, File.expand_path("fixture_kit/sql_subscriber", __dir__)
23
23
  autoload :Cache, File.expand_path("fixture_kit/cache", __dir__)
24
24
  autoload :FileCache, File.expand_path("fixture_kit/file_cache", __dir__)
25
25
  autoload :MemoryCache, File.expand_path("fixture_kit/memory_cache", __dir__)
@@ -27,6 +27,8 @@ module FixtureKit
27
27
  autoload :Adapter, File.expand_path("fixture_kit/adapter", __dir__)
28
28
  autoload :MinitestAdapter, File.expand_path("fixture_kit/adapters/minitest_adapter", __dir__)
29
29
  autoload :RSpecAdapter, File.expand_path("fixture_kit/adapters/rspec_adapter", __dir__)
30
+ autoload :Coder, File.expand_path("fixture_kit/coder", __dir__)
31
+ autoload :ActiveRecordCoder, File.expand_path("fixture_kit/coders/active_record_coder", __dir__)
30
32
 
31
33
  extend Singleton
32
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fixture_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ngan Pham
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-07 00:00:00.000000000 Z
11
+ date: 2026-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -160,9 +160,12 @@ files:
160
160
  - lib/fixture_kit/analyzer/text_formatter.rb
161
161
  - lib/fixture_kit/cache.rb
162
162
  - lib/fixture_kit/callbacks.rb
163
+ - lib/fixture_kit/coder.rb
164
+ - lib/fixture_kit/coders/active_record_coder.rb
163
165
  - lib/fixture_kit/configuration.rb
164
166
  - lib/fixture_kit/configuration_helper.rb
165
167
  - lib/fixture_kit/definition.rb
168
+ - lib/fixture_kit/event.rb
166
169
  - lib/fixture_kit/file_cache.rb
167
170
  - lib/fixture_kit/fixture.rb
168
171
  - lib/fixture_kit/memory_cache.rb
@@ -172,7 +175,6 @@ files:
172
175
  - lib/fixture_kit/rspec.rb
173
176
  - lib/fixture_kit/runner.rb
174
177
  - lib/fixture_kit/singleton.rb
175
- - lib/fixture_kit/sql_subscriber.rb
176
178
  - lib/fixture_kit/version.rb
177
179
  homepage: https://github.com/Gusto/fixture_kit
178
180
  licenses:
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/notifications"
4
- require "active_support/inflector"
5
-
6
- module FixtureKit
7
- class SqlSubscriber
8
- EVENT = "sql.active_record"
9
- NAME_PATTERN = /\A(?<model_name>.+?) (?:(?:Bulk )?(?:Insert|Upsert)|Create|Destroy|(?:Update|Delete)(?: All)?)\z/
10
-
11
- def self.capture(&block)
12
- models = Set.new
13
- subscriber = lambda do |_event_name, _start, _finish, _id, payload|
14
- name = payload[:name].to_s
15
- model_name = name[NAME_PATTERN, :model_name]
16
- next unless model_name
17
-
18
- models.add(ActiveSupport::Inflector.constantize(model_name))
19
- end
20
-
21
- ActiveSupport::Notifications.subscribed(subscriber, EVENT, monotonic: true, &block)
22
-
23
- models.map { |model| base_table_model(model) }.uniq
24
- end
25
-
26
- def self.base_table_model(model)
27
- model = model.superclass until model.superclass == ActiveRecord::Base || model.superclass.abstract_class?
28
- model
29
- end
30
- private_class_method :base_table_model
31
- end
32
- end