Module: Jinx::Inverse

Included in:
Metadata
Defined in:
lib/jinx/metadata/inverse.rb

Overview

Meta-data mix-in to infer and set inverse attributes.

Instance Method Summary collapse

Instance Method Details

#add_inverse_updater(attribute) ⇒ Object (private)

Modifies the given attribute writer method to update the given inverse.

Parameters:

  • attribute (Symbol)

    the subject attribute

  • the (Symbol)

    attribute inverse



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/jinx/metadata/inverse.rb', line 168

def add_inverse_updater(attribute)
  prop = property(attribute)
  # the reader and writer methods
  rdr, wtr = prop.accessors
  # the inverse attribute metadata
  inv_prop = prop.inverse_property
  # the inverse attribute reader and writer
  inv_rdr, inv_wtr = inv_accessors = inv_prop.accessors
  # Redefine the writer method to update the inverse by delegating to the inverse.
  redefine_method(wtr) do |old_wtr|
    # the attribute reader and (superseded) writer
    accessors = [rdr, old_wtr]
    if inv_prop.collection? then
      lambda { |other| add_to_inverse_collection(other, accessors, inv_rdr) }
    else
      lambda { |other| set_inversible_noncollection_attribute(other, accessors, inv_wtr) }
    end
  end
  logger.debug { "Injected inverse #{inv_prop} updater into #{qp}.#{attribute} writer method #{wtr}." }
end

#clear_inverse(property) ⇒ Object (protected)

Clears the property inverse, if there is one.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/jinx/metadata/inverse.rb', line 78

def clear_inverse(property)
  # the inverse property
  ip = property.inverse_property || return
  # If the property is a collection and the inverse is not, then delegate to
  # the inverse.
  if property.collection? then
    return ip.declarer.clear_inverse(ip) unless ip.collection?
  else
    # Restore the property reader and writer to the Java reader and writer, resp.
    alias_property_accessors(property)
  end
  # Unset the inverse.
  property.inverse = nil
end

#delegate_writer_to_inverse(attribute, inverse) ⇒ Object (protected)

Redefines the attribute writer method to delegate to its inverse writer. This is done to enforce inverse integrity.

For a Person attribute account with inverse holder, this is equivalent to the following:

class Person
  alias :set_account :account=
  def account=(acct)
    acct.holder = self if acct
    (acct)
  end
end


124
125
126
127
128
129
130
131
132
133
134
# File 'lib/jinx/metadata/inverse.rb', line 124

def delegate_writer_to_inverse(attribute, inverse)
  prop = property(attribute)
  # nothing to do if no inverse
  inv_prop = prop.inverse_property || return
  logger.debug { "Delegating #{qp}.#{attribute} update to the inverse #{prop.type.qp}.#{inv_prop}..." }
  # redefine the write to set the dependent inverse
  redefine_method(prop.writer) do |old_writer|
    # delegate to the Jinx::Resource set_inverse method
    lambda { |dep| set_inverse(dep, old_writer, inv_prop.writer) }
  end
end

#detect_inverse_attribute(klass) ⇒ Symbol? (protected)

Detects an unambiguous attribute which refers to the given referencing class. If there is exactly one attribute with the given return type, then that attribute is chosen. Otherwise, the attribute whose name matches the underscored referencing class name is chosen, if any.

Parameters:

  • klass (Class)

    the referencing class

Returns:

  • (Symbol, nil)

    the inverse attribute for the given referencing class and inverse, or nil if no owner attribute was detected



101
102
103
104
105
106
107
108
109
110
111
# File 'lib/jinx/metadata/inverse.rb', line 101

def detect_inverse_attribute(klass)
  # The candidate attributes return the referencing type and don't already have an inverse.
  candidates = domain_attributes.compose { |prop| klass <= prop.type and prop.inverse.nil? }
  pa = detect_inverse_attribute_from_candidates(klass, candidates)
  if pa then
    logger.debug { "#{qp} #{klass.qp} inverse attribute is #{pa}." }
  else
    logger.debug { "#{qp} #{klass.qp} inverse attribute was not detected." }
  end
  pa
end

#detect_inverse_attribute_from_candidates(klass, candidates) ⇒ Symbol? (private)

Returns the inverse attribute for the given referencing class and inverse, or nil if no owner attribute was detected.

Parameters:

  • candidates (<Symbol>)

    the attributes constrained to the target type

  • klass (Class)

    the referencing class

Returns:

  • (Symbol, nil)

    the inverse attribute for the given referencing class and inverse, or nil if no owner attribute was detected



155
156
157
158
159
160
161
162
163
# File 'lib/jinx/metadata/inverse.rb', line 155

def detect_inverse_attribute_from_candidates(klass, candidates)
  return if candidates.empty?
  # There can be at most one owner attribute per owner.
  return candidates.first.to_sym if candidates.size == 1
  # By convention, if more than one attribute references the owner type,
  # then the attribute named after the owner type is the owner attribute.
  tgt = klass.name.demodulize.underscore.to_sym
  tgt if candidates.detect { |pa| pa == tgt }
end

#infer_property_inverse(property) ⇒ Symbol? (protected)

Infers the inverse of the given property declared by this class. A domain attribute is recognized as an inverse according to the #detect_inverse_attribute criterion.

Parameters:

  • property (Property)

    the property to check

Returns:

  • (Symbol, nil)

    the inverse attribute, or nil if none was detected



29
30
31
32
33
# File 'lib/jinx/metadata/inverse.rb', line 29

def infer_property_inverse(property)
  inv = property.type.detect_inverse_attribute(self)
  set_attribute_inverse(property.attribute, inv) if inv
  inv
end

#inverse_property(prop, klass = nil) ⇒ Property?

Returns the inverse of the given attribute. If the attribute has an #Property#inverse_property, then that attribute’s inverse is returned. Otherwise, if the attribute is an #PropertyCharacteristics#owner?, then the target class dependent attribute which matches this type is returned, if it exists.

Parameters:

  • prop (Property)

    the subject attribute

  • klass (Class, nil) (defaults to: nil)

    the target class

Returns:

  • (Property, nil)

    the inverse attribute, if any



11
12
13
14
15
16
17
18
# File 'lib/jinx/metadata/inverse.rb', line 11

def inverse_property(prop, klass=nil)
  inv_prop = prop.inverse_property
  return inv_prop if inv_prop
  if prop.dependent? and klass then
    klass.owner_property_hash.each { |otype, op|
    return op if self <= otype }
  end
end

#restrict_attribute_inverse(prop, inverse) ⇒ Property (private)

Copies the given attribute metadata from its declarer to this class. The new attribute metadata has the same attribute access methods, but the declarer is this class and the inverse is the given inverse attribute.

Parameters:

  • prop (Property)

    the attribute to copy

  • the (Symbol)

    attribute inverse

Returns:

  • (Property)

    the copied attribute metadata



145
146
147
148
149
150
# File 'lib/jinx/metadata/inverse.rb', line 145

def restrict_attribute_inverse(prop, inverse)
  logger.debug { "Restricting #{prop.declarer.qp}.#{prop} to #{qp} with inverse #{inverse}..." }
  rst_prop = prop.restrict(self, :inverse => inverse)
  logger.debug { "Restricted #{prop.declarer.qp}.#{prop} to #{qp} with inverse #{inverse}." }
  rst_prop
end

#set_attribute_inverse(attribute, inverse) ⇒ Object (protected)

Sets the given bi-directional association attribute’s inverse.

Parameters:

  • attribute (Symbol)

    the subject attribute

  • the (Symbol)

    attribute inverse

Raises:

  • (TypeError)

    if the inverse type is incompatible with this Resource



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
# File 'lib/jinx/metadata/inverse.rb', line 40

def set_attribute_inverse(attribute, inverse)
  prop = property(attribute)
  # the standard attribute
  pa = prop.attribute
  # return if inverse is already set
  return if prop.inverse == inverse
  # the default inverse
  inverse ||= prop.type.detect_inverse_attribute(self)
  # If the attribute is not declared by this class, then make a new attribute
  # metadata specialized for this class.
  unless prop.declarer == self then
    prop = restrict_attribute_inverse(prop, inverse)
  end
  logger.debug { "Setting #{qp}.#{pa} inverse to #{inverse}..." }
  # the inverse attribute meta-data
  inv_prop = prop.type.property(inverse)
  # If the attribute is the many side of a 1:M relation, then delegate to the one side.
  if prop.collection? and not inv_prop.collection? then
    return prop.type.set_attribute_inverse(inverse, pa)
  end
  # This class must be the same as or a subclass of the inverse attribute type.
  unless self <= inv_prop.type then
    raise TypeError.new("Cannot set #{qp}.#{pa} inverse to #{prop.type.qp}.#{pa} with incompatible type #{inv_prop.type.qp}")
  end
  # Set the inverse in the attribute metadata.
  prop.inverse = inverse
  # If attribute is the one side of a 1:M or non-reflexive 1:1 relation, then add the inverse updater.
  unless prop.collection? then
    # Inject adding to the inverse collection into the attribute writer method. 
    add_inverse_updater(pa)
    unless prop.type == inv_prop.type or inv_prop.collection? then
      prop.type.delegate_writer_to_inverse(inverse, pa)
    end
  end
  logger.debug { "Set #{qp}.#{pa} inverse to #{inverse}." }
end