class Orocos::TaskConfigurations

Class handling multiple possible configuration for a single task

It can load configuration files that are structured as follows:

A configuration file is a YAML file that contains multiple sections. Each section starts with — and can contain options of the form option_name:value. The section header can be omitted for the very first section

For instance

--- name:default merge:true chain:default,test

The following options are possible:

name

it is optional for the first section and mandatory for further sections. It gives a name to the section, that can then be used to refer to the configuration information in #apply and #conf. If ommitted for the first section, the name ‘default’ is used

merge

If set to true, the section will be merged with previous configuration data previously stored under the same name. Otherwise, it replaces existing information. The default is false.

chain

If set, it has to be a comma-separated list of configuration names. It tells the configuration class that this configuration section should always be merged with the ones listed. The name of the current configuration section can be listed, in which case it will be merged in the specified order. Otherwise, it is added at the end.

Attributes

conf_options[R]
model[R]
sections[R]

Public Class Methods

config_as_hash(task) click to toggle source
# File lib/orocos/configurations.rb, line 373
def self.config_as_hash(task)
    current_config = Hash.new
    task.each_property do |prop|
        current_config[prop.name] = typelib_to_yaml_value(prop.raw_read)
    end
    current_config
end
merge_conf(a, b, override) click to toggle source

Helper method that adds the configuration of b into the existing configuration hash a

# File lib/orocos/configurations.rb, line 220
def self.merge_conf(a, b, override)
    result = if override
        a.recursive_merge(b) do |k, v1, v2|
            if v1.respond_to?(:to_ary) && v2.respond_to?(:to_ary)
                merge_conf_array(v1, v2, true)
            else
                v2
            end
        end
    else
        a.recursive_merge(b) do |k, v1, v2|
            if v1.respond_to?(:to_ary) && v2.respond_to?(:to_ary)
                merge_conf_array(v1, v2, false)
            elsif v1 != v2
                raise ArgumentError, "cannot merge configuration: conflict in field #{k} between v1=#{v1} and v2=#{v2}"
            else
                v1
            end
        end
    end
    result
end
merge_conf_array(a, b, override) click to toggle source
# File lib/orocos/configurations.rb, line 188
def self.merge_conf_array(a, b, override)
    result = []
    a.each_with_index do |v1, idx|
        v2 = b[idx]

        if !v2
            result << v1
            next
        elsif !v1
            result << v2
            next
        end

        if v1.kind_of?(Hash) && v2.kind_of?(Hash)
            result << merge_conf(v1, v2, override)
        elsif v1.respond_to?(:to_ary) && v2.respond_to?(:to_ary)
            result << merge_conf_array(v1, v2, override)
        elsif override || v1 == v2
            result << v2
        else
            raise ArgumentError, "cannot merge configuration: conflict in [#{idx}] between v1=#{v1} and v2=#{v2}"
        end
    end

    if b.size > a.size
        result.concat(b[a.size..-1])
    end
    result
end
new(task_model) click to toggle source
# File lib/orocos/configurations.rb, line 41
def initialize(task_model)
    @model = task_model
    @sections = Hash.new
    @merged_conf = Hash.new
end
save(task, file, name) click to toggle source

Saves the current configuration of task in the provided file. name is the name of the new section.

# File lib/orocos/configurations.rb, line 388
def self.save(task, file, name)
    if File.directory?(file)
        file = File.join(file, "#{task.model.name}.yml")
    else
        FileUtils.mkdir_p(File.dirname(file))
    end
    name ||= task.name

    current_config = config_as_hash(task)

    parts = []
    current_config.keys.sort.each do |property_name|
        doc = task.model.find_property(property_name).doc
        if doc
            parts << doc.split("\n").map { |s| "# #{s}" }.join("\n")
        else
            parts << "# no documentation available for this property"
        end

        property_hash = { property_name => current_config[property_name] }
        yaml = YAML.dump(property_hash)
        parts << yaml.split("\n")[1..-1].join("\n")
    end

    File.open(file, 'a') do |io|
        io.write("--- name:#{name}\n")
        io.write(parts.join("\n"))
        io.puts
    end
    current_config
end
typelib_to_yaml_value(value) click to toggle source
# File lib/orocos/configurations.rb, line 354
def self.typelib_to_yaml_value(value)
    if value.kind_of?(Typelib::CompoundType)
        result = Hash.new
        value.raw_each_field do |field_name, field_value|
            result[field_name] = typelib_to_yaml_value(field_value)
        end
        result
    elsif value.kind_of?(Symbol)
        value.to_s
    elsif value.respond_to?(:to_str)
        value.to_str
    elsif value.kind_of?(Typelib::ArrayType) || value.kind_of?(Typelib::ContainerType)
        value.raw_each.map(&method(:typelib_to_yaml_value))
    elsif value.kind_of?(Typelib::Type)
        Typelib.to_ruby(value)
    else value
    end
end

Public Instance Methods

[](section_name) click to toggle source

Retrieves the configuration for the given section name

# File lib/orocos/configurations.rb, line 48
def [](section_name)
    sections[section_name]
end
add(name, conf, options = Hash.new) click to toggle source
# File lib/orocos/configurations.rb, line 121
def add(name, conf, options = Hash.new)
    options = Kernel.validate_options options,
        :merge => true, :chain => nil

    conf = config_from_hash(conf)

    changed = false
    if self.sections[name]
        if options[:merge]
            conf = TaskConfigurations.merge_conf(self.sections[name], conf, true)
        end
        changed = changed || self.sections[name] != conf
    else
        changed = true
    end
    self.sections[name] = conf
    changed
end
apply(task, names, override = false) click to toggle source

Applies the specified configuration to the given task

See #configuration for a description of names and override

# File lib/orocos/configurations.rb, line 327
def apply(task, names, override = false)
    if names.respond_to?(:to_ary)
        config = conf(names, override)
    elsif names.respond_to?(:to_str)
        config = conf([names], override)
    else
        config = names
    end

    if !config
        if names == ['default']
            ConfigurationManager.info "required to apply configuration #{names.join(", ")} on #{task.name} of type #{task.model.name}, but this configuration is not registered or empty. Not changing anything."
            return
        else
            raise ArgumentError, "no configuration #{names.join(", ")} for #{task.model.name}"
        end
    end
    
    timestamp = Time.now
    config.each do |prop_name, conf|
        p = task.property(prop_name)
        result = p.raw_read
        result = apply_configuration_to_value(result, conf)
        p.write(result, timestamp)
    end
end
apply_configuration_array_to_value(value, conf) click to toggle source
# File lib/orocos/configurations.rb, line 301
def apply_configuration_array_to_value(value, conf)
    conf.each_with_index do |element, idx|
        while value.size <= idx
            new_value = value.class.deference.new
            new_value.zero!
            value.push(new_value)
        end
        value[idx] = apply_configuration_to_value(value.raw_get(idx), element)
    end
    value
end
apply_configuration_hash_to_value(value, conf) click to toggle source
# File lib/orocos/configurations.rb, line 294
def apply_configuration_hash_to_value(value, conf)
    conf.each do |conf_key, conf_value|
        value[conf_key] = apply_configuration_to_value(value.raw_get_field(conf_key), conf_value)
    end
    value
end
conf(names, override = false) click to toggle source

Returns the task configuration that is the combination of the configurations listed in names

If override is false (the default), a requested configuration cannot override a value set by another (the set of fields they are setting must be disjoint)

Otherwise, the configurations are merged in the same order than listed in names

For instance, let’s assume that the following configurations are available

--- name:default
threshold: 20
--- name: fast
speed: 10
--- name: slow
speed: 1

Then

configuration(['default', 'fast'])

returns { ‘threshold’ => 20, ‘speed’ => 10 } regardless of the value of the override parameter, while

configuration(['default', 'fast', 'slow'])

will raise ArgumentError and

configuration(['default', 'fast', 'slow'], true)

returns { ‘threshold’ => 20, ‘speed’ => 1 }

# File lib/orocos/configurations.rb, line 277
def conf(names, override = false)
    if names.size == 1
        return sections[names.first]
    elsif cached = @merged_conf[[names, override]]
        return cached
    else
        if !sections[names.last]
            raise ArgumentError, "#{names.last} is not a known configuration section"
        end
        config = conf(names[0..-2], override)
        config = TaskConfigurations.merge_conf(config, sections[names.last], override)

        @merged_conf[[names, override]] = config
        return config
    end
end
config_from_array(array, value_t) click to toggle source
# File lib/orocos/configurations.rb, line 140
def config_from_array(array, value_t)
    element_t = value_t.deference
    array.map do |value|
        if value.kind_of?(Hash)
            config_from_hash(value, element_t)
        elsif value.respond_to?(:to_ary)
            config_from_array(value, element_t)
        else
            Typelib.from_ruby(value, element_t)
        end
    end
end
load_from_yaml(file) click to toggle source

Loads the configurations from a YAML file

Multiple configurations can be saved in the file, in which case each configuration set must be separated by a line of the form

--- name:configuration_name

The first YAML document has, by default, the name ‘default’. One can also be provided if needed.

Returns a set of section names, of the section that have been modified

# File lib/orocos/configurations.rb, line 63
def load_from_yaml(file)
    document_lines = File.readlines(file)

    headers = document_lines.enum_for(:each_with_index).
        find_all { |line, _| line =~ /^---/ }
    if headers.empty? || headers.first[1] != 0
        headers.unshift ["--- name:default", -1]
    end

    options = headers.map do |line, line_number|
        line_options = Hash.new
        line = line.chomp
        line.split(/\s+/)[1..-1].each do |opt|
            if opt =~ /^(\w+):(.*)$/
                line_options[$1] = $2
            else
                raise ArgumentError, "#{file}:#{line_number}: wrong format #{opt}, expected option_name:value, where 'value' has no spaces"
            end
        end
        line_options['merge'] = (line_options['merge'] == 'true')
        line_options['chain'] = (line_options['chain'] || '').split(',')
        [line_options, line_number]
    end
    options[0][0]['name'] ||= 'default'

    options.each do |line_options, line_number|
        if !line_options['name']
            raise ArgumentError, "#{file}:#{line_number}: missing a 'name' option"
        end
    end

    sections = []
    options.each_cons(2) do |(_, line0), (_, line1)|
        sections << document_lines[line0 + 1, line1 - line0 - 1]
    end
    sections << document_lines[options[-1][1] + 1, document_lines.size - options[-1][1] - 1]

    changed_sections = []
    @conf_options = options

    sections.each_with_index do |doc, idx|
        doc = doc.join("")
        result = YAML.load(StringIO.new(doc))

        conf_options = options[idx].first
        name = conf_options.delete('name')
        if add(name, result || Hash.new, conf_options)
            changed_sections << name
        end
    end
    if !changed_sections.empty?
       @merged_conf.clear
    end
    changed_sections
rescue Exception => e
    raise e, "error loading #{file}: #{e.message}", e.backtrace
end
save(task, file, name) click to toggle source
# File lib/orocos/configurations.rb, line 381
def save(task, file, name)
    config_hash = self.class.save(task, file, name)
    sections[name] = config_from_hash(config_hash)
end