activerecord-null 0.1.4 → 0.1.5

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: 7367e3edc8eb1a5d3a539c08cb3ac8bc18a7334d36725edd397ca121adcd143c
4
- data.tar.gz: 8e86346fcf08e650daeaacc39a70c213dce6cf63119317742fb0ecd5ffbdc142
3
+ metadata.gz: a56f2bfbaf5d498a54f2698e96dc1d460b6a9036ed1c6946e54bcbf724a782c9
4
+ data.tar.gz: 4575ab2f7ffabae11b05bf8429acb57c0810840fd7e996902691f4758d63ce81
5
5
  SHA512:
6
- metadata.gz: 2ec01aaa257aebaf1e5f9773e9d9d54431a0ffedf51adba02426b18552dc1bd20e1f64f3a1613a797fe69330c510526383e295650db1720e50b66b67715d6d25
7
- data.tar.gz: eaf8a7c1df7a95fb2a48f44f519a81f5961d5ab681daa93933e47f80f88a3bfe335061642da59ad14456245363af478e5f62bbc9ecb616a610273b54ec88f287
6
+ metadata.gz: 5f737ae853ffbdf31b4ef323e313f9a844f7b549d2bb3a7aa8d5547af94bfb0779b5d2f099a47b11f6ff4b3a48f163e842d68e8c3efb8b52e519a08ae30dc620
7
+ data.tar.gz: e0af61bf86c190fb5e5ccf05eb7d8ec13f0cbb51e59b48e36eb3e0be1b29f3925ea69e778e391c2b0b700d7e1e053ab801db4840fe4e1ae91a130772b98c3132
data/CHANGELOG.md CHANGED
@@ -5,14 +5,28 @@ 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.4] - 2025-11-19
8
+ ## [0.1.5] - 2025-11-19
9
+
10
+ ### Added
11
+
12
+ - Void() method for defining non-singleton null objects (0e89194)
13
+ - Model.void(attributes) for creating instances with attribute overrides (0e89194)
9
14
 
10
15
  ### Changed
11
16
 
12
- - Only initialize attributes when table exists (dba2ace)
17
+ - Mimic module now delegates table_name to parent model (c6f3956)
18
+ - Extracted create_null_class, setup_singleton_attributes, setup_instance_attributes helpers (b0a77f8)
19
+ - Pass class_name to the Null() or Void() methods with an alternative class name. (d3284c4)
20
+
21
+ ## [0.1.5] - 2025-11-19
22
+
23
+ ### Added
13
24
 
14
- ## [0.1.4] - 2025-11-19
25
+ - Void() method for defining non-singleton null objects (0e89194)
26
+ - Model.void(attributes) for creating instances with attribute overrides (0e89194)
15
27
 
16
28
  ### Changed
17
29
 
18
- - Only initialize attributes when table exists (dba2ace)
30
+ - Mimic module now delegates table_name to parent model (c6f3956)
31
+ - Extracted create_null_class, setup_singleton_attributes, setup_instance_attributes helpers (b0a77f8)
32
+ - Pass class_name to the Null() or Void() methods with an alternative class name. (d3284c4)
data/README.md CHANGED
@@ -68,6 +68,81 @@ class User < ApplicationRecord
68
68
  end
69
69
  ```
70
70
 
71
+ Customize the null class name:
72
+
73
+ ```ruby
74
+ class User < ApplicationRecord
75
+ Null(class_name: "Guest")
76
+ class << self
77
+ alias_method :null, :guest
78
+ end
79
+ end
80
+
81
+ User.guest # returns a User::Guest instance
82
+ ```
83
+
84
+ ### Void Objects
85
+
86
+ While `Null` objects are singletons (one instance per model), `Void` objects are instantiable null objects that allow creating multiple instances with different attribute values.
87
+
88
+ Define a void object for the model:
89
+
90
+ ```ruby
91
+ class Product < ApplicationRecord
92
+ Void([:name] => "Unknown Product") do
93
+ def display_name
94
+ "Product: #{name}"
95
+ end
96
+ end
97
+ end
98
+ ```
99
+
100
+ Create instances with custom attributes:
101
+
102
+ ```ruby
103
+ product1 = Product.void(name: "Widget")
104
+ product2 = Product.void(name: "Gadget")
105
+
106
+ product1.name # => "Widget"
107
+ product2.name # => "Gadget"
108
+ ```
109
+
110
+ Each call to `.void` returns a new instance:
111
+
112
+ ```ruby
113
+ Product.void.object_id != Product.void.object_id # => true
114
+ ```
115
+
116
+ Instance attributes override defaults:
117
+
118
+ ```ruby
119
+ product = Product.void(name: "Custom")
120
+ product.name # => "Custom" (overrides default "Unknown Product")
121
+
122
+ default_product = Product.void
123
+ default_product.name # => "Unknown Product" (uses default)
124
+ ```
125
+
126
+ Void objects support the same features as Null objects:
127
+ - Callable defaults (lambdas/procs)
128
+ - Custom methods via block syntax
129
+ - Association handling
130
+ - All ActiveRecord query methods (`null?`, `persisted?`, etc.)
131
+ - Custom class names via `class_name:` parameter
132
+
133
+ ```ruby
134
+ class Product < ApplicationRecord
135
+ Void(class_name: "Placeholder")
136
+ class << self
137
+ alias_method :void, :placeholder
138
+ end
139
+ end
140
+
141
+ Product.placeholder # returns a Product::Placeholder instance
142
+ ```
143
+
144
+ Use `Null` when you need a single shared null object instance. Use `Void` when you need multiple null object instances with different attribute values.
145
+
71
146
  ## Development
72
147
 
73
148
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -14,7 +14,7 @@ module ActiveRecord
14
14
 
15
15
  def self.mimic_model_class = @mimic_model_class
16
16
 
17
- def self.table_name = @mimic_model_class.to_s.tableize
17
+ def self.table_name = @mimic_model_class.table_name
18
18
 
19
19
  def self.primary_key = @mimic_model_class.primary_key
20
20
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Null
5
- VERSION = "0.1.4"
5
+ VERSION = "0.1.5"
6
6
  end
7
7
  end
@@ -12,44 +12,20 @@ module ActiveRecord
12
12
  # extend ActiveRecord::Null
13
13
  # end
14
14
  module Null
15
- # Define a Null class for the given class.
16
- #
17
- # @example
18
- # class Business < ApplicationRecord
19
- # Null do
20
- # def name = "None"
21
- # end
22
- # end
23
- #
24
- # Business.null # => #<Business::Null:0x0000000000000000>
25
- # Business.null.name # => "None"
26
- #
27
- # class User < ApplicationRecord
28
- # Null([:name, :team_name] => "Unknown")
29
- # end
30
- #
31
- # User.null.name # => "Unknown"
32
- # User.null.team_name # => "Unknown"
33
- #
34
- # @param inherit [Class] The class from which the Null object inherits attributes
35
- # @param assignments [Array] The attributes to assign to the null object
36
- def Null(inherit = self, assignments = {}, &)
37
- if inherit.is_a?(Hash)
38
- assignments = inherit
39
- inherit = self
40
- end
15
+ private
16
+
17
+ # Shared method to create Null or Void classes
18
+ def create_null_class(inherit, assignments, singleton:)
41
19
  null_class = Class.new do
42
20
  include ::ActiveRecord::Null::Mimic
43
21
 
44
22
  mimics inherit
45
23
 
46
- include Singleton
47
-
48
- # Store assignments for lazy initialization
49
- @_null_assignments = assignments
24
+ # Store assignments
25
+ instance_variable_set(:@_assignments, assignments)
50
26
 
51
27
  class << self
52
- attr_reader :_null_assignments
28
+ attr_reader :_assignments
53
29
 
54
30
  def method_missing(method, ...)
55
31
  mimic_model_class.respond_to?(method) ? mimic_model_class.send(method, ...) : super
@@ -58,7 +34,22 @@ module ActiveRecord
58
34
  def respond_to_missing?(method, include_private = false)
59
35
  mimic_model_class.respond_to?(method, include_private) || super
60
36
  end
37
+ end
38
+ end
39
+
40
+ if singleton
41
+ null_class.include(Singleton)
42
+ setup_singleton_attributes(null_class)
43
+ else
44
+ setup_instance_attributes(null_class)
45
+ end
61
46
 
47
+ null_class
48
+ end
49
+
50
+ def setup_singleton_attributes(null_class)
51
+ null_class.class_eval do
52
+ class << self
62
53
  # Override instance to initialize attributes lazily
63
54
  def instance
64
55
  initialize_attribute_methods unless @_attributes_initialized
@@ -68,21 +59,17 @@ module ActiveRecord
68
59
  private
69
60
 
70
61
  def initialize_attribute_methods
71
- # Only initialize if table exists
72
62
  return unless mimic_model_class.table_exists?
73
63
 
74
- # Define custom assignment methods first
75
- if _null_assignments.any?
76
- _null_assignments.each do |attributes, value|
64
+ if _assignments.any?
65
+ _assignments.each do |attributes, value|
77
66
  define_attribute_methods(attributes, value:)
78
67
  end
79
68
  end
80
69
 
81
- # Then define database attributes
82
70
  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, _|
71
+ if _assignments.any?
72
+ _assignments.each do |attributes, _|
86
73
  nil_assignments -= attributes
87
74
  end
88
75
  end
@@ -92,13 +79,125 @@ module ActiveRecord
92
79
  end
93
80
  end
94
81
  end
95
- null_class.class_eval(&) if block_given?
82
+ end
83
+
84
+ def setup_instance_attributes(null_class)
85
+ null_class.class_eval do
86
+ def initialize(attributes = {})
87
+ @_instance_attributes = attributes
88
+ initialize_attribute_methods
89
+ end
90
+
91
+ private
92
+
93
+ def initialize_attribute_methods
94
+ return unless self.class.mimic_model_class.table_exists?
95
+
96
+ assignments = self.class._assignments
97
+
98
+ if assignments.any?
99
+ assignments.each do |attributes, default_value|
100
+ attributes.each do |attr|
101
+ attr_sym = attr.to_sym
102
+ next if respond_to?(attr_sym)
103
+
104
+ define_singleton_method(attr_sym) do
105
+ if @_instance_attributes.key?(attr_sym)
106
+ @_instance_attributes[attr_sym]
107
+ elsif default_value.is_a?(Proc)
108
+ instance_exec(&default_value)
109
+ else
110
+ default_value
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ nil_assignments = self.class.mimic_model_class.attribute_names
118
+ if assignments.any?
119
+ assignments.each do |attributes, _|
120
+ nil_assignments -= attributes.map(&:to_s)
121
+ end
122
+ end
96
123
 
97
- inherit.const_set(:Null, null_class)
124
+ nil_assignments.each do |attr|
125
+ attr_sym = attr.to_sym
126
+ next if respond_to?(attr_sym)
98
127
 
128
+ define_singleton_method(attr_sym) do
129
+ @_instance_attributes.key?(attr_sym) ? @_instance_attributes[attr_sym] : nil
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ public
137
+
138
+ # Define a Null class for the given class.
139
+ #
140
+ # @example
141
+ # class Business < ApplicationRecord
142
+ # Null do
143
+ # def name = "None"
144
+ # end
145
+ # end
146
+ #
147
+ # Business.null # => #<Business::Null:0x0000000000000000>
148
+ # Business.null.name # => "None"
149
+ #
150
+ # class User < ApplicationRecord
151
+ # Null([:name, :team_name] => "Unknown")
152
+ # end
153
+ #
154
+ # User.null.name # => "Unknown"
155
+ # User.null.team_name # => "Unknown"
156
+ #
157
+ # @param inherit [Class] The class from which the Null object inherits attributes
158
+ # @param assignments [Array] The attributes to assign to the null object
159
+ def Null(inherit = self, class_name: :Null, **assignments, &)
160
+ if inherit.is_a?(Hash)
161
+ assignments = inherit
162
+ inherit = self
163
+ end
164
+
165
+ null_class = create_null_class(inherit, assignments, singleton: true)
166
+ null_class.class_eval(&) if block_given?
167
+
168
+ inherit.const_set(class_name, null_class)
99
169
  inherit.define_singleton_method(:null) { null_class.instance }
100
170
  end
101
171
 
172
+ # Define a Void class for the given class.
173
+ # Unlike Null, Void objects are not singletons and can be instantiated
174
+ # multiple times with different attribute values.
175
+ #
176
+ # @example
177
+ # class Product < ApplicationRecord
178
+ # Void do
179
+ # def display_name = "Product: #{name}"
180
+ # end
181
+ # end
182
+ #
183
+ # product1 = Product.void(name: "Widget")
184
+ # product2 = Product.void(name: "Gadget")
185
+ #
186
+ # @param inherit [Class] The class from which the Void object inherits attributes
187
+ # @param assignments [Hash] The default attributes to assign to void objects
188
+ def Void(inherit = self, class_name: :Void, **assignments, &)
189
+ if inherit.is_a?(Hash)
190
+ assignments = inherit
191
+ inherit = self
192
+ end
193
+
194
+ void_class = create_null_class(inherit, assignments, singleton: false)
195
+ void_class.class_eval(&) if block_given?
196
+
197
+ inherit.const_set(class_name, void_class)
198
+ inherit.define_singleton_method(:void) { |attributes = {}| void_class.new(attributes) }
199
+ end
200
+
102
201
  def self.extended(base)
103
202
  base.define_method(:null?) { false }
104
203
  end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ApplicationRecord < ActiveRecord::Base
6
+ primary_abstract_class
7
+ extend ActiveRecord::Null
8
+ end
9
+
10
+ class Product < ApplicationRecord
11
+ self.table_name = "businesses" # Reuse existing table
12
+
13
+ Void([:name] => "Unknown Product") do
14
+ def display_name
15
+ "Product: #{name}"
16
+ end
17
+ end
18
+ end
19
+
20
+ class Comment < ApplicationRecord
21
+ self.table_name = "posts" # Reuse existing table
22
+
23
+ Void()
24
+ end
25
+
26
+ class ActiveRecord::TestVoid < Minitest::Spec
27
+ describe "Void class definition" do
28
+ it "creates Void constant on model" do
29
+ assert Product.const_defined?(:Void)
30
+ end
31
+
32
+ it "Void class includes Mimic" do
33
+ assert Product::Void.include?(ActiveRecord::Null::Mimic)
34
+ end
35
+
36
+ it "Void class does not include Singleton" do
37
+ refute Product::Void.include?(Singleton)
38
+ end
39
+
40
+ it "defines .void method on model" do
41
+ assert Product.respond_to?(:void)
42
+ end
43
+
44
+ it "Void can be defined without arguments" do
45
+ assert Comment.const_defined?(:Void)
46
+ end
47
+ end
48
+
49
+ describe ".void instantiation" do
50
+ it "returns new instance each time" do
51
+ void1 = Product.void
52
+ void2 = Product.void
53
+
54
+ assert_instance_of Product::Void, void1
55
+ assert_instance_of Product::Void, void2
56
+ refute_equal void1.object_id, void2.object_id
57
+ end
58
+
59
+ it "creates instance with no arguments" do
60
+ void_obj = Product.void
61
+
62
+ assert_instance_of Product::Void, void_obj
63
+ end
64
+
65
+ it "creates instance with empty hash" do
66
+ void_obj = Product.void({})
67
+
68
+ assert_instance_of Product::Void, void_obj
69
+ end
70
+ end
71
+
72
+ describe "attribute handling" do
73
+ it "returns default value from hash syntax" do
74
+ void_obj = Product.void
75
+
76
+ assert_equal "Unknown Product", void_obj.name
77
+ end
78
+
79
+ it "allows instance attributes to override defaults" do
80
+ void_obj = Product.void(name: "Custom Product")
81
+
82
+ assert_equal "Custom Product", void_obj.name
83
+ end
84
+
85
+ it "database attributes default to nil" do
86
+ # Assuming businesses table doesn't have a 'price' column
87
+ void_obj = Product.void
88
+
89
+ # Name has a default, so it should return that
90
+ assert_equal "Unknown Product", void_obj.name
91
+ end
92
+
93
+ it "supports multiple instances with different attributes" do
94
+ void1 = Product.void(name: "Product A")
95
+ void2 = Product.void(name: "Product B")
96
+
97
+ assert_equal "Product A", void1.name
98
+ assert_equal "Product B", void2.name
99
+ end
100
+
101
+ it "instance attributes don't affect class defaults" do
102
+ void1 = Product.void(name: "Modified")
103
+ void2 = Product.void
104
+
105
+ assert_equal "Modified", void1.name
106
+ assert_equal "Unknown Product", void2.name
107
+ end
108
+ end
109
+
110
+ describe "custom methods from block" do
111
+ it "block-defined methods are accessible" do
112
+ void_obj = Product.void
113
+
114
+ assert_respond_to void_obj, :display_name
115
+ end
116
+
117
+ it "block methods can access attributes" do
118
+ void_obj = Product.void(name: "Special")
119
+
120
+ assert_equal "Product: Special", void_obj.display_name
121
+ end
122
+
123
+ it "block methods work with defaults" do
124
+ void_obj = Product.void
125
+
126
+ assert_equal "Product: Unknown Product", void_obj.display_name
127
+ end
128
+ end
129
+
130
+ describe "type checking" do
131
+ it "is_a?(Model) returns true" do
132
+ void_obj = Product.void
133
+
134
+ assert void_obj.is_a?(Product)
135
+ end
136
+
137
+ it "is_a?(Model::Void) returns true" do
138
+ void_obj = Product.void
139
+
140
+ assert void_obj.is_a?(Product::Void)
141
+ end
142
+
143
+ it "null? returns true" do
144
+ void_obj = Product.void
145
+
146
+ assert void_obj.null?
147
+ end
148
+
149
+ it "persisted? returns false" do
150
+ void_obj = Product.void
151
+
152
+ refute void_obj.persisted?
153
+ end
154
+
155
+ it "new_record? returns false" do
156
+ void_obj = Product.void
157
+
158
+ refute void_obj.new_record?
159
+ end
160
+
161
+ it "destroyed? returns false" do
162
+ void_obj = Product.void
163
+
164
+ refute void_obj.destroyed?
165
+ end
166
+ end
167
+
168
+ describe "callable attribute values" do
169
+ it "supports callable defaults" do
170
+ test_class = Class.new(ApplicationRecord) do
171
+ def self.name
172
+ "CallableTest"
173
+ end
174
+
175
+ self.table_name = "users"
176
+
177
+ Void([:name] => -> { "Computed Name" })
178
+ end
179
+
180
+ void_obj = test_class.void
181
+
182
+ assert_equal "Computed Name", void_obj.name
183
+ end
184
+
185
+ it "callable defaults can access instance context" do
186
+ test_class = Class.new(ApplicationRecord) do
187
+ def self.name
188
+ "ContextTest"
189
+ end
190
+
191
+ self.table_name = "users"
192
+
193
+ Void([:name] => -> { "Hello" })
194
+
195
+ def greeting
196
+ "Welcome"
197
+ end
198
+ end
199
+
200
+ void_obj = test_class.void
201
+
202
+ assert_equal "Hello", void_obj.name
203
+ end
204
+ end
205
+
206
+ describe "integration with Null" do
207
+ it "Null and Void can coexist on same model" do
208
+ # Define both on a test class
209
+ test_class = Class.new(ApplicationRecord) do
210
+ def self.name
211
+ "TestModel"
212
+ end
213
+
214
+ self.table_name = "users"
215
+
216
+ Null([:name] => "Null Default")
217
+ Void([:name] => "Void Default")
218
+ end
219
+
220
+ null_obj = test_class.null
221
+ void_obj = test_class.void
222
+
223
+ assert_equal "Null Default", null_obj.name
224
+ assert_equal "Void Default", void_obj.name
225
+ assert_equal null_obj.object_id, test_class.null.object_id # Singleton
226
+ refute_equal void_obj.object_id, test_class.void.object_id # Not singleton
227
+ end
228
+ end
229
+
230
+ describe "model integration" do
231
+ it "respects table_name from parent model" do
232
+ assert_equal Product.table_name, Product::Void.table_name
233
+ end
234
+
235
+ it "respects primary_key from parent model" do
236
+ assert_equal "id", Product::Void.primary_key
237
+ end
238
+
239
+ it "has mimic_model_class reference" do
240
+ void_obj = Product.void
241
+
242
+ assert_equal Product, void_obj.mimic_model_class
243
+ end
244
+ end
245
+ end
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.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -41,6 +41,7 @@ files:
41
41
  - lib/activerecord/null/version.rb
42
42
  - test/activerecord/test_lazy_loading.rb
43
43
  - test/activerecord/test_null.rb
44
+ - test/activerecord/test_void.rb
44
45
  - test/support/schema.rb
45
46
  - test/test_helper.rb
46
47
  homepage: https://github.com/SOFware/activerecord-null