Class: Roby::OpenStruct

Inherits:
Object show all
Defined in:
lib/roby/state/open_struct.rb

Overview

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

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

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

Examples:

create an openstruct and assign a value in the hierarchy

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

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

Defined Under Namespace

Classes: Observer

Constant Summary collapse

FORBIDDEN_NAMES =
%w{marshal each enum to}.map { |str| "^#{str}_" }
FORBIDDEN_NAMES_RX =
/(?:#{FORBIDDEN_NAMES.join("|")})/
NOT_OVERRIDABLE =
%w{class} + instance_methods(false)
NOT_OVERRIDABLE_RX =
/(?:#{NOT_OVERRIDABLE.join("|")})/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model = nil, attach_to = nil, attach_name = nil) ⇒ OpenStruct

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



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, &update) ⇒ Object

:nodoc:



534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/roby/state/open_struct.rb', line 534

def method_missing(name, *args, &update) # :nodoc:
    if name !~ /^\w+(?:\?|=|!)?$/
        if name[-1, 1] == '?'
            return false
        else
            super
        end
    end

    name = name.to_s

    if name =~ FORBIDDEN_NAMES_RX
        super(name.to_sym, *args, &update) 
    end

    if name =~ /^(\w+)=$/
        ret = set($1, *args)
        return ret

    elsif name =~ /^(\w+)\?$/
        # Test
        name = @aliases[$1] || $1
        respond_to?(name) && get(name) && send(name)

    elsif args.empty? # getter
        attach
        return __get(name, &update)

    else
        super(name.to_sym, *args, &update)
    end
end

Instance Attribute Details

#__parent_nameObject (readonly)

Returns the value of attribute __parent_name.



140
141
142
# File 'lib/roby/state/open_struct.rb', line 140

def __parent_name
  @__parent_name
end

#__parent_structObject (readonly)

Returns the value of attribute __parent_struct.



140
141
142
# File 'lib/roby/state/open_struct.rb', line 140

def __parent_struct
  @__parent_struct
end

#modelObject (readonly)

Returns the value of attribute model.



41
42
43
# File 'lib/roby/state/open_struct.rb', line 41

def model
  @model
end

Class Method Details

._load(io) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# 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

Instance Method Details

#__get(name, create_substruct = true, &update) ⇒ Object



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# 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) ⇒ Object



577
578
579
580
581
582
583
584
585
586
587
588
# 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

#__parentObject



220
221
222
223
# File 'lib/roby/state/open_struct.rb', line 220

def __parent
    @__parent_struct ||
        (@attach_as[0] if @attach_as)
end

#__respond_to__(name) ⇒ Object

1.8.7’s #respond_to? takes two arguments, 1.8.6 only one. This is the common implementation for both version. #respond_to? is adapted (see above)



416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/roby/state/open_struct.rb', line 416

def __respond_to__(name) # :nodoc:
    name = name.to_s
    return false if name =~ FORBIDDEN_NAMES_RX

    if name =~ /=$/
        !@stable
    else
        if @members.has_key?(name)
            true
        else
            (alias_to = @aliases[name]) && respond_to?(alias_to)
        end
    end
end

#__rootObject



225
226
227
228
229
230
# File 'lib/roby/state/open_struct.rb', line 225

def __root
    if p = __parent
        return p.__root
    else self
    end
end

#__root?Boolean

Returns:

  • (Boolean)


216
217
218
# File 'lib/roby/state/open_struct.rb', line 216

def __root?
    !__parent
end

#_dump(lvl = -1)) ⇒ Object



132
133
134
135
136
137
138
# 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) ⇒ Object



567
568
569
# File 'lib/roby/state/open_struct.rb', line 567

def alias(from, to)
    @aliases[to.to_s] = from.to_s
end

#attachObject

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



189
190
191
192
193
194
195
196
197
198
# 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) ⇒ Object

Called by a child when #attach is called



210
211
212
213
# File 'lib/roby/state/open_struct.rb', line 210

def attach_child(name, obj)
    @members[name.to_s] = obj
    updated(name, obj)
end

#attach_modelObject

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



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# 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) ⇒ Object



178
179
180
181
# File 'lib/roby/state/open_struct.rb', line 178

def attach_to(parent, name)
    link_to(parent, name)
    attach
end

#attached?Boolean

If true, this field is attached to a parent structure

Returns:

  • (Boolean)


233
234
235
# File 'lib/roby/state/open_struct.rb', line 233

def attached?
    !!@__parent_struct
end

#clearObject



82
83
84
85
86
87
88
# 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_modelObject



90
91
92
# File 'lib/roby/state/open_struct.rb', line 90

def clear_model
    @model = nil
end

#create_modelObject



151
152
153
# File 'lib/roby/state/open_struct.rb', line 151

def create_model
    OpenStructModel.new
end

#create_subfield(name) ⇒ Object

Called by #method_missing to create a subfield when needed.

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



484
485
486
487
# 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) ⇒ Object

Raises:

  • (TypeError)


294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# 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!Object



322
323
324
# File 'lib/roby/state/open_struct.rb', line 322

def detached!
    @__parent_struct, @__parent_name, @attach_as = nil
end

#each_member(&block) ⇒ Object

Iterates on all defined members of this object



274
275
276
# File 'lib/roby/state/open_struct.rb', line 274

def each_member(&block)
    @members.each(&block)
end

#empty?Boolean

Returns true if this object has no member

Returns:

  • (Boolean)


384
# File 'lib/roby/state/open_struct.rb', line 384

def empty?; @members.empty? end

#filter(name, &block) ⇒ Object

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.



330
331
332
# File 'lib/roby/state/open_struct.rb', line 330

def filter(name, &block)
    @filters[name.to_s] = block
end

#freezeObject



346
347
348
349
350
351
# File 'lib/roby/state/open_struct.rb', line 346

def freeze
    freeze
    each_member do |name, field|
        field.freeze
    end
end

#get(name) ⇒ Object

Returns the value of the given field

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



434
435
436
# File 'lib/roby/state/open_struct.rb', line 434

def get(name)
    __get(name, false)
end

#global_filter(&block) ⇒ Object

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.



338
339
340
# File 'lib/roby/state/open_struct.rb', line 338

def global_filter(&block)
    @filters[nil] = block
end

#has_method?(name) ⇒ Boolean

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.

Returns:

  • (Boolean)


390
391
392
# File 'lib/roby/state/open_struct.rb', line 390

def has_method?(name)
    Object.instance_method(:respond_to?).bind(self).call(name, true)
end


174
175
176
# File 'lib/roby/state/open_struct.rb', line 174

def link_to(parent, name)
    @attach_as = [parent, name]
end

#new_modelObject

Create a model structure and associate it with this openstruct



143
144
145
146
147
148
149
# 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) ⇒ Object

Call block with the new value if name changes

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



252
253
254
255
256
257
# 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

#pathObject

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



440
441
442
443
444
445
446
447
448
449
# 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) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
# 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

#respond_to?(name) ⇒ Boolean

:nodoc:

Returns:

  • (Boolean)


394
395
396
397
# File 'lib/roby/state/open_struct.rb', line 394

def respond_to?(name, include_private = false) # :nodoc:
    return true  if super
    return __respond_to__(name)
end

#set(name, *args) ⇒ Object



489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# 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) ⇒ Object

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



356
357
358
359
360
361
# 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?Boolean

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

Returns:

  • (Boolean)


344
# File 'lib/roby/state/open_struct.rb', line 344

def stable?; @stable end

#to_hash(recursive = true) ⇒ Object

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



262
263
264
265
266
267
268
269
270
271
# 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| ... } ⇒ Object

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

Yields:

  • (_self)

Yield Parameters:



287
288
289
290
291
292
# 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) ⇒ Object



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# 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