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, Stable

Constant Summary collapse

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

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



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/roby/state/open_struct.rb', line 67

def initialize(model = nil, attach_to = nil, attach_name = nil)
    clear

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

    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:



543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# File 'lib/roby/state/open_struct.rb', line 543

def method_missing(name, *args, &update) # :nodoc:
    if name !~ /^\w+(?:\?|=|!)?$/
        if name.end_with?("?")
            return false
        else
            super
        end
    end

    name = name.to_s

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

    if name.end_with?("=")
        key = name[0..-2]
        set(key, *args)

    elsif name.end_with?("?")
        key = name[0..-2]
        name = @aliases[key] || key
        respond_to?(name) && get(name) && send(name)

    elsif args.empty? # getter
        attach unless member?(name)
        __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.



43
44
45
# File 'lib/roby/state/open_struct.rb', line 43

def __parent_name
  @__parent_name
end

#__parent_structObject (readonly)

Returns the value of attribute __parent_struct.



43
44
45
# File 'lib/roby/state/open_struct.rb', line 43

def __parent_struct
  @__parent_struct
end

#modelObject (readonly)

Returns the value of attribute model.



43
44
45
# File 'lib/roby/state/open_struct.rb', line 43

def model
  @model
end

Class Method Details

._load(io) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/roby/state/open_struct.rb', line 109

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



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
480
# File 'lib/roby/state/open_struct.rb', line 453

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]
    elsif alias_to = @aliases[name]
        return send(alias_to)
    elsif stable?
        raise Stable, "no such attribute #{name} (#{self} is stable)"
    elsif create_substruct
        attach
        member = @pending[name] = create_subfield(name)
    else
        return
    end

    if update
        member.update(&update)
    else
        member
    end
end

#__merge(other) ⇒ Object



594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'lib/roby/state/open_struct.rb', line 594

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



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

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

#__rootObject



232
233
234
235
236
237
238
# File 'lib/roby/state/open_struct.rb', line 232

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

#__root?Boolean

Returns:

  • (Boolean)


223
224
225
# File 'lib/roby/state/open_struct.rb', line 223

def __root?
    !__parent
end

#_dump(lvl = -1)) ⇒ Object



133
134
135
136
137
138
139
# File 'lib/roby/state/open_struct.rb', line 133

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



576
577
578
# File 'lib/roby/state/open_struct.rb', line 576

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

#alias?(name) ⇒ Boolean

Returns:

  • (Boolean)


590
591
592
# File 'lib/roby/state/open_struct.rb', line 590

def alias?(name)
    @aliases.key?(name.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



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/roby/state/open_struct.rb', line 190

def attach
    return unless @attach_as

    parent_struct, parent_name = @attach_as
    if parent_struct.stable? && !parent_struct.member?(parent_name)
        raise Stable,
              "cannot attach #{self} on #{parent_struct}, the parent is stable " \
              "and attaching would create a new field named #{parent_name}"
    end

    @__parent_struct, @__parent_name = @attach_as
    @attach_as = nil
    __parent_struct.attach_child(__parent_name, self)
    @model&.attach
end

#attach_child(name, obj) ⇒ Object

Called by a child when #attach is called



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

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



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/roby/state/open_struct.rb', line 156

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



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

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)


241
242
243
# File 'lib/roby/state/open_struct.rb', line 241

def attached?
    !!@__parent_struct
end

#clearObject



84
85
86
87
88
89
90
# File 'lib/roby/state/open_struct.rb', line 84

def clear
    @attach_as       = nil
    @stable          = false
    @members         = {}
    @pending         = {}
    @aliases         = {}
end

#clear_modelObject



92
93
94
# File 'lib/roby/state/open_struct.rb', line 92

def clear_model
    @model = nil
end

#create_modelObject



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

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



485
486
487
488
# File 'lib/roby/state/open_struct.rb', line 485

def create_subfield(name)
    model = self.model&.get(name)
    self.class.new(model, self, name)
end

#delete(name = nil) ⇒ Object

Raises:

  • (TypeError)


307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/roby/state/open_struct.rb', line 307

def delete(name = nil)
    raise TypeError, "cannot delete #{name}, #{self} is stable" if stable?

    if name
        name = name.to_s
        child = @members.delete(name) || @pending.delete(name)
        child.detached! if child.respond_to?(:detached!)

        # 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 }
    elsif __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

#detached!Object



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

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

#each_member(&block) ⇒ Object

Iterates on all defined members of this object



287
288
289
# File 'lib/roby/state/open_struct.rb', line 287

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

#empty?Boolean

Returns true if this object has no member

Returns:

  • (Boolean)


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

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.



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

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

#freezeObject



358
359
360
361
362
363
# File 'lib/roby/state/open_struct.rb', line 358

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



436
437
438
# File 'lib/roby/state/open_struct.rb', line 436

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.



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

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)


403
404
405
406
407
408
409
410
411
412
# File 'lib/roby/state/open_struct.rb', line 403

def has_method?(name)
    return false unless respond_to?(name, true)

    name = name.to_s
    if name.end_with?("?") || name.end_with?("=")
        name = name[0..-2]
    end

    !member?(name) && !alias?(name)
end


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

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

#member?(name) ⇒ Boolean

Returns:

  • (Boolean)


586
587
588
# File 'lib/roby/state/open_struct.rb', line 586

def member?(name)
    @members.key?(name.to_s)
end

#new_modelObject

Create a model structure and associate it with this openstruct



142
143
144
145
146
147
148
# File 'lib/roby/state/open_struct.rb', line 142

def new_model
    unless @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



264
265
266
267
268
269
# File 'lib/roby/state/open_struct.rb', line 264

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



442
443
444
445
446
447
448
449
450
451
# File 'lib/roby/state/open_struct.rb', line 442

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



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/roby/state/open_struct.rb', line 96

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.to_s
        end
        pp.breakable
        child_obj.pretty_print(pp)
    end
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

:nodoc:

Returns:

  • (Boolean)


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

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

    name = name.to_s
    return false if name =~ FORBIDDEN_NAMES_RX

    if name.end_with?("=") || name.end_with?("?")
        name = name[0..-2]
        return true if member?(name) || alias?(name)
        return false if respond_to?(name, include_private)

        !@stable
    elsif member?(name) || alias?(name)
        true
    else
        (alias_to = @aliases[name]) && respond_to?(alias_to)
    end
end

#set(name, *args) ⇒ Object



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
533
534
535
536
537
538
539
540
541
# File 'lib/roby/state/open_struct.rb', line 490

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

    attach_model, attach_name = @attach_as
    if attach_model&.stable? && !attach_model.member?(attach_name)
        raise Stable,
              "cannot set #{name}, its parent #{parent_state} is stable and " \
              "setting it would create a new field #{attach_name} on the parent"
    elsif stable? && !member?(name)
        raise Stable,
              "cannot set #{name} on #{self}, it is stable and currently has " \
              "no such field"
    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)
    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.



368
369
370
371
372
373
374
375
# File 'lib/roby/state/open_struct.rb', line 368

def stable!(recursive = false, is_stable = true)
    @stable = is_stable
    return unless recursive

    @members.each do |(_, object)|
        object.stable!(recursive, is_stable) if object.respond_to?(:stable!)
    end
end

#stable?Boolean

If self is stable, its structure cannot be changed

Any modification that would create new fields will raise a Stable exception

Returns:

  • (Boolean)


354
355
356
# File 'lib/roby/state/open_struct.rb', line 354

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



274
275
276
277
278
279
280
281
282
283
284
# File 'lib/roby/state/open_struct.rb', line 274

def to_hash(recursive = true)
    result = {}
    @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:



300
301
302
303
304
305
# File 'lib/roby/state/open_struct.rb', line 300

def update(hash = nil)
    attach
    hash&.each { |k, v| send("#{k}=", v) }
    yield(self) if block_given?
    self
end

#updated(name, value, recursive = false) ⇒ Object



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/roby/state/open_struct.rb', line 377

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

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