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

map_with_promotion(name, attribute_name, yield_key: true, enum_with: :each) click to toggle source

@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
map_without_promotion(name, attribute_name, yield_key: true, enum_with: :each) click to toggle source

@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
nomap_with_promotion(name, attribute_name, enum_with: :each) click to toggle source

@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
nomap_without_promotion(name, attribute_name, enum_with: :each) click to toggle source

@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

define_single_value_with_promotion(method_name, promotion_method_name, ivar) click to toggle source

@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
define_single_value_without_promotion(method_name, ivar) click to toggle source

@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
included(mod) click to toggle source
# File lib/metaruby/attributes.rb, line 39
def included(mod)
    mod.extend Attributes
end
inherited_single_value_attribute(name, &default_value) click to toggle source

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