activerecord-null 0.1.2 → 0.1.4

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: f5da1cf27cad7e88a6c6696874f43ea35670baea4ba390b6d19d2ab9f044d842
4
- data.tar.gz: fd69ad9dfe3b66e186aecb8b6b8411d814fad60ad908d2816e0e9a0ac2ad0a39
3
+ metadata.gz: 7367e3edc8eb1a5d3a539c08cb3ac8bc18a7334d36725edd397ca121adcd143c
4
+ data.tar.gz: 8e86346fcf08e650daeaacc39a70c213dce6cf63119317742fb0ecd5ffbdc142
5
5
  SHA512:
6
- metadata.gz: 5c9f845b168b47184197bfc10367fc4a7b93bec34e9f31e01a81a88c64b4a55e7b1d09e911ed08cf19eb562738cc9255f43e43a03d5a832ced57d295e642fcca
7
- data.tar.gz: a7fb4fe100e29d9151d2fefd5696db658171f4c061a0ded77424b7c1d2e947db4fec50c4c5d6cb0132a56463b22bfddc0da98ce064670d02c1713b159b1042bf
6
+ metadata.gz: 2ec01aaa257aebaf1e5f9773e9d9d54431a0ffedf51adba02426b18552dc1bd20e1f64f3a1613a797fe69330c510526383e295650db1720e50b66b67715d6d25
7
+ data.tar.gz: eaf8a7c1df7a95fb2a48f44f519a81f5961d5ab681daa93933e47f80f88a3bfe335061642da59ad14456245363af478e5f62bbc9ecb616a610273b54ec88f287
data/CHANGELOG.md CHANGED
@@ -5,16 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.1.2] - 2025-06-17
8
+ ## [0.1.4] - 2025-11-19
9
9
 
10
- ## [0.1.1] - 2024-10-25
10
+ ### Changed
11
11
 
12
- ### Added
12
+ - Only initialize attributes when table exists (dba2ace)
13
13
 
14
- - `null?` method to both parent class objects and null objects
15
- - Null objects now have default nil values for attributes of the mimic model class
16
- - `Null()` method can now accept a hash of attribute names and values to assign to the null object
17
- - `has_query_constraints?` method to null objects
18
- - `respond_to?` method to null objects
19
- - `_read_attribute` method to null objects
20
- - Null class now return false for `composite_primary_key?`
14
+ ## [0.1.4] - 2025-11-19
15
+
16
+ ### Changed
17
+
18
+ - Only initialize attributes when table exists (dba2ace)
data/Rakefile CHANGED
@@ -3,7 +3,9 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "minitest/test_task"
5
5
 
6
- Minitest::TestTask.create
6
+ Minitest::TestTask.create do |t|
7
+ t.test_prelude = 'require "test_helper"'
8
+ end
7
9
 
8
10
  task default: :test
9
11
 
@@ -11,4 +13,5 @@ require "reissue/gem"
11
13
 
12
14
  Reissue::Task.create :reissue do |task|
13
15
  task.version_file = "lib/activerecord/null/version.rb"
16
+ task.fragment = :git
14
17
  end
@@ -35,7 +35,13 @@ module ActiveRecord
35
35
 
36
36
  attr_reader :id
37
37
 
38
- def [](*)
38
+ def [](key)
39
+ normalized_key = key.to_sym
40
+ association_names = mimic_model_class.reflect_on_all_associations.map(&:name)
41
+
42
+ return nil if association_names.include?(normalized_key)
43
+
44
+ respond_to?(normalized_key) ? send(normalized_key) : nil
39
45
  end
40
46
 
41
47
  def is_a?(klass)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Null
5
- VERSION = "0.1.2"
5
+ VERSION = "0.1.4"
6
6
  end
7
7
  end
@@ -40,11 +40,17 @@ module ActiveRecord
40
40
  end
41
41
  null_class = Class.new do
42
42
  include ::ActiveRecord::Null::Mimic
43
+
43
44
  mimics inherit
44
45
 
45
46
  include Singleton
46
47
 
48
+ # Store assignments for lazy initialization
49
+ @_null_assignments = assignments
50
+
47
51
  class << self
52
+ attr_reader :_null_assignments
53
+
48
54
  def method_missing(method, ...)
49
55
  mimic_model_class.respond_to?(method) ? mimic_model_class.send(method, ...) : super
50
56
  end
@@ -52,18 +58,42 @@ module ActiveRecord
52
58
  def respond_to_missing?(method, include_private = false)
53
59
  mimic_model_class.respond_to?(method, include_private) || super
54
60
  end
61
+
62
+ # Override instance to initialize attributes lazily
63
+ def instance
64
+ initialize_attribute_methods unless @_attributes_initialized
65
+ super
66
+ end
67
+
68
+ private
69
+
70
+ def initialize_attribute_methods
71
+ # Only initialize if table exists
72
+ return unless mimic_model_class.table_exists?
73
+
74
+ # Define custom assignment methods first
75
+ if _null_assignments.any?
76
+ _null_assignments.each do |attributes, value|
77
+ define_attribute_methods(attributes, value:)
78
+ end
79
+ end
80
+
81
+ # Then define database attributes
82
+ nil_assignments = mimic_model_class.attribute_names
83
+ # Remove custom assignments from database attributes
84
+ if _null_assignments.any?
85
+ _null_assignments.each do |attributes, _|
86
+ nil_assignments -= attributes
87
+ end
88
+ end
89
+ define_attribute_methods(nil_assignments) if nil_assignments.any?
90
+
91
+ @_attributes_initialized = true
92
+ end
55
93
  end
56
94
  end
57
95
  null_class.class_eval(&) if block_given?
58
96
 
59
- nil_assignments = inherit.attribute_names
60
- if assignments.any?
61
- assignments.each do |attributes, value|
62
- nil_assignments -= attributes
63
- null_class.define_attribute_methods(attributes, value:)
64
- end
65
- end
66
- null_class.define_attribute_methods(nil_assignments)
67
97
  inherit.const_set(:Null, null_class)
68
98
 
69
99
  inherit.define_singleton_method(:null) { null_class.instance }
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ # Test that Null classes can be defined without database access
6
+ class LazyLoadingTest < Minitest::Spec
7
+ describe "Lazy attribute loading" do
8
+ it "allows Null class definition without database access" do
9
+ # Create a new model class with a fake table_exists? check
10
+ table_available = false
11
+
12
+ test_class = Class.new(ApplicationRecord) do
13
+ def self.name
14
+ "TestModel"
15
+ end
16
+
17
+ define_singleton_method(:table_exists?) do
18
+ table_available
19
+ end
20
+
21
+ define_singleton_method(:attribute_names) do
22
+ ["id", "name", "email"]
23
+ end
24
+ end
25
+
26
+ # Extend with Null - this should NOT require database
27
+ test_class.extend ActiveRecord::Null
28
+
29
+ # Define the Null class - this should also NOT require database
30
+ # This is the key fix - Null() doesn't call attribute_names
31
+ assert_silent do
32
+ test_class.Null([:name] => "Unknown")
33
+ end
34
+
35
+ # The Null class constant should exist
36
+ assert test_class.const_defined?(:Null)
37
+
38
+ # Now make table available
39
+ table_available = true
40
+
41
+ # When we call .null with table available, attributes get loaded
42
+ null_instance = test_class.null
43
+ assert_instance_of test_class::Null, null_instance
44
+ assert_equal "Unknown", null_instance.name
45
+ assert_nil null_instance.email
46
+ end
47
+
48
+ it "initializes attributes only once" do
49
+ call_count = 0
50
+ test_class = Class.new(ApplicationRecord) do
51
+ def self.name
52
+ "CountingModel"
53
+ end
54
+
55
+ define_singleton_method(:table_exists?) do
56
+ true
57
+ end
58
+
59
+ define_singleton_method(:attribute_names) do
60
+ call_count += 1
61
+ ["id", "value"]
62
+ end
63
+ end
64
+
65
+ test_class.extend ActiveRecord::Null
66
+ test_class.Null
67
+
68
+ # Calling .null multiple times should only initialize once
69
+ assert_equal 0, call_count
70
+ test_class.null
71
+ assert_equal 1, call_count
72
+ test_class.null
73
+ assert_equal 1, call_count
74
+ test_class.null
75
+ assert_equal 1, call_count
76
+ end
77
+
78
+ it "handles missing database gracefully" do
79
+ # Create a model that simulates a missing database
80
+ test_class = Class.new(ApplicationRecord) do
81
+ def self.name
82
+ "MissingDbModel"
83
+ end
84
+
85
+ define_singleton_method(:table_exists?) do
86
+ false
87
+ end
88
+
89
+ define_singleton_method(:attribute_names) do
90
+ raise ActiveRecord::NoDatabaseError, "Database 'test' does not exist"
91
+ end
92
+ end
93
+
94
+ test_class.extend ActiveRecord::Null
95
+
96
+ # Should be able to define Null class without database
97
+ assert_silent do
98
+ test_class.Null([:name] => "Unknown")
99
+ end
100
+
101
+ # Accessing .null returns the instance (but no attributes are defined yet)
102
+ null_instance = test_class.null
103
+ assert_instance_of test_class::Null, null_instance
104
+
105
+ # Without a table, no attributes work (not even custom ones)
106
+ # This is fine - in CI, you wouldn't call .null before running migrations
107
+ assert_raises(NoMethodError) { null_instance.name }
108
+ end
109
+
110
+ it "handles missing table gracefully" do
111
+ # Create a model that simulates a missing table
112
+ test_class = Class.new(ApplicationRecord) do
113
+ def self.name
114
+ "MissingTableModel"
115
+ end
116
+
117
+ define_singleton_method(:table_exists?) do
118
+ false
119
+ end
120
+
121
+ define_singleton_method(:attribute_names) do
122
+ raise ActiveRecord::StatementInvalid, "Table 'missing_table_models' doesn't exist"
123
+ end
124
+ end
125
+
126
+ test_class.extend ActiveRecord::Null
127
+
128
+ # Should be able to define Null class without table
129
+ assert_silent do
130
+ test_class.Null([:status] => "inactive")
131
+ end
132
+
133
+ # Accessing .null returns the instance
134
+ null_instance = test_class.null
135
+ assert_instance_of test_class::Null, null_instance
136
+
137
+ # Without a table, attributes don't work
138
+ assert_raises(NoMethodError) { null_instance.status }
139
+ end
140
+ end
141
+ end
@@ -124,6 +124,68 @@ class ActiveRecord::TestNull < Minitest::Spec
124
124
  it "responds to mimic methods" do
125
125
  expect(Post.null.respond_to?(:description)).must_equal true
126
126
  end
127
+
128
+ describe "bracket access" do
129
+ describe "with attributes" do
130
+ it "accesses attribute with symbol key" do
131
+ expect(User.null[:name]).must_equal "None"
132
+ end
133
+
134
+ it "accesses attribute with string key" do
135
+ expect(User.null["name"]).must_equal "None"
136
+ end
137
+
138
+ it "returns same value for string and symbol keys" do
139
+ expect(User.null[:team_name]).must_equal User.null["team_name"]
140
+ expect(User.null[:team_name]).must_equal "Unknown"
141
+ end
142
+
143
+ it "returns custom attribute values" do
144
+ expect(User.null[:name]).must_equal "None"
145
+ end
146
+
147
+ it "returns static assigned attribute values" do
148
+ expect(User.null[:team_name]).must_equal "Unknown"
149
+ expect(User.null[:other]).must_equal "Unknown"
150
+ end
151
+
152
+ it "returns callable attribute values" do
153
+ expect(Post.null[:description]).must_equal "From the callable!"
154
+ expect(Post.null["description"]).must_equal "From the callable!"
155
+ end
156
+
157
+ it "returns nil for default attributes" do
158
+ expect(User.null[:id]).must_be_nil
159
+ expect(Post.null[:title]).must_be_nil
160
+ end
161
+ end
162
+
163
+ describe "with associations" do
164
+ it "returns nil for belongs_to association" do
165
+ expect(User.null[:business]).must_be_nil
166
+ expect(Post.null[:user]).must_be_nil
167
+ end
168
+
169
+ it "returns nil for has_many association" do
170
+ expect(User.null[:posts]).must_be_nil
171
+ end
172
+
173
+ it "still allows dot notation for associations" do
174
+ expect(User.null.business).must_be_instance_of Business::Null
175
+ expect(User.null.posts).must_be_kind_of ActiveRecord::Relation
176
+ end
177
+ end
178
+
179
+ describe "with non-existent keys" do
180
+ it "returns nil for unknown symbol key" do
181
+ expect(User.null[:nonexistent]).must_be_nil
182
+ end
183
+
184
+ it "returns nil for unknown string key" do
185
+ expect(User.null["nonexistent"]).must_be_nil
186
+ end
187
+ end
188
+ end
127
189
  end
128
190
 
129
191
  describe "Parent class object" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-null
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -39,6 +39,7 @@ files:
39
39
  - lib/activerecord/null.rb
40
40
  - lib/activerecord/null/mimic.rb
41
41
  - lib/activerecord/null/version.rb
42
+ - test/activerecord/test_lazy_loading.rb
42
43
  - test/activerecord/test_null.rb
43
44
  - test/support/schema.rb
44
45
  - test/test_helper.rb
@@ -62,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
63
  - !ruby/object:Gem::Version
63
64
  version: '0'
64
65
  requirements: []
65
- rubygems_version: 3.6.7
66
+ rubygems_version: 3.7.2
66
67
  specification_version: 4
67
68
  summary: Null Objects for ActiveRecord
68
69
  test_files: []