1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
|
#
# = ostruct.rb: OpenStruct implementation
#
# Author:: Yukihiro Matsumoto
# Documentation:: Gavin Sinclair
#
# OpenStruct allows the creation of data objects with arbitrary attributes.
# See OpenStruct for an example.
#
#
# An OpenStruct is a data structure, similar to a Hash, that allows the
# definition of arbitrary attributes with their accompanying values. This is
# accomplished by using Ruby's metaprogramming to define methods on the class
# itself.
#
# == Examples:
#
# require 'ostruct'
#
# person = OpenStruct.new
# person.name = "John Smith"
# person.age = 70
# person.pension = 300
#
# puts person.name # -> "John Smith"
# puts person.age # -> 70
# puts person.address # -> nil
#
# An OpenStruct employs a Hash internally to store the methods and values and
# can even be initialized with one:
#
# australia = OpenStruct.new(:country => "Australia", :population => 20_000_000)
# p australia # -> <OpenStruct country="Australia" population=20000000>
#
# Hash keys with spaces or characters that would normally not be able to use for
# method calls (e.g. ()[]*) will not be immediately available on the
# OpenStruct object as a method for retrieval or assignment, but can be still be
# reached through the Object#send method.
#
# measurements = OpenStruct.new("length (in inches)" => 24)
# measurements.send("length (in inches)") # -> 24
#
# data_point = OpenStruct.new(:queued? => true)
# data_point.queued? # -> true
# data_point.send("queued?=",false)
# data_point.queued? # -> false
#
# Removing the presence of a method requires the execution the delete_field
# method as setting the property value to +nil+ will not remove the method.
#
# first_pet = OpenStruct.new(:name => 'Rowdy', :owner => 'John Smith')
# first_pet.owner = nil
# second_pet = OpenStruct.new(:name => 'Rowdy')
#
# first_pet == second_pet # -> false
#
# first_pet.delete_field(:owner)
# first_pet == second_pet # -> true
#
#
# == Implementation:
#
# An OpenStruct utilizes Ruby's method lookup structure to find and define the
# necessary methods for properties. This is accomplished through the method
# method_missing and define_method.
#
# This should be a consideration if there is a concern about the performance of
# the objects that are created, as there is much more overhead in the setting
# of these properties compared to using a Hash or a Struct.
#
class OpenStruct
#
# Creates a new OpenStruct object. By default, the resulting OpenStruct
# object will have no attributes.
#
# The optional +hash+, if given, will generate attributes and values
# (can be a Hash, an OpenStruct or a Struct).
# For example:
#
# require 'ostruct'
# hash = { "country" => "Australia", :population => 20_000_000 }
# data = OpenStruct.new(hash)
#
# p data # -> <OpenStruct country="Australia" population=20000000>
#
def initialize(hash=nil)
@table = {}
if hash
hash.each_pair do |k, v|
k = k.to_sym
@table[k] = v
new_ostruct_member(k)
end
end
end
# Duplicate an OpenStruct object members.
def initialize_copy(orig)
super
@table = @table.dup
@table.each_key{|key| new_ostruct_member(key)}
end
#
# Converts the OpenStruct to a hash with keys representing
# each attribute (as symbols) and their corresponding values
# Example:
#
# require 'ostruct'
# data = OpenStruct.new("country" => "Australia", :population => 20_000_000)
# data.to_h # => {:country => "Australia", :population => 20000000 }
#
def to_h
@table.dup
end
#
# Yields all attributes (as a symbol) along with the corresponding values
# or returns an enumerator if not block is given.
# Example:
#
# require 'ostruct'
# data = OpenStruct.new("country" => "Australia", :population => 20_000_000)
# data.each_pair.to_a # => [[:country, "Australia"], [:population, 20000000]]
#
def each_pair
return to_enum(__method__) { @table.size } unless block_given?
@table.each_pair{|p| yield p}
end
#
# Provides marshalling support for use by the Marshal library.
#
def marshal_dump
@table
end
#
# Provides marshalling support for use by the Marshal library.
#
def marshal_load(x)
@table = x
@table.each_key{|key| new_ostruct_member(key)}
end
#
# Used internally to check if the OpenStruct is able to be
# modified before granting access to the internal Hash table to be modified.
#
def modifiable
begin
@modifiable = true
rescue
raise RuntimeError, "can't modify frozen #{self.class}", caller(3)
end
@table
end
protected :modifiable
#
# Used internally to defined properties on the
# OpenStruct. It does this by using the metaprogramming function
# define_singleton_method for both the getter method and the setter method.
#
def new_ostruct_member(name)
name = name.to_sym
unless respond_to?(name)
define_singleton_method(name) { @table[name] }
define_singleton_method("#{name}=") { |x| modifiable[name] = x }
end
name
end
protected :new_ostruct_member
def method_missing(mid, *args) # :nodoc:
mname = mid.id2name
len = args.length
if mname.chomp!('=')
if len != 1
raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
end
modifiable[new_ostruct_member(mname)] = args[0]
elsif len == 0
@table[mid]
else
err = NoMethodError.new "undefined method `#{mid}' for #{self}", mid, args
err.set_backtrace caller(1)
raise err
end
end
# Returns the value of a member.
#
# person = OpenStruct.new('name' => 'John Smith', 'age' => 70)
# person[:age] # => 70, same as ostruct.age
#
def [](name)
@table[name.to_sym]
end
#
# Sets the value of a member.
#
# person = OpenStruct.new('name' => 'John Smith', 'age' => 70)
# person[:age] = 42 # => equivalent to ostruct.age = 42
# person.age # => 42
#
def []=(name, value)
modifiable[new_ostruct_member(name)] = value
end
#
# Remove the named field from the object. Returns the value that the field
# contained if it was defined.
#
# require 'ostruct'
#
# person = OpenStruct.new('name' => 'John Smith', 'age' => 70)
#
# person.delete_field('name') # => 'John Smith'
#
def delete_field(name)
sym = name.to_sym
singleton_class.__send__(:remove_method, sym, "#{sym}=")
@table.delete sym
end
InspectKey = :__inspect_key__ # :nodoc:
#
# Returns a string containing a detailed summary of the keys and values.
#
def inspect
str = "#<#{self.class}"
ids = (Thread.current[InspectKey] ||= [])
if ids.include?(object_id)
return str << ' ...>'
end
ids << object_id
begin
first = true
for k,v in @table
str << "," unless first
first = false
str << " #{k}=#{v.inspect}"
end
return str << '>'
ensure
ids.pop
end
end
alias :to_s :inspect
attr_reader :table # :nodoc:
protected :table
#
# Compares this object and +other+ for equality. An OpenStruct is equal to
# +other+ when +other+ is an OpenStruct and the two objects' Hash tables are
# equal.
#
def ==(other)
return false unless other.kind_of?(OpenStruct)
@table == other.table
end
#
# Compares this object and +other+ for equality. An OpenStruct is eql? to
# +other+ when +other+ is an OpenStruct and the two objects' Hash tables are
# eql?.
#
def eql?(other)
return false unless other.kind_of?(OpenStruct)
@table.eql?(other.table)
end
# Compute a hash-code for this OpenStruct.
# Two hashes with the same content will have the same hash code
# (and will be eql?).
def hash
@table.hash
end
end
|