fixture_kit 0.9.1 → 0.10.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: ad45a151930afb73f5c8ba3f22a93536046fa783d240ea703c248e5329d6ceae
4
- data.tar.gz: fdcf019b31154c187c5057df7990647e2102c173cc6310236866202d549e846c
3
+ metadata.gz: 41dfc0a3fb06edb02068d8127c89708ec7e2c5792b40fb031ee553716a7c54ca
4
+ data.tar.gz: 4193bec6652924d99a7f564212087afa7f678b1f7995fca67f883dcfb1a45d24
5
5
  SHA512:
6
- metadata.gz: 73e72901b28d7f55c7714cbb581b3fa4703533968d23b20c6d0e010be4001a7cc39655ad56726f125d7b655a9ad94ff770735a72eef54a50041b9f21a81511fd
7
- data.tar.gz: b104a22228a595e9cac03e8facb58ca9174b62d22d15cb347b4fd9ef6c5fb11de1187b3d05c1c8cd9bd71ae0b417d965e1af436a0c4af9efdd6f4f06e75649ba
6
+ metadata.gz: e9d7108cd6bda22319c02ea76384772210bdbea00f3ac93c6f6263dd72f5d7475f4cb0087f57f2d316dd17bc2fe36ad3597e87a417dae8149b0404f9196e6a0f
7
+ data.tar.gz: 25c72f2db8d0c93e06e244acc12d224ec36a0cbb85cbbd65c9a4a9d92268b76847c9fb0150508f78d2a1446a7c66587b3c5d479b701c2f072d97e6df92ec289d
@@ -8,10 +8,11 @@ require "active_support/inflector"
8
8
  module FixtureKit
9
9
  class Cache
10
10
  ANONYMOUS_DIRECTORY = "_anonymous"
11
+ MemoryData = Data.define(:records, :exposed)
11
12
 
12
13
  include ConfigurationHelper
13
14
 
14
- attr_reader :fixture
15
+ attr_reader :fixture, :data
15
16
 
16
17
  def initialize(fixture)
17
18
  @fixture = fixture
@@ -33,7 +34,7 @@ module FixtureKit
33
34
  end
34
35
 
35
36
  def exists?
36
- @data || File.exist?(path)
37
+ data || File.exist?(path)
37
38
  end
38
39
 
39
40
  def load
@@ -41,8 +42,8 @@ module FixtureKit
41
42
  raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.identifier}'"
42
43
  end
43
44
 
44
- @data ||= JSON.parse(File.read(path))
45
- statements_by_connection(@data.fetch("records")).each do |connection, statements|
45
+ @data ||= load_memory_data
46
+ statements_by_connection(data.records).each do |connection, statements|
46
47
  connection.disable_referential_integrity do
47
48
  # execute_batch is private in current supported Rails versions.
48
49
  # This should be revisited when Rails 8.2 makes it public.
@@ -50,31 +51,32 @@ module FixtureKit
50
51
  end
51
52
  end
52
53
 
53
- Repository.new(@data.fetch("exposed"))
54
+ Repository.new(data.exposed)
54
55
  end
55
56
 
56
57
  def save
57
58
  FixtureKit.runner.adapter.execute do |context|
58
- models = SqlSubscriber.capture do
59
- fixture.definition.evaluate(context)
59
+ captured_models = SqlSubscriber.capture do
60
+ fixture.definition.evaluate(context, parent: fixture.parent&.mount)
60
61
  end
61
62
 
62
- @data = {
63
- "records" => generate_statements(models),
64
- "exposed" => build_exposed_mapping(fixture.definition.exposed)
65
- }
63
+ if fixture.parent
64
+ captured_models.concat(fixture.parent.cache.data.records.keys)
65
+ end
66
+
67
+ @data = MemoryData.new(
68
+ records: generate_statements(captured_models),
69
+ exposed: build_exposed_mapping(fixture.definition.exposed)
70
+ )
66
71
  end
67
72
 
68
- FileUtils.mkdir_p(File.dirname(path))
69
- File.write(path, JSON.pretty_generate(@data))
73
+ save_file_data
70
74
  end
71
75
 
72
76
  private
73
77
 
74
78
  def generate_statements(models)
75
- statements_by_model = {}
76
-
77
- models.each do |model|
79
+ models.uniq.each_with_object({}) do |model, statements|
78
80
  columns = model.column_names
79
81
 
80
82
  rows = []
@@ -87,10 +89,8 @@ module FixtureKit
87
89
  end
88
90
 
89
91
  sql = rows.empty? ? nil : build_insert_sql(model.table_name, columns, rows, model.connection)
90
- statements_by_model[model.name] = sql
92
+ statements[model] = sql
91
93
  end
92
-
93
- statements_by_model
94
94
  end
95
95
 
96
96
  def build_delete_sql(model)
@@ -105,17 +105,19 @@ module FixtureKit
105
105
  end
106
106
 
107
107
  def build_exposed_mapping(exposed)
108
- exposed.each_with_object({}) do |(name, value), hash|
109
- was_array = value.is_a?(Array)
110
- records = Array.wrap(value).map { |record| { "model" => record.class.name, "id" => record.id } }
111
- hash[name] = was_array ? records : records.first
108
+ exposed.each_with_object({}) do |(name, record), hash|
109
+ if record.is_a?(Array)
110
+ hash[name] = record.map { |record| { record.class => record.id } }
111
+ else
112
+ hash[name] = { record.class => record.id }
113
+ end
112
114
  end
113
115
  end
114
116
 
115
117
  def statements_by_connection(records)
116
118
  deleted_tables = Set.new
117
- records.each_with_object({}) do |(model_name, sql), grouped|
118
- model = ActiveSupport::Inflector.constantize(model_name)
119
+
120
+ records.each_with_object({}) do |(model, sql), grouped|
119
121
  connection = model.connection
120
122
  grouped[connection] ||= []
121
123
 
@@ -127,5 +129,27 @@ module FixtureKit
127
129
  grouped[connection] << sql if sql
128
130
  end
129
131
  end
132
+
133
+ def load_memory_data
134
+ file_data = JSON.parse(File.read(path))
135
+ records = file_data.fetch("records").transform_keys do |model_name|
136
+ ActiveSupport::Inflector.constantize(model_name)
137
+ end
138
+
139
+ exposed = file_data.fetch("exposed").each_with_object({}) do |(name, value), hash|
140
+ if value.is_a?(Array)
141
+ hash[name.to_sym] = value.map { |r| { ActiveSupport::Inflector.constantize(r.keys.first) => r.values.first } }
142
+ else
143
+ hash[name.to_sym] = { ActiveSupport::Inflector.constantize(value.keys.first) => value.values.first }
144
+ end
145
+ end
146
+
147
+ MemoryData.new(records: records, exposed: exposed)
148
+ end
149
+
150
+ def save_file_data
151
+ FileUtils.mkdir_p(File.dirname(path))
152
+ File.write(path, JSON.pretty_generate(data.to_h))
153
+ end
130
154
  end
131
155
  end
@@ -2,16 +2,16 @@
2
2
 
3
3
  module FixtureKit
4
4
  class Definition
5
- attr_reader :exposed, :source_location
5
+ attr_reader :exposed, :extends
6
6
 
7
- def initialize(&definition)
7
+ def initialize(extends: nil, &definition)
8
8
  @definition = definition
9
- @source_location = definition.source_location
10
9
  @exposed = {}
10
+ @extends = extends
11
11
  end
12
12
 
13
- def evaluate(context)
14
- context.singleton_class.prepend(mixin)
13
+ def evaluate(context, parent: nil)
14
+ context.singleton_class.prepend(mixin(parent))
15
15
  context.instance_exec(&@definition)
16
16
  end
17
17
 
@@ -27,13 +27,17 @@ module FixtureKit
27
27
 
28
28
  private
29
29
 
30
- def mixin
30
+ def mixin(parent)
31
31
  definition = self
32
32
 
33
33
  Module.new do
34
34
  define_method(:expose) do |**records|
35
35
  definition.expose(**records)
36
36
  end
37
+
38
+ define_method(:parent) do
39
+ parent
40
+ end
37
41
  end
38
42
  end
39
43
  end
@@ -6,17 +6,23 @@ module FixtureKit
6
6
  class Fixture
7
7
  include ConfigurationHelper
8
8
 
9
- attr_reader :identifier, :definition
9
+ attr_reader :identifier, :definition, :parent, :cache
10
10
 
11
11
  def initialize(identifier, definition)
12
12
  @identifier = identifier
13
13
  @definition = definition
14
14
  @cache = Cache.new(self)
15
+
16
+ if definition.extends
17
+ @parent = FixtureKit.runner.registry.add(definition.extends)
18
+ end
15
19
  end
16
20
 
17
- def cache(force: false)
21
+ def generate(force: false)
18
22
  return if @cache.exists? && !force
19
23
 
24
+ parent&.generate
25
+
20
26
  emit(:cache_save)
21
27
  emit(:cache_saved) { @cache.save }
22
28
  end
@@ -8,8 +8,10 @@ module FixtureKit
8
8
  DECLARATION_CLASS_ATTRIBUTE = :fixture_kit_declaration
9
9
 
10
10
  module ClassMethods
11
- def fixture(name = nil, &definition_block)
12
- self.fixture_kit_declaration = FixtureKit.runner.register(self, name, &definition_block)
11
+ def fixture(name = nil, extends: nil, &block)
12
+ definition = Definition.new(extends: extends, &block) if block_given?
13
+ declaration = FixtureKit.runner.register(name || definition, self)
14
+ self.fixture_kit_declaration = declaration
13
15
  end
14
16
 
15
17
  def run_suite(reporter, options = {})
@@ -17,7 +19,7 @@ module FixtureKit
17
19
  if declaration && !filter_runnable_methods(options).empty?
18
20
  runner = FixtureKit.runner
19
21
  runner.start unless runner.started?
20
- declaration.cache
22
+ declaration.generate
21
23
  end
22
24
 
23
25
  super
@@ -7,22 +7,27 @@ module FixtureKit
7
7
  def initialize
8
8
  @declarations = {}
9
9
  @fixtures = {}
10
+ @resolving = []
10
11
  end
11
12
 
12
- def add(scope, name_or_block)
13
- if @declarations.key?(scope)
14
- raise FixtureKit::MultipleFixtures, "cannot load multiple fixtures in the same context"
15
- end
16
-
17
- @declarations[scope] =
18
- case name_or_block
13
+ def add(name_or_definition, scope = nil)
14
+ fixture =
15
+ case name_or_definition
19
16
  when String
20
- fetch_named_fixture(name_or_block)
21
- when Proc
22
- fetch_anonymous_fixture(scope, name_or_block)
17
+ fetch_named_fixture(name_or_definition)
18
+ when Definition
19
+ raise ArgumentError, "scope is required for anonymous fixture declarations" unless scope
20
+ fetch_anonymous_fixture(scope, name_or_definition)
23
21
  else
24
- raise FixtureKit::InvalidFixtureDeclaration, "unsupported fixture declaration type: #{name_or_block.class}"
22
+ raise FixtureKit::InvalidFixtureDeclaration, "unsupported fixture declaration type: #{name_or_definition.class}"
25
23
  end
24
+
25
+ if scope
26
+ raise FixtureKit::MultipleFixtures, "cannot load multiple fixtures in the same context" if @declarations.key?(scope)
27
+ @declarations[scope] = fixture
28
+ end
29
+
30
+ fixture
26
31
  end
27
32
 
28
33
  def fixtures
@@ -32,11 +37,23 @@ module FixtureKit
32
37
  private
33
38
 
34
39
  def fetch_named_fixture(name)
35
- @fixtures[name] ||= Fixture.new(name, load_named_definition(name))
40
+ return @fixtures[name] if @fixtures.key?(name)
41
+
42
+ if @resolving.include?(name)
43
+ chain = @resolving + [name]
44
+ start = chain.index(name)
45
+ raise FixtureKit::CircularFixtureInheritance,
46
+ "circular fixture inheritance detected: #{chain[start..].join(" -> ")}"
47
+ end
48
+
49
+ @resolving.push(name)
50
+ @fixtures[name] = Fixture.new(name, load_named_definition(name))
51
+ @resolving.pop
52
+ @fixtures[name]
36
53
  end
37
54
 
38
55
  def fetch_anonymous_fixture(scope, definition)
39
- @fixtures[scope] ||= Fixture.new(scope, Definition.new(&definition))
56
+ @fixtures[scope] ||= Fixture.new(scope, definition)
40
57
  end
41
58
 
42
59
  def load_named_definition(name)
@@ -5,7 +5,7 @@ require "active_support/inflector"
5
5
  module FixtureKit
6
6
  class Repository
7
7
  def initialize(exposed_records)
8
- @records = exposed_records.transform_keys(&:to_sym)
8
+ @records = exposed_records
9
9
  @loaded_records = {}
10
10
  define_readers
11
11
  end
@@ -33,8 +33,7 @@ module FixtureKit
33
33
  end
34
34
 
35
35
  def load_record(record_info)
36
- model = ActiveSupport::Inflector.constantize(record_info.fetch("model"))
37
- model.find_by(id: record_info.fetch("id"))
36
+ record_info.keys.first.find_by(id: record_info.values.first)
38
37
  end
39
38
  end
40
39
  end
@@ -27,12 +27,13 @@ module FixtureKit
27
27
  # end
28
28
  # end
29
29
  # end
30
- def fixture(name = nil, &definition_block)
31
- declaration = ::RSpec.configuration.fixture_kit.register(self, name, &definition_block)
30
+ def fixture(name = nil, extends: nil, &block)
31
+ definition = Definition.new(extends: extends, &block) if block_given?
32
+ declaration = ::RSpec.configuration.fixture_kit.register(name || definition, self)
32
33
  metadata[DECLARATION_METADATA_KEY] = declaration
33
34
 
34
35
  prepend_before(:context) do
35
- self.class.metadata[DECLARATION_METADATA_KEY].cache
36
+ self.class.metadata[DECLARATION_METADATA_KEY].generate
36
37
  end
37
38
  end
38
39
  end
@@ -15,8 +15,8 @@ module FixtureKit
15
15
  @started = false
16
16
  end
17
17
 
18
- def register(scope, name = nil, &definition_block)
19
- registry.add(scope, normalize_registration(name, definition_block))
18
+ def register(name_or_definition, scope)
19
+ registry.add(name_or_definition, scope)
20
20
  end
21
21
 
22
22
  def start
@@ -43,16 +43,5 @@ module FixtureKit
43
43
  def preserve_cache?
44
44
  ENV[PRESERVE_CACHE_ENV_KEY].to_s.match?(/\A(1|true|yes)\z/i)
45
45
  end
46
-
47
- def normalize_registration(name, definition_block)
48
- if name && definition_block
49
- raise FixtureKit::InvalidFixtureDeclaration, "cannot provide both fixture name and definition block"
50
- end
51
-
52
- name_or_block = name || definition_block
53
- return name_or_block if name_or_block
54
-
55
- raise FixtureKit::InvalidFixtureDeclaration, "must provide fixture name or definition block"
56
- end
57
46
  end
58
47
  end
@@ -11,8 +11,8 @@ module FixtureKit
11
11
  @runner ||= Runner.new
12
12
  end
13
13
 
14
- def define(&block)
15
- Definition.new(&block)
14
+ def define(extends: nil, &block)
15
+ Definition.new(extends: extends, &block)
16
16
  end
17
17
 
18
18
  def reset
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FixtureKit
4
- VERSION = "0.9.1"
4
+ VERSION = "0.10.0"
5
5
  end
data/lib/fixture_kit.rb CHANGED
@@ -8,6 +8,7 @@ module FixtureKit
8
8
  class CacheMissingError < Error; end
9
9
  class FixtureDefinitionNotFound < Error; end
10
10
  class RunnerAlreadyStartedError < Error; end
11
+ class CircularFixtureInheritance < Error; end
11
12
 
12
13
  autoload :VERSION, File.expand_path("fixture_kit/version", __dir__)
13
14
  autoload :Configuration, File.expand_path("fixture_kit/configuration", __dir__)
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.9.1
4
+ version: 0.10.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-02-24 00:00:00.000000000 Z
11
+ date: 2026-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport