Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,56 @@ class User < ApplicationRecord
end
```

### Void Objects

While `Null` objects are singletons (one instance per model), `Void` objects are instantiable null objects that allow creating multiple instances with different attribute values.

Define a void object for the model:

```ruby
class Product < ApplicationRecord
Void([:name] => "Unknown Product") do
def display_name
"Product: #{name}"
end
end
end
```

Create instances with custom attributes:

```ruby
product1 = Product.void(name: "Widget")
product2 = Product.void(name: "Gadget")

product1.name # => "Widget"
product2.name # => "Gadget"
```

Each call to `.void` returns a new instance:

```ruby
Product.void.object_id != Product.void.object_id # => true
```

Instance attributes override defaults:

```ruby
product = Product.void(name: "Custom")
product.name # => "Custom" (overrides default "Unknown Product")

default_product = Product.void
default_product.name # => "Unknown Product" (uses default)
```

Void objects support the same features as Null objects:
- Callable defaults (lambdas/procs)
- Custom methods via block syntax
- Association handling
- All ActiveRecord query methods (`null?`, `persisted?`, etc.)

Use `Null` when you need a single shared null object instance. Use `Void` when you need multiple null object instances with different attribute values.

## Development

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.
Expand Down
106 changes: 106 additions & 0 deletions lib/activerecord/null.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,112 @@ def initialize_attribute_methods
inherit.define_singleton_method(:null) { null_class.instance }
end

# Define a Void class for the given class.
# Unlike Null, Void objects are not singletons and can be instantiated
# multiple times with different attribute values.
#
# @example
# class Product < ApplicationRecord
# Void do
# def display_name = "Product: #{name}"
# end
# end
#
# product1 = Product.void(name: "Widget")
# product2 = Product.void(name: "Gadget")
#
# @param inherit [Class] The class from which the Void object inherits attributes
# @param assignments [Hash] The default attributes to assign to void objects
def Void(inherit = self, assignments = {}, &)
if inherit.is_a?(Hash)
assignments = inherit
inherit = self
end

void_class = Class.new do
include ::ActiveRecord::Null::Mimic

mimics inherit

# Store default assignments for merging with instance attributes
@_void_assignments = assignments

class << self
attr_reader :_void_assignments

def method_missing(method, ...)
mimic_model_class.respond_to?(method) ? mimic_model_class.send(method, ...) : super
end

def respond_to_missing?(method, include_private = false)
mimic_model_class.respond_to?(method, include_private) || super
end
end

# Initialize a new void instance with optional attribute overrides
def initialize(attributes = {})
@_instance_attributes = attributes
initialize_attribute_methods
end

private

def initialize_attribute_methods
# Only initialize if table exists
return unless self.class.mimic_model_class.table_exists?

# Get default assignments from class
void_assignments = self.class._void_assignments

# Define custom assignment methods with instance overrides
if void_assignments.any?
void_assignments.each do |attributes, default_value|
attributes.each do |attr|
attr_sym = attr.to_sym
next if respond_to?(attr_sym) # Skip if already defined

define_singleton_method(attr_sym) do
if @_instance_attributes.key?(attr_sym)
@_instance_attributes[attr_sym]
elsif default_value.is_a?(Proc)
instance_exec(&default_value)
else
default_value
end
end
end
end
end

# Define database attributes
nil_assignments = self.class.mimic_model_class.attribute_names

# Remove custom assignments from database attributes
if void_assignments.any?
void_assignments.each do |attributes, _|
nil_assignments -= attributes.map(&:to_s)
end
end

# Define remaining database attributes with instance override support
nil_assignments.each do |attr|
attr_sym = attr.to_sym
next if respond_to?(attr_sym) # Skip if already defined

define_singleton_method(attr_sym) do
@_instance_attributes.key?(attr_sym) ? @_instance_attributes[attr_sym] : nil
end
end
end
end

void_class.class_eval(&) if block_given?

inherit.const_set(:Void, void_class)

inherit.define_singleton_method(:void) { |attributes = {}| void_class.new(attributes) }
end

def self.extended(base)
base.define_method(:null?) { false }
end
Expand Down
Loading