1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
module Virtus
# Include this Module for Value Object semantics
#
# The idea is that instances should be immutable and compared based on state
# (rather than identity, as is typically the case)
#
# @example
# class GeoLocation
# include Virtus::ValueObject
# attribute :latitude, Float
# attribute :longitude, Float
# end
#
# location = GeoLocation.new(:latitude => 10, :longitude => 100)
# same_location = GeoLocation.new(:latitude => 10, :longitude => 100)
# location == same_location #=> true
# hash = { location => :foo }
# hash[same_location] #=> :foo
module ValueObject
# Callback to configure including Class as a Value Object
#
# Including Class will include Virtus and have additional
# value object semantics defined in this module
#
# @return [Undefined]
#
# TODO: stacking modules is getting painful
# time for Facets' module_inheritance, ActiveSupport::Concern or the like
#
# @api private
def self.included(base)
Virtus.warn "Virtus::ValueObject is deprecated and will be removed in 1.0.0 #{caller.first}"
base.instance_eval do
include Virtus
include InstanceMethods
extend ClassMethods
extend AllowedWriterMethods
private :attributes=
end
end
private_class_method :included
module InstanceMethods
# ValueObjects are immutable and can't be cloned
#
# They always represent the same value
#
# @example
#
# value_object.clone === value_object # => true
#
# @return [self]
#
# @api public
def clone
self
end
alias dup clone
# Create a new ValueObject by combining the passed attribute hash with
# the instances attributes.
#
# @example
#
# number = PhoneNumber.new(kind: "mobile", number: "123-456-78-90")
# number.with(number: "987-654-32-10")
# # => #<PhoneNumber kind="mobile" number="987-654-32-10">
#
# @return [Object]
#
# @api public
def with(attribute_updates)
self.class.new(attribute_set.get(self).merge(attribute_updates))
end
end
module AllowedWriterMethods
# The list of writer methods that can be mass-assigned to in #attributes=
#
# @return [Set]
#
# @api private
def allowed_writer_methods
@allowed_writer_methods ||=
begin
allowed_writer_methods = super
allowed_writer_methods += attribute_set.map{|attr| "#{attr.name}="}
allowed_writer_methods.to_set.freeze
end
end
end
module ClassMethods
# Define an attribute on the receiver
#
# The Attribute will have private writer methods (eg., immutable instances)
# and be used in equality/equivalence comparisons
#
# @example
# class GeoLocation
# include Virtus::ValueObject
#
# attribute :latitude, Float
# attribute :longitude, Float
# end
#
# @see Virtus::ClassMethods.attribute
#
# @return [self]
#
# @api public
def attribute(name, type, options = {})
equalizer << name
super name, type, options.merge(:writer => :private)
end
# Define and include a module that provides Value Object semantics
#
# Included module will have #inspect, #eql?, #== and #hash
# methods whose definition is based on the _keys_ argument
#
# @example
# virtus_class.equalizer
#
# @return [Equalizer]
# An Equalizer module which defines #inspect, #eql?, #== and #hash
# for instances of this class
#
# @api public
def equalizer
@equalizer ||=
begin
equalizer = Virtus::Equalizer.new(name || inspect)
include equalizer
equalizer
end
end
end # module ClassMethods
end # module ValueObject
end # module Virtus
|