Class: Roby::OpenStruct
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
Direct Known Subclasses
ConfModel, GoalSpace, OpenStructModel, StateDataSourceField, StateField, StateLastValueField
Defined Under Namespace
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
-
#__parent_name ⇒ Object
readonly
Returns the value of attribute __parent_name.
-
#__parent_struct ⇒ Object
readonly
Returns the value of attribute __parent_struct.
-
#model ⇒ Object
readonly
Returns the value of attribute model.
Class Method Summary collapse
Instance Method Summary collapse
- #__get(name, create_substruct = true, &update) ⇒ Object
- #__merge(other) ⇒ Object
- #__parent ⇒ Object
- #__root ⇒ Object
- #__root? ⇒ Boolean
- #_dump(lvl = -1)) ⇒ Object
- #alias(from, to) ⇒ Object
- #alias?(name) ⇒ Boolean
-
#attach ⇒ Object
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.
-
#attach_child(name, obj) ⇒ Object
Called by a child when #attach is called.
-
#attach_model ⇒ Object
Do the necessary initialization after having added a model to this task.
- #attach_to(parent, name) ⇒ Object
-
#attached? ⇒ Boolean
If true, this field is attached to a parent structure.
- #clear ⇒ Object
- #clear_model ⇒ Object
- #create_model ⇒ Object
-
#create_subfield(name) ⇒ Object
Called by #method_missing to create a subfield when needed.
- #delete(name = nil) ⇒ Object
- #detached! ⇒ Object
-
#each_member(&block) ⇒ Object
Iterates on all defined members of this object.
-
#empty? ⇒ Boolean
Returns true if this object has no member.
-
#filter(name, &block) ⇒ Object
Define a filter for the
nameattribute on self. - #freeze ⇒ Object
-
#get(name) ⇒ Object
Returns the value of the given field.
-
#global_filter(&block) ⇒ Object
Define a filter for the
nameattribute on self. -
#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.
-
#initialize(model = nil, attach_to = nil, attach_name = nil) ⇒ OpenStruct
constructor
attach_toandattach_nameare used so that root = OpenStruct.new root.bla does not add ablaattribute to root, while the following constructs root.bla.test = 20 bla = root.bla bla.test = 20 does. - #link_to(parent, name) ⇒ Object
- #member?(name) ⇒ Boolean
-
#method_missing(name, *args, &update) ⇒ Object
:nodoc:.
-
#new_model ⇒ Object
Create a model structure and associate it with this openstruct.
-
#on_change(name = nil, recursive = false, &block) ⇒ Object
Call
blockwith the new value ifnamechanges. -
#path ⇒ Object
Returns the path to root, i.e.
- #pretty_print(pp) ⇒ Object
-
#respond_to_missing?(name, include_private = false) ⇒ Boolean
:nodoc:.
- #set(name, *args) ⇒ Object
-
#stable!(recursive = false, is_stable = true) ⇒ Object
Sets the stable attribute of
selftois_stable. -
#stable? ⇒ Boolean
If self is stable, its structure cannot be changed.
-
#to_hash(recursive = true) ⇒ Object
Converts this OpenStruct into a corresponding hash, where all keys are symbols.
-
#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.
- #updated(name, value, recursive = false) ⇒ Object
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_name ⇒ Object (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_struct ⇒ Object (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 |
#model ⇒ Object (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}: #{$!.}" end end result.instance_variable_set("@aliases", aliases) result rescue Exception Roby::DRoby.warn "cannot load #{marshalled_members} #{io}: #{$!.}" 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 |
#__parent ⇒ Object
227 228 229 230 |
# File 'lib/roby/state/open_struct.rb', line 227 def __parent @__parent_struct || (@attach_as[0] if @attach_as) end |
#__root ⇒ Object
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
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
590 591 592 |
# File 'lib/roby/state/open_struct.rb', line 590 def alias?(name) @aliases.key?(name.to_s) end |
#attach ⇒ Object
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_model ⇒ Object
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
241 242 243 |
# File 'lib/roby/state/open_struct.rb', line 241 def attached? !!@__parent_struct end |
#clear ⇒ Object
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_model ⇒ Object
92 93 94 |
# File 'lib/roby/state/open_struct.rb', line 92 def clear_model @model = nil end |
#create_model ⇒ Object
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
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
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 |
#freeze ⇒ Object
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.
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 |
#link_to(parent, name) ⇒ Object
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
586 587 588 |
# File 'lib/roby/state/open_struct.rb', line 586 def member?(name) @members.key?(name.to_s) end |
#new_model ⇒ Object
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 |
#path ⇒ Object
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:
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
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
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 |