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
-
#add_inverse_updater(attribute) ⇒ Object
private
Modifies the given attribute writer method to update the given inverse.
-
#clear_inverse(property) ⇒ Object
protected
Clears the property inverse, if there is one.
-
#delegate_writer_to_inverse(attribute, inverse) ⇒ Object
protected
Redefines the attribute writer method to delegate to its inverse writer.
-
#detect_inverse_attribute(klass) ⇒ Symbol?
protected
Detects an unambiguous attribute which refers to the given referencing class.
-
#detect_inverse_attribute_from_candidates(klass, candidates) ⇒ Symbol?
private
The inverse attribute for the given referencing class and inverse, or nil if no owner attribute was detected.
-
#infer_property_inverse(property) ⇒ Symbol?
protected
Infers the inverse of the given property declared by this class.
-
#inverse_property(prop, klass = nil) ⇒ Property?
Returns the inverse of the given attribute.
-
#restrict_attribute_inverse(prop, inverse) ⇒ Property
private
Copies the given attribute metadata from its declarer to this class.
-
#set_attribute_inverse(attribute, inverse) ⇒ Object
protected
Sets the given bi-directional association attribute’s inverse.
Instance Method Details
#add_inverse_updater(attribute) ⇒ Object (private)
Modifies the given attribute writer method to update the given 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
set_account(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.
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.
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.
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.
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.
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.
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 |