class Syskit::DependencyInjection

Representation and manipulation of dependency injection selection

Constants

IGNORED_MODELS
ROOT_MODELS

Attributes

defaults[R]
explicit[R]
resolved[R]

Public Class Methods

do_not_inherit() click to toggle source
# File lib/syskit/dependency_injection.rb, line 482
def self.do_not_inherit
    @do_not_inherit ||= SpecialDIValue.new('do_not_inherit')
end
find_name_resolution(name, mappings) click to toggle source

Resolves a name into a component object

@param [String] name the name to be resolved. It can be a plain

name, i.e. the name of a component in 'mappings', or a
name.service, i.e. the name of a service for a component in
'mappings'

@param [Hash] mappings (see {DependencyInjection#explicit}) @return [#instanciate,nil] the component model or nil if the name

cannot be resolved

@raise [NameResolutionError] if the name cannot be resolved,

either because the base name does not exist or the specified
service cannot be found in it
# File lib/syskit/dependency_injection.rb, line 367
def self.find_name_resolution(name, mappings)
    if name =~ /^(\w+)\.(.*)$/
        object_name, service_name = $1, $2
    else
        object_name = name
    end

    main_object = mappings[object_name]
    return if !main_object || main_object.respond_to?(:to_str)

    if service_name
        if !main_object.respond_to?(:find_data_service)
            raise NameResolutionError.new(object_name), "cannot select a service on #{main_object}"
        end
        if srv = main_object.find_data_service(service_name)
            return srv
        else
            raise NameResolutionError.new(object_name), "#{main_object} has no service called #{service_name}"
        end
    else return main_object
    end
end
new(*base) click to toggle source

creates a new DependencyInjection instance

If arguments are provided, they must match the format expected by add

# File lib/syskit/dependency_injection.rb, line 22
def initialize(*base)
    @explicit = Hash.new
    @defaults = Set.new
    if !base.empty?
        add(*base)
    end
end
normalize_selected_object(value, key = nil) click to toggle source
# File lib/syskit/dependency_injection.rb, line 524
def self.normalize_selected_object(value, key = nil)
    if !value
        raise ArgumentError, "found nil as selection for #{key}, but it is not accepted anymore"
    end

    if value.kind_of?(InstanceSelection)
        value = value.component || value.selected
    end

    # 'value' must be one of String,Model<Component>,Component,DataService,Model<BoundDataService>,BoundDataService
    if !value.respond_to?(:to_str) &&
        !value.kind_of?(SpecialDIValue) &&
        !value.kind_of?(Component) &&
        !value.kind_of?(BoundDataService) &&
        !value.kind_of?(Models::BoundDataService) &&
        !value.kind_of?(Models::DataServiceModel) &&
        !value.kind_of?(InstanceRequirements) &&
        (!value.kind_of?(Class) || !(value <= Component))
        if value.respond_to?(:to_instance_requirements)
            value = value.to_instance_requirements
        else
            if key
                raise ArgumentError, "found #{value}(of class #{value.class}) as a selection for #{key}, but only name,component models,components,data service models and bound data services are allowed"
            else
                raise ArgumentError, "found #{value}(of class #{value.class}) as a selection, but only name,component models,components,data service models and bound data services are allowed"
            end
        end
    end
    value
end
normalize_selection(selection) click to toggle source

Normalizes an explicit selection

The input can map any of string, Component and DataService to string, Component, DataService and BoundDataService

A normalized selection has this form:

  • string to String,{Component},{DataService},{BoundDataService},{.do_not_inherit},{.nothing}

  • Component to String,{Component},{.do_not_inherit},{.nothing}

  • DataService to String,{DataService},{BoundDataService},{.do_not_inherit},{.nothing}

@raise ArgumentError if the key and value are not valid

selection (see above)

@raise ArgumentError if the selected component or service does

not fullfill the key

@raise AmbiguousServiceSelection if a component is selected for a

data service, but there are multiple services of that type in
the component
# File lib/syskit/dependency_injection.rb, line 169
def self.normalize_selection(selection)
    normalized = Hash.new
    selection.each do |key, value|
        # 'key' must be one of String, Component or DataService
        if !key.respond_to?(:to_str) &&
            !key.kind_of?(Models::DataServiceModel) &&
            (!key.kind_of?(Class) || !(key <= Component))

            raise ArgumentError, "found #{value} as a selection key, but only names, component models and data service models are allowed"
        end

        # 'value' must be one of String,Model<Component>,Component,DataService,Model<BoundDataService>,BoundDataService or nil
        value = normalize_selected_object(value, key)

        if key.respond_to?(:to_str)
            normalized[key] = value
            next
        end

        if value.respond_to?(:fullfills?)
            if !value.fullfills?(key)
                raise ArgumentError, "found #{value.short_name}(of class #{value.class}) as a selection for #{key.short_name}, but #{value.short_name} does not fullfill #{key.short_name}"
            end
        end

        if key <= Component
            if value.kind_of?(Models::BoundDataService)
                value = value.component_model
            elsif value.kind_of?(Syskit::BoundDataService)
                value = value.component
            end
            normalized[key] = value
        elsif key <= DataService
            if value.respond_to?(:find_data_service_from_type)
                value = value.find_data_service_from_type(key)
            end
            normalized[key] = value
        else
            raise NotImplementedError, "should not have get there, but did"
        end
    end
    normalized
end
nothing() click to toggle source
# File lib/syskit/dependency_injection.rb, line 486
def self.nothing
    @nothing ||= SpecialDIValue.new('nothing')
end
partition_use_arguments(*mappings) click to toggle source

Helper method that separates the default selections from the explicit selections in the call to use

@return [(Hash,Set)] the explicit selections and a list of

default selections
# File lib/syskit/dependency_injection.rb, line 650
def self.partition_use_arguments(*mappings)
    explicit = Hash.new
    defaults = Set.new
    mappings.each do |element|
        if element.kind_of?(Hash)
            explicit.merge!(element)
        else
            defaults << element
        end
    end
    return explicit, defaults
end
resolve_default_selections(using_spec, default_selections) click to toggle source

Helper methods that adds to a dependency inject mapping a list of default selections

Default selections are a list of objects for which no specification is used. They are resolved as “select X for all models of X for which there is no explicit selection already”

# File lib/syskit/dependency_injection.rb, line 561
def self.resolve_default_selections(using_spec, default_selections)
    if !default_selections || default_selections.empty?
        return using_spec
    end

    debug do
        debug "Resolving default selections"
        default_selections.map(&:to_s).sort.each do |sel|
            debug "    #{sel}"
        end
        debug "  into"
        using_spec.map { |k, v| [k.to_s, v.to_s] }.sort.each do |k, v|
            debug "    #{k} => #{v}"
        end
        break
    end

    result = using_spec.dup

    ambiguous_default_selections = Hash.new
    resolved_default_selections = Hash.new

    default_selections.each do |selection|
        selection = resolve_selection_recursively(selection, using_spec)
        selection.each_fullfilled_model do |m|
            next if IGNORED_MODELS.include?(m)
            if selection.respond_to?(:find_all_data_services_from_type) &&
                m.kind_of?(Models::DataServiceModel)
                # Ignore if it is provided multiple times by the
                # selection

                next if selection.find_all_data_services_from_type(m).size != 1
            end

            break if ROOT_MODELS.include?(m)
            if using_spec[m]
                debug do
                    debug "  rejected #{selection.short_name}"
                    debug "    for #{m.short_name}"
                    debug "    reason: already explicitely selected"
                    break
                end
            elsif ambiguous_default_selections.has_key?(m)
                ambiguity = ambiguous_default_selections[m]
                debug do
                    debug "  rejected #{selection.short_name}"
                    debug "    for #{m.short_name}"
                    debug "    reason: ambiguity with"
                    ambiguity.each do |model|
                        debug "      #{model.short_name}"
                    end
                    break
                end
                ambiguity << selection
            elsif resolved_default_selections[m] && resolved_default_selections[m] != selection
                removed = resolved_default_selections.delete(m)
                ambiguous_default_selections[m] = [selection, removed].to_set
                debug do
                    debug "  removing #{removed.short_name}"
                    debug "    for #{m.short_name}"
                    debug "    reason: ambiguity with"
                    debug "      #{selection.short_name}"
                    break
                end
            else
                debug do
                    debug "  adding #{selection.short_name}"
                    debug "    for #{m.short_name}"
                    break
                end
                resolved_default_selections[m] = selection
            end
        end
    end
    debug do
        debug "  selected defaults:"
        resolved_default_selections.each do |key, sel|
            debug "    #{key.respond_to?(:short_name) ? key.short_name : key}: #{sel}"
        end
        break
    end
    result.merge!(resolved_default_selections)
end
resolve_recursive_selection_mapping(spec) click to toggle source

Helper method that resolves recursive selections in a dependency injection mapping

# File lib/syskit/dependency_injection.rb, line 492
def self.resolve_recursive_selection_mapping(spec)
    spec.map_value do |key, value|
        resolve_selection_recursively(value, spec)
    end
end
resolve_selection_recursively(value, spec) click to toggle source

Helper method that resolves one single object recursively

# File lib/syskit/dependency_injection.rb, line 499
def self.resolve_selection_recursively(value, spec)
    while value && !value.respond_to?(:to_str)
        case value
        when Models::BoundDataService
            return value if !value.component_model.kind_of?(Class)
            component_model = value.component_model
            if (selected = spec[component_model]) && !selected.respond_to?(:to_str)
                if selected != component_model
                    new_value = selected.selected_for(value).selected_model
                end
            end
        when Module
            new_value = spec[value]
        else return value
        end
        return value if !new_value
        return value if value == new_value
        value = new_value
    end
    value
end

Public Instance Methods

==(obj) click to toggle source
# File lib/syskit/dependency_injection.rb, line 16
def ==(obj); eql?(obj) end
add(*mappings) click to toggle source

@overload add(default0, default1, key0 => value0) @overload add([default0, default1], key0 => value0) @overload add(dependency_injection)

Add default and explicit selections in one call

# File lib/syskit/dependency_injection.rb, line 91
def add(*mappings)
    explicit, defaults = DependencyInjection.partition_use_arguments(*mappings)
    explicit = DependencyInjection.normalize_selection(explicit)

    filtered_defaults = Set.new
    defaults.each do |obj|
        if obj.kind_of?(DependencyInjection)
            # Do not use merge here. One wants to override the
            # existing selections with the new ones
            explicit = obj.explicit.merge!(explicit)
            filtered_defaults |= obj.defaults
        else
            filtered_defaults << DependencyInjection.normalize_selected_object(obj)
        end
    end

    add_explicit(explicit)
    add_defaults(filtered_defaults)
    self
end
add_defaults(list) click to toggle source

Add a list of objects to the default list.

# File lib/syskit/dependency_injection.rb, line 214
def add_defaults(list)
    # Invalidate the @resolved cached
    @resolved = nil
    list.each { |v| v.freeze if v.kind_of?(InstanceRequirements) }
    @defaults |= list
end
add_explicit(mappings) click to toggle source

Add a new dependency injection pattern to the current set

The new mapping overrides existing mappings

# File lib/syskit/dependency_injection.rb, line 115
def add_explicit(mappings)
    # Invalidate the @resolved cached
    if !defaults.empty?
        @resolved = nil
    end
    mappings.each_value do |v|
        v.freeze if v.kind_of?(InstanceRequirements)
    end

    explicit.merge!(mappings) do |k, v1, v2|
        # There is a pathological case here. If v2 == k, then we
        # should keep the k => v1 mapping (because of recursive
        # resolution). However, this is the one case where it does
        # not work, as the merge! overrides the existing selection.
        #
        # However, when this happens, we can simply ignore the
        # identity selection
        if v2 == k then v1
        else v2
        end
    end

    @explicit = DependencyInjection.resolve_recursive_selection_mapping(explicit)
end
add_mask(mask) click to toggle source
# File lib/syskit/dependency_injection.rb, line 140
def add_mask(mask)
    mask.each do |key|
        explicit[key] = DependencyInjection.do_not_inherit
    end
end
clear() click to toggle source
# File lib/syskit/dependency_injection.rb, line 43
def clear
    explicit.clear
    defaults.clear
end
direct_selection_for(obj) click to toggle source
# File lib/syskit/dependency_injection.rb, line 241
def direct_selection_for(obj)
    if defaults.empty?
        if (sel = self.explicit[obj]) && sel != DependencyInjection.do_not_inherit
            sel
        end
    else
        @resolved ||= resolve
        return @resolved.direct_selection_for(obj)
    end
end
each(&block) click to toggle source

Enumerates the selected objects (not the keys)

# File lib/syskit/dependency_injection.rb, line 465
def each(&block)
    block_given? or return enum_for(__method__)
    explicit.each_value(&block)
    defaults.each(&block)
    self
end
each_selection_key(&block) click to toggle source
# File lib/syskit/dependency_injection.rb, line 472
def each_selection_key(&block)
    explicit.each_key(&block)
end
empty?() click to toggle source

True if this object contains no selection at all

# File lib/syskit/dependency_injection.rb, line 39
def empty?
    @explicit.empty? && @defaults.empty?
end
eql?(obj) click to toggle source
# File lib/syskit/dependency_injection.rb, line 11
def eql?(obj)
    obj.kind_of?(DependencyInjection) &&
        explicit == obj.explicit &&
        defaults == obj.defaults
end
has_selection_for?(name) click to toggle source

True if there is an explicit selection for the given name

# File lib/syskit/dependency_injection.rb, line 147
def has_selection_for?(name)
    !!direct_selection_for(name)
end
hash() click to toggle source
# File lib/syskit/dependency_injection.rb, line 10
def hash; [explicit, defaults].hash end
initialize_copy(from) click to toggle source
Calls superclass method
# File lib/syskit/dependency_injection.rb, line 32
def initialize_copy(from)
    super
    @explicit = from.explicit.dup
    @defaults = from.defaults.dup
end
instance_selection_for(name, requirements) click to toggle source

Returns the non-ambiguous selection for the given name and requirements

@param [String,nil] name the name that should be used for

resolution, or nil if there is no name

@param [InstanceRequirements,nil] the required models, or nil if

none are specified

@return [InstanceSelection] @raise (see selection_for)

# File lib/syskit/dependency_injection.rb, line 230
def instance_selection_for(name, requirements)
    instance, component_model, selected_services, used_keys =
        selection_for(name, requirements)
    selection = InstanceSelection.new(
        instance,
        InstanceRequirements.from_object(component_model, requirements), 
        requirements,
        selected_services)
    return selection, used_keys
end
map(&block) click to toggle source

Create a new DependencyInjection object, with a modified selection

Like map!, this method yields the [selection_key, selected_instance] pairs to a block that must return a new value for the for selected_instance.

# File lib/syskit/dependency_injection.rb, line 435
def map(&block)
    copy = dup
    copy.map!(&block)
end
map!() { |v| ... } click to toggle source

Changes the selections

This method yields the [selection_key, selected_instance] pairs to a block that must return a new value for the for selected_instance. It modifies self

# File lib/syskit/dependency_injection.rb, line 445
def map!(&block)
    # Invalidate the @resolved cached
    @resolved = nil
    changed = false
    explicit = self.explicit.map_value do |k, v|
        result = yield(v)
        changed ||= (result != v)
        result
    end
    if changed
        @explicit = DependencyInjection.resolve_recursive_selection_mapping(explicit)
    end

    @defaults.map! do |v|
        yield(v)
    end
    self
end
merge(other) click to toggle source

Merge the selections in other into self.

If both objects provide selections for the same keys, raises ArgumentError if the two selections are incompatible

# File lib/syskit/dependency_injection.rb, line 667
def merge(other)
    # Invalidate the @resolved cached
    @resolved = nil
    @explicit.merge!(other.explicit) do |match, model1, model2|
        if model1 == model2
            model1
        elsif model1.fullfills?(model2)
            model1
        elsif model2.fullfills?(model1)
            model2
        else
            raise ArgumentError, "cannot use both #{model1} and #{model2} for #{match}"
        end
    end
    @defaults |= other.defaults
end
pretty_print(pp) click to toggle source
# File lib/syskit/dependency_injection.rb, line 54
def pretty_print(pp)
    pp.text "DependencyInjection"

    pp.breakable
    pp.text "Explicit:"
    if !explicit.empty?
        pp.nest(2) do
            pp.breakable
            explicit = self.explicit.map do |k, v|
                k = k.short_name
                v = v.short_name
                [k, "#{k} => #{v}"]
            end.sort_by(&:first)
            pp.seplist(explicit) do |kv|
                pp.text kv[1]
            end
        end
    end

    pp.breakable
    pp.text "Defaults:"
    if !defaults.empty?
        pp.nest(2) do
            pp.breakable
            defaults = self.defaults.map(&:to_s).sort
            pp.seplist(defaults) do |v|
                pp.text v.to_s
            end
        end
    end
end
remove_unresolved() click to toggle source

Removes the unresolved instances from the list of selections

So far, unresolved selections are the ones that are represented as strings. The entries are not removed per se, but they are replaced by nil, to mark “do not use” selections.

# File lib/syskit/dependency_injection.rb, line 420
def remove_unresolved
    defaults.delete_if { |v| v.respond_to?(:to_str) }
    map! do |value|
        if value.respond_to?(:to_str)
            nil
        else value
        end
    end
end
resolve() click to toggle source

Resolves the selections by generating a direct mapping (as a hash) representing the required selection

# File lib/syskit/dependency_injection.rb, line 349
def resolve
    result = dup
    result.resolve!
    result
end
resolve!() click to toggle source
# File lib/syskit/dependency_injection.rb, line 342
def resolve!
    resolve_default_selections
    @explicit = DependencyInjection.resolve_recursive_selection_mapping(explicit)
end
resolve_default_selections() click to toggle source
# File lib/syskit/dependency_injection.rb, line 337
def resolve_default_selections
    @explicit = DependencyInjection.resolve_default_selections(explicit, defaults)
    defaults.clear
end
resolve_names(mapping = Hash.new) click to toggle source

Recursively resolve the selections that are specified as strings using the provided block

@return [Set<String>] the set of names that could not be resolved

# File lib/syskit/dependency_injection.rb, line 394
def resolve_names(mapping = Hash.new)
    unresolved = Set.new
    map! do |v|
        if v.respond_to?(:to_str)
            result = DependencyInjection.find_name_resolution(v, mapping)
            if !result
                unresolved << v
                v
            else result
            end

        elsif v.respond_to?(:resolve_names)
            # The value is e.g. an InstanceRequirements 
            unresolved |= v.resolve_names(mapping)
            v
        else v
        end
    end
    unresolved
end
selection_for(name, requirements) click to toggle source

Returns the selected instance based on the given name and requirements

@param [String,nil] name the selection name if there is one, or nil @param [InstanceRequirements] requirements the requirements for the selected

instance

@return

(Task,Model<Component>,Hash<Model<DataService>,Models::BoundDataService>,Set<Object>)

the selected instance.

If no matching selection is found, a matching model task proxy
is created. The first hash is a service mapping, from requested
service models to bound services in the task model. Finally, the
last set is the set of keys that have been used for the resolution.

@raise [IncompatibleComponentModels] if the various selections

lead to component models that are incompatible (i.e. to two
component models that are different and not subclassing one
another)
# File lib/syskit/dependency_injection.rb, line 268
def selection_for(name, requirements)
    if defaults.empty?
        selection = self.explicit
    else
        @resolved ||= resolve
        return @resolved.selection_for(name, requirements)
    end

    used_keys = Set.new
    selected_services = Hash.new
    selections = Set.new
    if name && (sel = selection[name]) && (sel != DependencyInjection.do_not_inherit)
        used_keys << name
        if sel == DependencyInjection.nothing
            sel = requirements
        end
        selections << [sel]
        selection.each do |key, value|
            next if !value.respond_to?(:component_model) || value.component_model != sel

            requirements.each_required_model do |req_m|
                if key.respond_to?(:fullfills?) && key.fullfills?(req_m)
                    selected_services[req_m] ||= value
                end
            end
        end
    else
        requirements.each_required_model do |required_m|
            if sel = direct_selection_for(required_m)
                selections << [sel, required_m]
                used_keys << required_m
            else
                selections << [required_m, required_m]
            end
        end
    end

    selected_instance, selected_requirements = nil, InstanceRequirements.new
    selections.each do |sel_m, required_m|
        if sel_m.respond_to?(:to_task)
            sel_task = sel_m.to_task
            selected_instance ||= sel_task
            if selected_instance != sel_task
                raise ArgumentError, "task instances #{selected_instance} and #{sel_m} are both selected for #{required_m || requirements}, but they are not compatible"
            end
        end

        sel_m = sel_m.to_instance_requirements
        if sel_m.service
            if required_m
                selected_services[required_m] = sel_m.service
            else
                requirements.each_required_model do |req_m|
                    if sel_m.fullfills?(req_m)
                        selected_services[req_m] ||= sel_m.service
                    end
                end
            end
        end
        selected_requirements.merge(sel_m.to_component_model, keep_abstract: true)
    end

    if selected_instance && !selected_instance.fullfills?(requirements, requirements.arguments)
        raise ArgumentError, "explicitly selected #{selected_instance}, but it does not fullfill the required #{requirements}"
    end

    return selected_instance, selected_requirements, selected_services, used_keys
end