class Roby::OpenStruct

This module defines functionality that can be mixed-in other objects to have an 'automatically extensible struct' behaviour, i.e.

Roby::OpenStruct objects are OpenStructs where attributes have a default class. They are used to build hierarchical data structure on-the-fly. Additionally, they may have a model which constrains what can be created on them

For instance

@example create an openstruct and assign a value in the hierarchy

root = Roby::OpenStruct.new
root.child.value = 42

However, you cannot check if a value is defined or not with

if (root.child)
    <do something>
end

You'll have to test with respond_to? or field_name?. The second one will return true only if the attribute is defined and it is not false

@example test for the presence of a value in the hierarchy

if root.respond_to?(:child)
    <do something if child has been set>
end
if root.child?
    <do something if child has been set and is non-nil>
end

Handling of methods defined on parents

Methods defined in Object or Kernel are automatically overriden if needed. For instance, if you're managing a (x, y, z) position using OpenStruct, you will want YAML#y to not get in the way. The exceptions are the methods listed in NOT_OVERRIDABLE

Constants

FORBIDDEN_NAMES
FORBIDDEN_NAMES_RX
NOT_OVERRIDABLE
NOT_OVERRIDABLE_RX

Attributes

__parent_name[R]
__parent_struct[R]
attach_as[R]
model[R]

Public Class Methods

_load(io) click to toggle source
# File lib/roby/state/open_struct.rb, line 107
def self._load(io)
    marshalled_members, aliases = Marshal.load(io)

    result = new
    marshalled_members.each do |name, marshalled_field|
        begin
            value = Marshal.load(marshalled_field)
            if value.kind_of?(OpenStruct)
                value.attach_to(result, name)
            else
                result.set(name, value)
            end
        rescue Exception
            Roby::DRoby.warn "cannot load #{name} #{marshalled_field}: #{$!.message}"
        end
    end

    result.instance_variable_set("@aliases", aliases)
    result

rescue Exception
    Roby::DRoby.warn "cannot load #{marshalled_members} #{io}: #{$!.message}"
    raise
end
new(model = nil, attach_to = nil, attach_name = nil) click to toggle source

attach_to and attach_name are used so that

root = OpenStruct.new
root.bla

does not add a bla attribute to root, while the following constructs

root.bla.test = 20
bla = root.bla
bla.test = 20

does

Note, however that

bla = root.bla
root.bla = 10
bla.test = 20

will not make root.bla be the bla object. And that

bla = root.bla
root.stable!
bla.test = 20

will not fail

# File lib/roby/state/open_struct.rb, line 65
def initialize(model = nil, attach_to = nil, attach_name = nil) # :nodoc
    clear

    @model = model
    @observers       = Hash.new { |h, k| h[k] = [] }
    @filters         = Hash.new

    if attach_to
        link_to(attach_to, attach_name)
    end

    if model
        attach_model
        attach
    end
end

Public Instance Methods

__get(name, create_substruct = true, &update) click to toggle source
# File lib/roby/state/open_struct.rb, line 451
def __get(name, create_substruct = true, &update)
    name = name.to_s

    if model
        # We never automatically create levels as the model should tell us
        # what we want
        create_substruct = false
    end

    if @members.has_key?(name)
        member = @members[name]
    else
        if alias_to = @aliases[name]
            return send(alias_to)
        elsif stable?
            raise NoMethodError, "no such attribute #{name} (#{self} is stable)"
        elsif create_substruct
            attach
            member = @pending[name] = create_subfield(name)
        else return
        end
    end

    if update
        member.update(&update)
    else
        member
    end
end
__merge(other) click to toggle source
# File lib/roby/state/open_struct.rb, line 577
def __merge(other)
    @members.merge(other) do |k, v1, v2|
        if v1.kind_of?(OpenStruct) && v2.kind_of?(OpenStruct)
            if v1.class != v2.class
                raise ArgumentError, "#{k} is a #{v1.class} in self and #{v2.class} in other, I don't know what to do"
            end
            v1.__merge(v2)
        else
            v2
        end
    end
end
__parent() click to toggle source
# File lib/roby/state/open_struct.rb, line 220
def __parent
    @__parent_struct ||
        (@attach_as[0] if @attach_as)
end
__root() click to toggle source
# File lib/roby/state/open_struct.rb, line 225
def __root
    if p = __parent
        return p.__root
    else self
    end
end
__root?() click to toggle source
# File lib/roby/state/open_struct.rb, line 216
def __root?
    !__parent
end
_dump(lvl = -1) click to toggle source
# File lib/roby/state/open_struct.rb, line 132
def _dump(lvl = -1)
    marshalled_members = @members.map do |name, value|
        [name, Marshal.dump(value)] rescue nil
    end
    marshalled_members.compact!
    Marshal.dump([marshalled_members, @aliases])
end
alias(from, to) click to toggle source
# File lib/roby/state/open_struct.rb, line 567
def alias(from, to)
    @aliases[to.to_s] = from.to_s
end
attach() click to toggle source

When a field is dynamically created by method_missing, it is created in a pending state, in which it is not yet attached to its parent structure

This method does the attachment. It calls attach_child on the parent to notify it

# File lib/roby/state/open_struct.rb, line 189
def attach
    if @attach_as
        @__parent_struct, @__parent_name = @attach_as
        @attach_as = nil
        __parent_struct.attach_child(__parent_name, self)
        if @model
            @model.attach
        end
    end
end
attach_child(name, obj) click to toggle source

Called by a child when attach is called

# File lib/roby/state/open_struct.rb, line 210
def attach_child(name, obj)
    @members[name.to_s] = obj
    updated(name, obj)
end
attach_model() click to toggle source

Do the necessary initialization after having added a model to this task

# File lib/roby/state/open_struct.rb, line 157
def attach_model
    model.each_member do |name, field|
        case field
        when OpenStructModel
            @members[name] ||= create_subfield(name)
        end
    end

    # Trigger updating the structure whenever the state model is
    # changed
    model.on_change(nil, false) do |name, value|
        if value.kind_of?(OpenStructModel)
            @members[name] ||= create_subfield(name)
        end
    end
end
attach_to(parent, name) click to toggle source
# File lib/roby/state/open_struct.rb, line 178
def attach_to(parent, name)
    link_to(parent, name)
    attach
end
attached?() click to toggle source

If true, this field is attached to a parent structure

# File lib/roby/state/open_struct.rb, line 233
def attached?
    !!@__parent_struct
end
clear() click to toggle source
# File lib/roby/state/open_struct.rb, line 82
def clear
    @attach_as       = nil
    @stable          = false
    @members         = Hash.new
    @pending         = Hash.new
    @aliases         = Hash.new
end
clear_model() click to toggle source
# File lib/roby/state/open_struct.rb, line 90
def clear_model
    @model = nil
end
create_model() click to toggle source
# File lib/roby/state/open_struct.rb, line 151
def create_model
    OpenStructModel.new
end
create_subfield(name) click to toggle source

Called by method_missing to create a subfield when needed.

The default is to create a subfield of the same class than self

# File lib/roby/state/open_struct.rb, line 484
def create_subfield(name)
    model = if self.model then self.model.get(name) end
    self.class.new(model, self, name)
end
delete(name = nil) click to toggle source
# File lib/roby/state/open_struct.rb, line 294
def delete(name = nil)
    raise TypeError, "#{self} is stable" if stable?
    if name
        name = name.to_s
        child = @members.delete(name) ||
            @pending.delete(name)
        if child && child.respond_to?(:detached!)
            child.detached!
        end

        # We don't detach aliases
        if !child && !@aliases.delete(name)
            raise ArgumentError, "no such child #{name}"
        end

        # and remove aliases that point to +name+
        @aliases.delete_if { |_, pointed_to| pointed_to == name }
    else
        if __parent_struct
            __parent_struct.delete(__parent_name)
        elsif @attach_as
            @attach_as.first.delete(@attach_as.last)
        else
            raise ArgumentError, "#{self} is attached to nothing"
        end
    end
end
detached!() click to toggle source
# File lib/roby/state/open_struct.rb, line 322
def detached!
    @__parent_struct, @__parent_name, @attach_as = nil
end
each_member(&block) click to toggle source

Iterates on all defined members of this object

# File lib/roby/state/open_struct.rb, line 274
def each_member(&block)
    @members.each(&block)
end
empty?() click to toggle source

Returns true if this object has no member

# File lib/roby/state/open_struct.rb, line 384
def empty?; @members.empty? end
filter(name, &block) click to toggle source

Define a filter for the name attribute on self. The given block is called when the attribute is written with both the attribute name and value. It should return the value that should actually be written, and raise an exception if the new value is invalid.

# File lib/roby/state/open_struct.rb, line 330
def filter(name, &block)
    @filters[name.to_s] = block
end
freeze() click to toggle source
# File lib/roby/state/open_struct.rb, line 346
def freeze
    freeze
    each_member do |name, field|
        field.freeze
    end
end
get(name) click to toggle source

Returns the value of the given field

Unlike method_missing, it will return nil if the field is not set

# File lib/roby/state/open_struct.rb, line 434
def get(name)
    __get(name, false)
end
global_filter(&block) click to toggle source

Define a filter for the name attribute on self. The given block is called when the attribute is written with both the attribute name and value. It should return the value that should actually be written, and raise an exception if the new value is invalid.

# File lib/roby/state/open_struct.rb, line 338
def global_filter(&block)
    @filters[nil] = block
end
has_method?(name) click to toggle source

has_method? will be used to know if a given method is already defined on the OpenStruct object, without taking into account the members and aliases.

# File lib/roby/state/open_struct.rb, line 390
def has_method?(name)
    Object.instance_method(:respond_to?).bind(self).call(name, true)
end
new_model() click to toggle source

Create a model structure and associate it with this openstruct

# File lib/roby/state/open_struct.rb, line 143
def new_model
    if !@model
        @model = create_model
        attach_model
    end
    @model
end
on_change(name = nil, recursive = false, &block) click to toggle source

Call block with the new value if name changes

If name is not given, it will be called for any change

# File lib/roby/state/open_struct.rb, line 252
def on_change(name = nil, recursive = false, &block)
    attach
    name = name.to_s if name
    @observers[name] << Observer.new(recursive, block)
    self
end
path() click to toggle source

Returns the path to root, i.e. the list of field names from the root of the extended struct tree

# File lib/roby/state/open_struct.rb, line 440
def path
    result = []
    obj = self
    while obj
        result.unshift(obj.__parent_name)
        obj = obj.__parent_struct
    end
    result.shift # we alwas add a nil for one-after-the-root
    result
end
pretty_print(pp) click to toggle source
# File lib/roby/state/open_struct.rb, line 94
def pretty_print(pp)
    pp.seplist(@members) do |child|
        child_name, child_obj = *child
        if child_obj.kind_of?(OpenStruct)
            pp.text "#{child_name} >"
        else
            pp.text "#{child_name}"
        end
        pp.breakable
        child_obj.pretty_print(pp)
    end
end
set(name, *args) click to toggle source
# File lib/roby/state/open_struct.rb, line 489
def set(name, *args)
    name = name.to_s
    name = @aliases[name] || name

    if model && !model.get(name).kind_of?(OpenStructModel::Variable)
        raise ArgumentError, "#{name} is not a state variable on #{self}"
    end

    value = args.first

    if stable?
        raise NoMethodError, "#{self} is stable"
    elsif @filters.has_key?(name)
        value = @filters[name].call(value)
    elsif @filters.has_key?(nil)
        value = @filters[nil].call(name, value)
    end

    if has_method?(name)
        if NOT_OVERRIDABLE_RX =~ name
            raise ArgumentError, "#{name} is already defined an cannot be overriden"
        end

        # Override it
        singleton_class.class_eval do
            define_method(name) do
                method_missing(name)
            end
        end
    end

    attach

    @aliases.delete(name)
    pending = @pending.delete(name)

    if pending && pending != value
        pending.detach
    end

    @members[name] = value
    updated(name, value)
    return value
end
stable!(recursive = false, is_stable = true) click to toggle source

Sets the stable attribute of self to is_stable. If recursive is true, set it on the child struct as well.

# File lib/roby/state/open_struct.rb, line 356
def stable!(recursive = false, is_stable = true)
    @stable = is_stable
    if recursive
        @members.each { |name, object| object.stable!(recursive, is_stable) if object.respond_to?(:stable!) }
    end
end
stable?() click to toggle source

If self is stable, it cannot be updated. That is, calling a setter method raises NoMethodError

# File lib/roby/state/open_struct.rb, line 344
def stable?; @stable end
to_hash(recursive = true) click to toggle source

Converts this OpenStruct into a corresponding hash, where all keys are symbols. If recursive is true, any member which responds to to_hash will be converted as well

# File lib/roby/state/open_struct.rb, line 262
def to_hash(recursive = true)
    result = Hash.new
    @members.each do |k, v|
        result[k.to_sym] = if recursive && v.respond_to?(:to_hash)
                               v.to_hash
                           else v
                           end
    end
    result
end
update(hash = nil) { |self| ... } click to toggle source

Update a set of values on this struct If a hash is given, it is an name => value hash of attribute values. A given block is yield with self, so that the construct

my.extendable.struct.very.deep.update do |deep|
  <update deep>
end

can be used

# File lib/roby/state/open_struct.rb, line 287
def update(hash = nil)
    attach
    hash.each { |k, v| send("#{k}=", v) } if hash
    yield(self) if block_given?
    self
end
updated(name, value, recursive = false) click to toggle source
# File lib/roby/state/open_struct.rb, line 363
def updated(name, value, recursive = false)
    if @observers.has_key?(name)
        @observers[name].each do |ob|
            if ob.recursive? || !recursive
                ob.call(name, value)
            end
        end
    end

    @observers[nil].each do |ob|
        if ob.recursive? || !recursive
            ob.call(name, value)
        end
    end

    if __parent_struct
        __parent_struct.updated(__parent_name, self, true)
    end
end

Protected Instance Methods

detach() click to toggle source

When a field is dynamically created by method_missing, it is created in a pending state, in which it is not yet attached to its parent structure

This method makes sure that the field will never be attached to the parent. It has no effect once attach has been called

# File lib/roby/state/open_struct.rb, line 206
def detach
    @attach_as = nil
end