Module: Jinx::Mergeable

Included in:
Resource
Defined in:
lib/jinx/resource/mergeable.rb

Overview

A Mergeable supports merging Resource attribute values.

Instance Method Summary collapse

Instance Method Details

#merge(*args, &filter) ⇒ Object Also known as: merge!



50
51
52
# File 'lib/jinx/resource/mergeable.rb', line 50

def merge(*args, &filter)
  merge_attributes(*args, &filter)
end

#merge_attribute(attribute, newval, matches = nil) {|value| ... } ⇒ Object

Merges the value newval into the attribute as follows:

  • If the value is nil, empty or equal to the current attribute value, then no merge is performed.

  • Otherwise, if a merger block is given to this method, then that block is called to perform the merge.

  • Otherwise, if the attribute is a non-domain attribute and the current value is non-nil, then no merge is performed.

  • Otherwise, if the attribute is a non-domain attribute and the current value is nil, then set the attribute to the newval.

  • Otherwise, if the attribute is a domain non-collection attribute, then newval is recursively merged into the current referenced domain object.

  • Otherwise, attribute is a domain collection attribute and matching newval members are merged into the corresponding current collection members and non-matching newval members are added to the current collection.

Parameters:

  • attribute (Symbol)

    the merge attribute

  • newval

    the value to merge

  • the ({Resource => Resource}, nil)

    optional merge source => target reference matches

Yields:

  • (value)

    the optional filter block

Yield Parameters:

  • value

    the source merge attribute value

Returns:

  • the merged attribute value



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
# File 'lib/jinx/resource/mergeable.rb', line 77

def merge_attribute(attribute, newval, matches=nil)
  # the previous value
  oldval = send(attribute)
  # Filter the newval into the srcval.
  srcval = if newval and block_given? then
    if newval.collection? then
      newval.select { |v| yield(v) }
    elsif yield(newval) then
      newval
    end
  else
    newval
  end
  
  # If there is no point in merging, then bail. 
  return oldval if srcval.nil_or_empty? or mergeable__equal?(oldval, newval)
  
  # Discriminate between a domain and non-domain attribute.
  prop = self.class.property(attribute)
  if prop.domain? then
    merge_domain_property_value(prop, oldval, srcval, matches)
  else
    merge_nondomain_property_value(prop, oldval, srcval)
  end
end

#merge_attributes(other, attributes = nil, matches = nil) {|value| ... } ⇒ Mergeable

Merges the values of the other attributes into this object and returns self. The other argument can be either a Hash or an object whose class responds to the mergeable_attributes method. The optional attributes argument can be either a single attribute symbol or a collection of attribute symbols.

A hash argument consists of attribute name => value associations. For example, given a Mergeable person object with attributes ssn and children, the call:

person.merge_attributes(:ssn => '555-55-5555', :children => children)

is equivalent to:

person.ssn ||= '555-55-5555'
person.children ||= []
person.children.merge(children, :deep)

An unrecognized attribute is ignored.

If other is not a Hash, then the other object’s attributes values are merged into this object. The default attributes is this mergeable’s class Propertied#mergeable_attributes.

The merge is performed by calling #merge_attribute on each attribute with the matches and filter block given to this method.

Parameters:

Yields:

  • (value)

    the optional filter block

Yield Parameters:

  • value

    the source merge attribute value

Returns:

Raises:

  • (ArgumentError)

    if none of the following are true:

    • other is a Hash

    • attributes is non-nil

    • the other class responds to mergeable_attributes



38
39
40
41
42
43
44
45
46
47
48
# File 'lib/jinx/resource/mergeable.rb', line 38

def merge_attributes(other, attributes=nil, matches=nil, &filter)
  return self if other.nil? or other.equal?(self)
  attributes = [attributes] if Symbol === attributes
  attributes ||= self.class.mergeable_attributes

  # if the source object is not a hash, then convert it to an attribute => value hash
  vh = Hashable === other ? other : other.value_hash(attributes)
  # merge the value hash
  vh.each { |pa, value| merge_attribute(pa, value, matches, &filter) }
  self
end

#merge_domain_property_value(property, oldval, newval, matches) ⇒ Object (private)

See Also:



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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/jinx/resource/mergeable.rb', line 117

def merge_domain_property_value(property, oldval, newval, matches)
  # the dependent owner writer method, if any
  if property.dependent? then
    val = property.collection? ? newval.first : newval
    klass = val.class if val
    inv_prop = self.class.inverse_property(property, klass)
    if inv_prop and not inv_prop.collection? then
      owtr = inv_prop.writer
    end
  end

  # If the attribute is a collection, then merge the matches into the current attribute
  # collection value and add each unmatched source to the collection.
  # Otherwise, if the attribute is not yet set and there is a new value, then set it
  # to the new value match or the new value itself if unmatched.
  if property.collection? then
    # TODO - refactor into method
    if oldval.nil? then
      raise ValidationError.new("Merge into #{qp} #{property} with nil collection value is not supported")
    end
    # the references to add
    adds = []
    logger.debug { "Merging #{newval.qp} into #{qp} #{property} #{oldval.qp}..." } unless newval.nil_or_empty?
    newval.enumerate do |src|
      # If the match target is in the current collection, then update the matched
      # target from the source.
      # Otherwise, if there is no match or the match is a new reference created
      # from the match, then add the match to the oldval collection.
      if matches && matches.has_key?(src) then
        # the source match
        tgt = matches[src]
        if tgt then
          if oldval.include?(tgt) then
            tgt.merge_attributes(src)
          else
            adds << tgt
          end
        end
      else
        adds << src
      end
    end
    # add the unmatched sources
    logger.debug { "Adding #{qp} #{property} unmatched #{adds.qp}..." } unless adds.empty?
    adds.each do |ref|
      # If there is an owner writer attribute, then add the ref to the attribute collection by
      # delegating to the owner writer. Otherwise, add the ref to the attribute collection directly.
      owtr ? delegate_to_inverse_setter(property, ref, owtr) : oldval << ref
    end
    oldval
  elsif newval.nil? then
    # no merge source
    oldval
  elsif oldval then
    # merge the source into the target
    oldval.merge(newval)
  else
    # No target; set the attribute to the source.
    # The target is either a source match or the source itself.
    ref = (matches[newval] if matches) || newval
    logger.debug { "Setting #{qp} #{property} reference #{ref.qp}..." }
    # If the target is a dependent, then set the dependent owner, which will in turn
    # set the attribute to the dependent. Otherwise, set the attribute to the target.
    owtr ? delegate_to_inverse_setter(property, ref, owtr) : set_typed_property_value(property, ref)
  end
  newval
end

#merge_nondomain_property_value(property, oldval, newval) ⇒ Object (private)

See Also:



106
107
108
109
110
111
112
113
114
# File 'lib/jinx/resource/mergeable.rb', line 106

def merge_nondomain_property_value(property, oldval, newval)
  if oldval.nil? then
    set_typed_property_value(property, newval)
  elsif property.collection? then
    oldval.merge(newval)
  else
    oldval
  end
end

#mergeable__equal?(v1, v2) ⇒ Boolean (private)

element-wise comparator. Work around this rare aberration by converting the TreeSet to a Ruby Set.

Returns:

Technology idiosyncracy:

  • Java

    Java TreeSet comparison uses the TreeSet comparator rather than an



188
189
190
# File 'lib/jinx/resource/mergeable.rb', line 188

def mergeable__equal?(v1, v2)
  Java::JavaUtil::TreeSet === v1 && Java::JavaUtil::TreeSet === v2 ? v1.to_set == v2.to_set : v1 == v2
end