Class: ActiveValue::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/active_value/base.rb

Overview

ActiveValue::Base is base class for immutable value object that has interfaces like ActiveRecord. In a class inherited this class, constant variables get the behavior like records of ActiveRecord.

Usage.

1. Define the target class inherited from ActiveValue::Base
2. List attributes of the object using attr_accessor
3. Declare constant variables as this class

Example.

class QuestionType < ActiveValue::Base
  attr_accessor :id, :symbol, :name
  CHECKBOX  = new id: 1, symbol: :checkbox, name: "Check Box"
  RADIO     = new id: 2, symbol: :radio,    name: "Radio Button"
  SELECTBOX = new id: 3, symbol: :select,   name: "Select Box"
  TEXT      = new id: 4, symbol: :text,     name: "Text Box"
  TEXTAREA  = new id: 5, symbol: :textarea, name: "Text Area"
end
QuestionType.find(1)
=> QuestionType::CHECKBOX
QuestionType.find(1).name
=> "Check Box"
QuestionType.find(1).checkbox?
=> true
QuestionType.pluck(:id, :name)
=> [[1, "Check Box"], [2, "Radio Button"], [3, "Select Box"], ...]

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attributes = {}) ⇒ Base

If self instance is passed as an argument, create a new instance that has copied attributes. (it’s like copy constructor by shallow copy) Hash instance is passed, the hash attributes apply a new instance.



96
97
98
99
100
101
# File 'lib/active_value/base.rb', line 96

def initialize(attributes = {})
  case attributes
  when self.class then self.class.accessors.map(&:to_s).each { |attribute| public_send(attribute + '=', attributes.public_send(attribute)) }
  when Hash       then attributes.stringify_keys.each { |key, value| public_send(key + '=', value) if respond_to?(key + '=') }
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

If objects have symbol attributes, the objects can be checked equivalence by the method named symbol + ‘?`



104
105
106
107
108
109
110
111
# File 'lib/active_value/base.rb', line 104

def method_missing(method, *args, &block)
  object = self.class.unsorted_all.find { |object| object.respond_to?(:symbol) && method.to_s == object.public_send(:symbol).to_s + '?' }
  if object.nil?
    super
  else
    self == object
  end
end

Class Method Details

.accessorsObject

Get accessors the overrided method saved.



80
81
82
83
84
85
# File 'lib/active_value/base.rb', line 80

def self.accessors
  readers = instance_methods.reject { |attr| attr.to_s[-1] == '=' }
  writers = instance_methods.select { |attr| attr.to_s[-1] == '=' }.map { |attr| attr.to_s.chop.to_sym }
  accessors = readers & writers - [:!]
  Array(@accessors) | accessors.reverse!
end

.allObject

Get all constant instances. (Sorted by the first defined accessor)



57
58
59
# File 'lib/active_value/base.rb', line 57

def self.all
  unsorted_all.sort
end

.attr_accessor(*several_variants) ⇒ Object

Wrap default attr_accessor method in order to save accessor defined order.



74
75
76
77
# File 'lib/active_value/base.rb', line 74

def self.attr_accessor(*several_variants)
  @accessors = *several_variants
  super
end

.define_question_methods(attr_name = :symbol) ⇒ Object

Automatically these methods are defined in this version. This method is remained only for compatibility.



67
68
69
70
71
# File 'lib/active_value/base.rb', line 67

def self.define_question_methods(attr_name = :symbol)
  unsorted_all.each do |object|
    define_method(object.public_send(attr_name).to_s + '?') { self == object } if object.respond_to?(attr_name)
  end
end

.find(index) ⇒ Object

Getter interface by the id element like ‘find` method in ActiveRecord.



39
40
41
42
# File 'lib/active_value/base.rb', line 39

def self.find(index)
  object = all.bsearch { |object| object.public_send(:id) >= index }
  object&.id == index ? object : nil
end

.find_by(conditions) ⇒ Object

Getter interface by the argument element like ‘find_by` method in ActiveRecord.



45
46
47
48
49
# File 'lib/active_value/base.rb', line 45

def self.find_by(conditions)
  unsorted_all.find do |object|
    conditions.all? { |key, value| object.public_send(key) == value }
  end
end

.method_missing(method, *args, &block) ⇒ Object

Delegate undefined method calls to ‘all` method (returns Array).



34
35
36
# File 'lib/active_value/base.rb', line 34

def self.method_missing(method, *args, &block)
  all.public_send(method, *args, &block)
end

.patched_dup(object) ⇒ Object

Wrapper dup method for can’t dup on Fixnum#dup (NilClass etc.) before Ruby 2.4



88
89
90
91
92
# File 'lib/active_value/base.rb', line 88

def self.patched_dup(object)
  object.dup
rescue TypeError
  object
end

.pluck(*accessors) ⇒ Object

Get attributes



62
63
64
# File 'lib/active_value/base.rb', line 62

def self.pluck(*accessors)
  map { |record| Array(accessors).map { |accessor| record.public_send(accessor) } }.map { |array| array.size == 1 ? array.first : array }
end

.unsorted_allObject

Get all constant instances. (Unsorted)



52
53
54
# File 'lib/active_value/base.rb', line 52

def self.unsorted_all
  constants.collect { |name| const_get(name) }.select { |object| object.instance_of?(self) }
end

Instance Method Details

#<=>(another) ⇒ Object

Define the spaceship operator. Compare self with another by the first defined accessor. (In many cases, it’s ‘id` implicitly)



153
154
155
156
# File 'lib/active_value/base.rb', line 153

def <=>(another)
  attr = self.class.accessors.first || :object_id
  public_send(attr) <=> another.public_send(attr) if respond_to?(attr) && another.respond_to?(attr)
end

#==(another) ⇒ Object Also known as: eql?

Define the equal operator. ‘A == B` expression means every attribute has same value. (NOT object_id comparison)



142
143
144
# File 'lib/active_value/base.rb', line 142

def ==(another)
  self.class.accessors.all? { |attr| respond_to?(attr) && another.respond_to?(attr) && public_send(attr) == another.public_send(attr) }
end

#hashObject

Hash method for equivalence comparison with ‘eql?` method. (Referenced by `Enumerable#uniq` method etc.)



148
149
150
# File 'lib/active_value/base.rb', line 148

def hash
  to_json.hash
end

#inspectObject



136
137
138
139
# File 'lib/active_value/base.rb', line 136

def inspect
  hash = to_shallow_hash
  Hash === hash ? '#<' << self.class.name.split('::').last << ' ' << hash.map { |key, value| key.to_s << ': ' << value.inspect }.join(', ') << '>' : hash.inspect
end

#to_deep_hashObject Also known as: to_h

Convert to hash with deep copy. If values include collections(Hash, Array, etc.), search and convert into collections recursively.



119
120
121
122
123
124
125
126
127
128
129
# File 'lib/active_value/base.rb', line 119

def to_deep_hash
  scan = ->(value) do
    case value
    when Hash  then value.each_with_object({}) { |(k, v), h| h[k] = scan.call(v) }
    when Array then value.map { |v| scan.call(v) }
    when Base  then scan.call(value.to_shallow_hash)
    else self.class.patched_dup(value)
    end
  end
  self.class.accessors.each_with_object({}) { |key, hash| hash[key] = scan.call(public_send(key)) }
end

#to_jsonObject



132
133
134
# File 'lib/active_value/base.rb', line 132

def to_json
  JSON.generate(to_h)
end

#to_shallow_hashObject

Convert to hash with shallow copy. If values include collections(Hash, Array, etc.), search and convert without elements of the collection.



114
115
116
# File 'lib/active_value/base.rb', line 114

def to_shallow_hash
  self.class.accessors.each_with_object({}) { |key, hash| hash[key] = self.class.patched_dup(public_send(key)) }
end