module MetaRuby::Attributes
Basic functionality for attributes that are aware of inheritance
There's basically two main interfaces: {.inherited_single_value_attribute} and {.inherited_attribute}.
The former will return the value as set at the lowest level in the model hierarchy (i.e. on self, then on supermodel if self is unset and so forth).
The latter enumerates a collection (e.g. Array, Set or Hash). If a map is used (such as a Hash), additional functionality gives different choices as to how the key-value mapping is handled w.r.t. model hierarchy.
In both cases, a value returned by a submodel is optionally passed through a promotion method (called promote_#attributename) which allows to update the returned value (as e.g. update a possible back-reference of the enumerated value to its containing model from the original model to the receiver).
@example promotion to update back references
class Port; attr_accessor :component_model end class Component # Update #component_model to make it point to the receiver instead # of the original model. Note that metaruby does not memoize the # result, so it has to be done in the promote method if it is # desired def promote_port(port) port = port.dup port.component_model = self port end inherited_attribute(:ports, :port, map: true) { Hash.new } end
Constants
- ANCESTORS_ACCESS
Public Class Methods
@api private
Helper class that defines the iteration method for inherited_attribute when :map is set and there is a promotion method
# File lib/metaruby/attributes.rb, line 410 def self.map_with_promotion(name, attribute_name, yield_key: true, enum_with: :each) code, file, line =<<-EOF, __FILE__, __LINE__+1 def each_#{name}(key = nil, uniq = true) if !block_given? return enum_for(:each_#{name}, key, uniq) end #{ANCESTORS_ACCESS} if key promotions = [] for klass in ancestors if attr = klass.instance_variable_get(:@#{attribute_name}) if attr.has_key?(key) value = attr[key] for p in promotions value = p.promote_#{name}(key, value) end yield(value) return self if uniq end end promotions.unshift(klass) if klass.respond_to?(:promote_#{name}) end elsif !uniq promotions = [] for klass in ancestors if attr = klass.instance_variable_get(:@#{attribute_name}) attr.#{enum_with} do |k, v| for p in promotions v = p.promote_#{name}(k, v) end #{if yield_key then 'yield(k, v)' else 'yield(v)' end} end end promotions.unshift(klass) if klass.respond_to?(:promote_#{name}) end else seen = Set.new promotions = [] for klass in ancestors if attr = klass.instance_variable_get(:@#{attribute_name}) attr.#{enum_with} do |k, v| unless seen.include?(k) for p in promotions v = p.promote_#{name}(k, v) end seen << k #{if yield_key then 'yield(k, v)' else 'yield(v)' end} end end end promotions.unshift(klass) if klass.respond_to?(:promote_#{name}) end end self end EOF return code, file, line end
@api private
Helper class that defines the iteration method for inherited_attribute when :map is set and there is not promotion method
# File lib/metaruby/attributes.rb, line 340 def self.map_without_promotion(name, attribute_name, yield_key: true, enum_with: :each) code, file, line =<<-EOF, __FILE__, __LINE__+1 def each_#{name}(key = nil, uniq = true) if !block_given? return enum_for(:each_#{name}, key, uniq) end #{ANCESTORS_ACCESS} if key for klass in ancestors if attr = klass.instance_variable_get(:@#{attribute_name}) if attr.has_key?(key) yield(attr[key]) return self if uniq end end end elsif !uniq for klass in ancestors if attr = klass.instance_variable_get(:@#{attribute_name}) attr.#{enum_with} do |el| yield(el) end end end else seen = Set.new for klass in ancestors if attr = klass.instance_variable_get(:@#{attribute_name}) attr.#{enum_with} do |el_key, el| if !seen.include?(el_key) seen << el_key #{if yield_key then 'yield(el_key, el)' else 'yield(el)' end} end end end end end self end EOF return code, file, line end
@api private
Helper class that defines the iteration method for inherited_attribute when :map is not set and there is a promotion method
# File lib/metaruby/attributes.rb, line 474 def self.nomap_with_promotion(name, attribute_name, enum_with: :each) code, file, line =<<-EOF, __FILE__, __LINE__+1 def each_#{name} if !block_given? return enum_for(:each_#{name}) end #{ANCESTORS_ACCESS} promotions = [] for klass in ancestors if attr = klass.instance_variable_get(:@#{attribute_name}) attr.#{enum_with} do |value| for p in promotions value = p.promote_#{name}(value) end yield(value) end end promotions.unshift(klass) if klass.respond_to?(:promote_#{name}) end self end EOF return code, file, line end
@api private
Helper class that defines the iteration method for inherited_attribute when :map is not set and there is no promotion method
# File lib/metaruby/attributes.rb, line 389 def self.nomap_without_promotion(name, attribute_name, enum_with: :each) code, file, line =<<-EOF, __FILE__, __LINE__+1 def each_#{name} return enum_for(__method__) if !block_given? #{ANCESTORS_ACCESS} for klass in ancestors if attr = klass.instance_variable_get(:@#{attribute_name}) attr.#{enum_with} { |el| yield(el) } end end self end EOF return code, file, line end
Public Instance Methods
@api private
Helper method for {#inherited_single_value_attribute} in case there is a promotion method defined
# File lib/metaruby/attributes.rb, line 130 def define_single_value_with_promotion(method_name, promotion_method_name, ivar) class_eval <<-EOF, __FILE__, __LINE__+1 def #{method_name} #{ANCESTORS_ACCESS} promotions = [] for klass in ancestors if klass.instance_variable_defined?(:#{ivar}) has_value = true value = klass.instance_variable_get(:#{ivar}) break end promotions.unshift(klass) if klass.respond_to?(:#{promotion_method_name}) end if !has_value && respond_to?(:#{method_name}_default) # Look for default has_value = true value = send(:#{method_name}_default).call base = nil promotions.clear for klass in ancestors if !klass.respond_to?(:#{method_name}_default) break end base = klass promotions.unshift(klass) if klass.respond_to?(:#{promotion_method_name}) end promotions.shift base.instance_variable_set :#{ivar}, value end if has_value promotions.inject(value) { |v, k| k.#{promotion_method_name}(v) } end end EOF end
@api private
Helper method for {#inherited_single_value_attribute} in case there are no promotion method(s) defined
# File lib/metaruby/attributes.rb, line 94 def define_single_value_without_promotion(method_name, ivar) class_eval <<-EOF, __FILE__, __LINE__+1 def #{method_name} #{ANCESTORS_ACCESS} has_value = false for klass in ancestors if klass.instance_variable_defined?(:#{ivar}) has_value = true value = klass.instance_variable_get(:#{ivar}) break end end if !has_value && respond_to?(:#{method_name}_default) # Look for default has_value = true value = send(:#{method_name}_default).call base = nil for klass in ancestors if !klass.respond_to?(:#{method_name}_default) break end base = klass end base.instance_variable_set :#{ivar}, value end value end EOF end
# File lib/metaruby/attributes.rb, line 39 def included(mod) mod.extend Attributes end
Defines an attribute that holds at most a single value
The value returned by the defined accessor will be the one set at the lowest level in the model hierarchy (i.e. self, then superclass, …)
@param [String] name the attribute name @return [InheritedAttribute] the attribute definition @raise [ArgumentError] if no attribute with that name exists
# File lib/metaruby/attributes.rb, line 51 def inherited_single_value_attribute(name, &default_value) dsl_attribute_name = "__dsl_attribute__#{name}" ivar = "@#{dsl_attribute_name}" dsl_attribute(dsl_attribute_name) if default_value define_method("#{dsl_attribute_name}_get_default") { default_value } end promotion_method = "promote_#{name}" if method_defined?(promotion_method) define_single_value_with_promotion("#{dsl_attribute_name}_get", promotion_method, ivar) else define_single_value_without_promotion("#{dsl_attribute_name}_get", ivar) end define_method(name) do |*args| if args.empty? # Getter call send("#{dsl_attribute_name}_get") else # Setter call, delegate to the dsl_attribute implementation send(dsl_attribute_name, *args) end end nil end