ostruct 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/ostruct.rb +157 -82
- data/ostruct.gemspec +11 -3
- metadata +7 -9
- data/.travis.yml +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44161760b6aef61a50eddb473940b3a23c75682afbd4e9573124ee4ae0afbf50
|
4
|
+
data.tar.gz: 757b50ba4015029f3fcacaa00e94b685eff032d65ea894e0523de585903e3775
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8020aad504a528f4b6bb3abd3b9a6993d9abbbdd3bb6e1306c3e33d21d5628a5119d78b4b749ae44cf98ed2bc707a9012f38ea1eb0a378e16b9db384af84dd3
|
7
|
+
data.tar.gz: 7201f711580ee6590ba6598328a611cea8ef82cd4b2cba636b2b684c0d01e2a52baff2494ac2428b0b3998b8f11ef0fc766025526310bd74ad15ebbf302d61f4
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
# OpenStruct
|
1
|
+
# OpenStruct [](https://badge.fury.io/rb/ostruct) [](https://stdgems.org/ostruct/) [](https://github.com/ruby/ostruct/actions?query=workflow%3Atest)
|
2
2
|
|
3
|
-
An OpenStruct is a data structure, similar to a Hash, that allows the definition of arbitrary attributes with their
|
3
|
+
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.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
data/lib/ostruct.rb
CHANGED
@@ -36,9 +36,10 @@
|
|
36
36
|
# Hash keys with spaces or characters that could normally not be used for
|
37
37
|
# method calls (e.g. <code>()[]*</code>) will not be immediately available
|
38
38
|
# on the OpenStruct object as a method for retrieval or assignment, but can
|
39
|
-
# still be reached through the Object#send method.
|
39
|
+
# still be reached through the Object#send method or using [].
|
40
40
|
#
|
41
41
|
# measurements = OpenStruct.new("length (in inches)" => 24)
|
42
|
+
# measurements[:"length (in inches)"] # => 24
|
42
43
|
# measurements.send("length (in inches)") # => 24
|
43
44
|
#
|
44
45
|
# message = OpenStruct.new(:queued? => true)
|
@@ -61,8 +62,9 @@
|
|
61
62
|
# first_pet # => #<OpenStruct name="Rowdy">
|
62
63
|
# first_pet == second_pet # => true
|
63
64
|
#
|
65
|
+
# Ractor compatibility: A frozen OpenStruct with shareable values is itself shareable.
|
64
66
|
#
|
65
|
-
# ==
|
67
|
+
# == Caveats
|
66
68
|
#
|
67
69
|
# An OpenStruct utilizes Ruby's method lookup structure to find and define the
|
68
70
|
# necessary methods for properties. This is accomplished through the methods
|
@@ -71,8 +73,41 @@
|
|
71
73
|
# This should be a consideration if there is a concern about the performance of
|
72
74
|
# the objects that are created, as there is much more overhead in the setting
|
73
75
|
# of these properties compared to using a Hash or a Struct.
|
76
|
+
# Creating an open struct from a small Hash and accessing a few of the
|
77
|
+
# entries can be 200 times slower than accessing the hash directly.
|
78
|
+
#
|
79
|
+
# This is a potential security issue; building OpenStruct from untrusted user data
|
80
|
+
# (e.g. JSON web request) may be susceptible to a "symbol denial of service" attack
|
81
|
+
# since the keys create methods and names of methods are never garbage collected.
|
82
|
+
#
|
83
|
+
# This may also be the source of incompatibilities between Ruby versions:
|
84
|
+
#
|
85
|
+
# o = OpenStruct.new
|
86
|
+
# o.then # => nil in Ruby < 2.6, enumerator for Ruby >= 2.6
|
87
|
+
#
|
88
|
+
# Builtin methods may be overwritten this way, which may be a source of bugs
|
89
|
+
# or security issues:
|
90
|
+
#
|
91
|
+
# o = OpenStruct.new
|
92
|
+
# o.methods # => [:to_h, :marshal_load, :marshal_dump, :each_pair, ...
|
93
|
+
# o.methods = [:foo, :bar]
|
94
|
+
# o.methods # => [:foo, :bar]
|
95
|
+
#
|
96
|
+
# To help remedy clashes, OpenStruct uses only protected/private methods ending with `!`
|
97
|
+
# and defines aliases for builtin public methods by adding a `!`:
|
98
|
+
#
|
99
|
+
# o = OpenStruct.new(make: 'Bentley', class: :luxury)
|
100
|
+
# o.class # => :luxury
|
101
|
+
# o.class! # => OpenStruct
|
102
|
+
#
|
103
|
+
# It is recommended (but not enforced) to not use fields ending in `!`;
|
104
|
+
# Note that a subclass' methods may not be overwritten, nor can OpenStruct's own methods
|
105
|
+
# ending with `!`.
|
106
|
+
#
|
107
|
+
# For all these reasons, consider not using OpenStruct at all.
|
74
108
|
#
|
75
109
|
class OpenStruct
|
110
|
+
VERSION = "0.3.0"
|
76
111
|
|
77
112
|
#
|
78
113
|
# Creates a new OpenStruct object. By default, the resulting OpenStruct
|
@@ -89,31 +124,64 @@ class OpenStruct
|
|
89
124
|
# data # => #<OpenStruct country="Australia", capital="Canberra">
|
90
125
|
#
|
91
126
|
def initialize(hash=nil)
|
92
|
-
@table = {}
|
93
127
|
if hash
|
94
|
-
hash
|
95
|
-
|
96
|
-
|
97
|
-
end
|
128
|
+
update_to_values!(hash)
|
129
|
+
else
|
130
|
+
@table = {}
|
98
131
|
end
|
99
132
|
end
|
100
133
|
|
101
134
|
# Duplicates an OpenStruct object's Hash table.
|
102
|
-
def
|
135
|
+
private def initialize_clone(orig) # :nodoc:
|
136
|
+
super # clones the singleton class for us
|
137
|
+
@table = @table.dup unless @table.frozen?
|
138
|
+
end
|
139
|
+
|
140
|
+
private def initialize_dup(orig) # :nodoc:
|
103
141
|
super
|
104
|
-
@table
|
142
|
+
update_to_values!(@table)
|
143
|
+
end
|
144
|
+
|
145
|
+
private def update_to_values!(hash) # :nodoc:
|
146
|
+
@table = {}
|
147
|
+
hash.each_pair do |k, v|
|
148
|
+
set_ostruct_member_value!(k, v)
|
149
|
+
end
|
105
150
|
end
|
106
151
|
|
152
|
+
#
|
153
|
+
# call-seq:
|
154
|
+
# ostruct.to_h -> hash
|
155
|
+
# ostruct.to_h {|name, value| block } -> hash
|
107
156
|
#
|
108
157
|
# Converts the OpenStruct to a hash with keys representing
|
109
158
|
# each attribute (as symbols) and their corresponding values.
|
110
159
|
#
|
160
|
+
# If a block is given, the results of the block on each pair of
|
161
|
+
# the receiver will be used as pairs.
|
162
|
+
#
|
111
163
|
# require "ostruct"
|
112
164
|
# data = OpenStruct.new("country" => "Australia", :capital => "Canberra")
|
113
165
|
# data.to_h # => {:country => "Australia", :capital => "Canberra" }
|
114
|
-
#
|
115
|
-
|
116
|
-
|
166
|
+
# data.to_h {|name, value| [name.to_s, value.upcase] }
|
167
|
+
# # => {"country" => "AUSTRALIA", "capital" => "CANBERRA" }
|
168
|
+
#
|
169
|
+
if {test: :to_h}.to_h{ [:works, true] }[:works] # RUBY_VERSION < 2.6 compatibility
|
170
|
+
def to_h(&block)
|
171
|
+
if block
|
172
|
+
@table.to_h(&block)
|
173
|
+
else
|
174
|
+
@table.dup
|
175
|
+
end
|
176
|
+
end
|
177
|
+
else
|
178
|
+
def to_h(&block)
|
179
|
+
if block
|
180
|
+
@table.map(&block).to_h
|
181
|
+
else
|
182
|
+
@table.dup
|
183
|
+
end
|
184
|
+
end
|
117
185
|
end
|
118
186
|
|
119
187
|
#
|
@@ -137,83 +205,60 @@ class OpenStruct
|
|
137
205
|
#
|
138
206
|
# Provides marshalling support for use by the Marshal library.
|
139
207
|
#
|
140
|
-
def marshal_dump
|
208
|
+
def marshal_dump # :nodoc:
|
141
209
|
@table
|
142
210
|
end
|
143
211
|
|
144
212
|
#
|
145
213
|
# Provides marshalling support for use by the Marshal library.
|
146
214
|
#
|
147
|
-
def marshal_load(x)
|
215
|
+
def marshal_load(x) # :nodoc:
|
216
|
+
x.each_key{|key| new_ostruct_member!(key)}
|
148
217
|
@table = x
|
149
218
|
end
|
150
219
|
|
151
|
-
#
|
152
|
-
# Used internally to check if the OpenStruct is able to be
|
153
|
-
# modified before granting access to the internal Hash table to be modified.
|
154
|
-
#
|
155
|
-
def modifiable? # :nodoc:
|
156
|
-
begin
|
157
|
-
@modifiable = true
|
158
|
-
rescue
|
159
|
-
exception_class = defined?(FrozenError) ? FrozenError : RuntimeError
|
160
|
-
raise exception_class, "can't modify frozen #{self.class}", caller(3)
|
161
|
-
end
|
162
|
-
@table
|
163
|
-
end
|
164
|
-
private :modifiable?
|
165
|
-
|
166
|
-
# ::Kernel.warn("do not use OpenStruct#modifiable", uplevel: 1)
|
167
|
-
alias modifiable modifiable? # :nodoc:
|
168
|
-
protected :modifiable
|
169
|
-
|
170
220
|
#
|
171
221
|
# Used internally to defined properties on the
|
172
222
|
# OpenStruct. It does this by using the metaprogramming function
|
173
223
|
# define_singleton_method for both the getter method and the setter method.
|
174
224
|
#
|
175
225
|
def new_ostruct_member!(name) # :nodoc:
|
176
|
-
name
|
177
|
-
|
178
|
-
define_singleton_method(name) { @table[name] }
|
179
|
-
define_singleton_method("#{name}=") {|x| modifiable?[name] = x}
|
226
|
+
unless @table.key?(name) || is_method_protected!(name)
|
227
|
+
define_singleton_method!(name) { @table[name] }
|
228
|
+
define_singleton_method!("#{name}=") {|x| @table[name] = x}
|
180
229
|
end
|
181
|
-
name
|
182
230
|
end
|
183
231
|
private :new_ostruct_member!
|
184
232
|
|
185
|
-
|
186
|
-
|
187
|
-
|
233
|
+
private def is_method_protected!(name) # :nodoc:
|
234
|
+
if !respond_to?(name, true)
|
235
|
+
false
|
236
|
+
elsif name.match?(/!$/)
|
237
|
+
true
|
238
|
+
else
|
239
|
+
method!(name).owner < OpenStruct
|
240
|
+
end
|
241
|
+
end
|
188
242
|
|
189
243
|
def freeze
|
190
|
-
@table.
|
244
|
+
@table.freeze
|
191
245
|
super
|
192
246
|
end
|
193
247
|
|
194
|
-
def
|
195
|
-
mname = mid.to_s.chomp("=").to_sym
|
196
|
-
@table&.key?(mname) || super
|
197
|
-
end
|
198
|
-
|
199
|
-
def method_missing(mid, *args) # :nodoc:
|
248
|
+
private def method_missing(mid, *args) # :nodoc:
|
200
249
|
len = args.length
|
201
250
|
if mname = mid[/.*(?==\z)/m]
|
202
251
|
if len != 1
|
203
|
-
raise ArgumentError, "wrong number of arguments (#{len}
|
204
|
-
end
|
205
|
-
modifiable?[new_ostruct_member!(mname)] = args[0]
|
206
|
-
elsif len == 0 # and /\A[a-z_]\w*\z/ =~ mid #
|
207
|
-
if @table.key?(mid)
|
208
|
-
new_ostruct_member!(mid) unless frozen?
|
209
|
-
@table[mid]
|
252
|
+
raise! ArgumentError, "wrong number of arguments (given #{len}, expected 1)", caller(1)
|
210
253
|
end
|
254
|
+
set_ostruct_member_value!(mname, args[0])
|
255
|
+
elsif len == 0
|
211
256
|
else
|
212
257
|
begin
|
213
258
|
super
|
214
259
|
rescue NoMethodError => err
|
215
260
|
err.backtrace.shift
|
216
|
-
raise
|
261
|
+
raise!
|
217
262
|
end
|
218
263
|
end
|
219
264
|
end
|
@@ -222,7 +267,7 @@ class OpenStruct
|
|
222
267
|
# :call-seq:
|
223
268
|
# ostruct[name] -> object
|
224
269
|
#
|
225
|
-
# Returns the value of an attribute.
|
270
|
+
# Returns the value of an attribute, or `nil` if there is no such attribute.
|
226
271
|
#
|
227
272
|
# require "ostruct"
|
228
273
|
# person = OpenStruct.new("name" => "John Smith", "age" => 70)
|
@@ -244,34 +289,32 @@ class OpenStruct
|
|
244
289
|
# person.age # => 42
|
245
290
|
#
|
246
291
|
def []=(name, value)
|
247
|
-
|
292
|
+
name = name.to_sym
|
293
|
+
new_ostruct_member!(name)
|
294
|
+
@table[name] = value
|
248
295
|
end
|
296
|
+
alias_method :set_ostruct_member_value!, :[]=
|
297
|
+
private :set_ostruct_member_value!
|
249
298
|
|
250
|
-
#
|
251
299
|
# :call-seq:
|
252
|
-
# ostruct.dig(name,
|
300
|
+
# ostruct.dig(name, *identifiers) -> object
|
253
301
|
#
|
254
|
-
#
|
255
|
-
#
|
256
|
-
#
|
302
|
+
# Finds and returns the object in nested objects
|
303
|
+
# that is specified by +name+ and +identifiers+.
|
304
|
+
# The nested objects may be instances of various classes.
|
305
|
+
# See {Dig Methods}[rdoc-ref:doc/dig_methods.rdoc].
|
257
306
|
#
|
307
|
+
# Examples:
|
258
308
|
# require "ostruct"
|
259
309
|
# address = OpenStruct.new("city" => "Anytown NC", "zip" => 12345)
|
260
310
|
# person = OpenStruct.new("name" => "John Smith", "address" => address)
|
261
|
-
#
|
262
|
-
# person.dig(:
|
263
|
-
# person.dig(:business_address, "zip") # => nil
|
264
|
-
#
|
265
|
-
# data = OpenStruct.new(:array => [1, [2, 3]])
|
266
|
-
#
|
267
|
-
# data.dig(:array, 1, 0) # => 2
|
268
|
-
# data.dig(:array, 0, 0) # TypeError: Integer does not have #dig method
|
269
|
-
#
|
311
|
+
# person.dig(:address, "zip") # => 12345
|
312
|
+
# person.dig(:business_address, "zip") # => nil
|
270
313
|
def dig(name, *names)
|
271
314
|
begin
|
272
315
|
name = name.to_sym
|
273
316
|
rescue NoMethodError
|
274
|
-
raise TypeError, "#{name} is not a symbol nor a string"
|
317
|
+
raise! TypeError, "#{name} is not a symbol nor a string"
|
275
318
|
end
|
276
319
|
@table.dig(name, *names)
|
277
320
|
end
|
@@ -284,7 +327,7 @@ class OpenStruct
|
|
284
327
|
#
|
285
328
|
# person = OpenStruct.new(name: "John", age: 70, pension: 300)
|
286
329
|
#
|
287
|
-
# person.delete_field("age")
|
330
|
+
# person.delete_field!("age") # => 70
|
288
331
|
# person # => #<OpenStruct name="John", pension=300>
|
289
332
|
#
|
290
333
|
# Setting the value to +nil+ will not remove the attribute:
|
@@ -299,7 +342,7 @@ class OpenStruct
|
|
299
342
|
rescue NameError
|
300
343
|
end
|
301
344
|
@table.delete(sym) do
|
302
|
-
raise NameError.new("no field `#{sym}' in #{self}", sym)
|
345
|
+
raise! NameError.new("no field `#{sym}' in #{self}", sym)
|
303
346
|
end
|
304
347
|
end
|
305
348
|
|
@@ -322,13 +365,13 @@ class OpenStruct
|
|
322
365
|
ids.pop
|
323
366
|
end
|
324
367
|
end
|
325
|
-
['#<', self.class
|
368
|
+
['#<', self.class!, detail, '>'].join
|
326
369
|
end
|
327
370
|
alias :to_s :inspect
|
328
371
|
|
329
372
|
attr_reader :table # :nodoc:
|
330
|
-
protected :table
|
331
373
|
alias table! table
|
374
|
+
protected :table!
|
332
375
|
|
333
376
|
#
|
334
377
|
# Compares this object and +other+ for equality. An OpenStruct is equal to
|
@@ -359,11 +402,43 @@ class OpenStruct
|
|
359
402
|
end
|
360
403
|
|
361
404
|
# Computes a hash code for this OpenStruct.
|
362
|
-
|
363
|
-
# (and will compare using #eql?).
|
364
|
-
#
|
365
|
-
# See also Object#hash.
|
366
|
-
def hash
|
405
|
+
def hash # :nodoc:
|
367
406
|
@table.hash
|
368
407
|
end
|
408
|
+
|
409
|
+
#
|
410
|
+
# Provides marshalling support for use by the YAML library.
|
411
|
+
#
|
412
|
+
def encode_with(coder) # :nodoc:
|
413
|
+
@table.each_pair do |key, value|
|
414
|
+
coder[key.to_s] = value
|
415
|
+
end
|
416
|
+
if @table.size == 1 && @table.key?(:table) # support for legacy format
|
417
|
+
# in the very unlikely case of a single entry called 'table'
|
418
|
+
coder['legacy_support!'] = true # add a bogus second entry
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
#
|
423
|
+
# Provides marshalling support for use by the YAML library.
|
424
|
+
#
|
425
|
+
def init_with(coder) # :nodoc:
|
426
|
+
h = coder.map
|
427
|
+
if h.size == 1 # support for legacy format
|
428
|
+
key, val = h.first
|
429
|
+
if key == 'table'
|
430
|
+
h = val
|
431
|
+
end
|
432
|
+
end
|
433
|
+
update_to_values!(h)
|
434
|
+
end
|
435
|
+
|
436
|
+
# Make all public methods (builtin or our own) accessible with `!`:
|
437
|
+
instance_methods.each do |method|
|
438
|
+
new_name = "#{method}!"
|
439
|
+
alias_method new_name, method
|
440
|
+
end
|
441
|
+
# Other builtin private methods we use:
|
442
|
+
alias_method :raise!, :raise
|
443
|
+
private :raise!
|
369
444
|
end
|
data/ostruct.gemspec
CHANGED
@@ -1,8 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
name = File.basename(__FILE__, ".gemspec")
|
4
|
+
version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir|
|
5
|
+
break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
|
6
|
+
/^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
|
7
|
+
end rescue nil
|
8
|
+
end
|
9
|
+
|
3
10
|
Gem::Specification.new do |spec|
|
4
|
-
spec.name =
|
5
|
-
spec.version =
|
11
|
+
spec.name = name
|
12
|
+
spec.version = version
|
6
13
|
spec.authors = ["Marc-Andre Lafortune"]
|
7
14
|
spec.email = ["ruby-core@marc-andre.ca"]
|
8
15
|
|
@@ -10,8 +17,9 @@ Gem::Specification.new do |spec|
|
|
10
17
|
spec.description = %q{Class to build custom data structures, similar to a Hash.}
|
11
18
|
spec.homepage = "https://github.com/ruby/ostruct"
|
12
19
|
spec.license = "BSD-2-Clause"
|
20
|
+
spec.required_ruby_version = ">= 2.5.0"
|
13
21
|
|
14
|
-
spec.files = [".gitignore", "
|
22
|
+
spec.files = [".gitignore", "Gemfile", "LICENSE.txt", "README.md", "Rakefile", "bin/console", "bin/setup", "lib/ostruct.rb", "ostruct.gemspec"]
|
15
23
|
spec.bindir = "exe"
|
16
24
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
17
25
|
spec.require_paths = ["lib"]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ostruct
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc-Andre Lafortune
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -46,7 +46,6 @@ extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
48
|
- ".gitignore"
|
49
|
-
- ".travis.yml"
|
50
49
|
- Gemfile
|
51
50
|
- LICENSE.txt
|
52
51
|
- README.md
|
@@ -59,7 +58,7 @@ homepage: https://github.com/ruby/ostruct
|
|
59
58
|
licenses:
|
60
59
|
- BSD-2-Clause
|
61
60
|
metadata: {}
|
62
|
-
post_install_message:
|
61
|
+
post_install_message:
|
63
62
|
rdoc_options: []
|
64
63
|
require_paths:
|
65
64
|
- lib
|
@@ -67,16 +66,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
66
|
requirements:
|
68
67
|
- - ">="
|
69
68
|
- !ruby/object:Gem::Version
|
70
|
-
version:
|
69
|
+
version: 2.5.0
|
71
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
71
|
requirements:
|
73
72
|
- - ">="
|
74
73
|
- !ruby/object:Gem::Version
|
75
74
|
version: '0'
|
76
75
|
requirements: []
|
77
|
-
|
78
|
-
|
79
|
-
signing_key:
|
76
|
+
rubygems_version: 3.0.8
|
77
|
+
signing_key:
|
80
78
|
specification_version: 4
|
81
79
|
summary: Class to build custom data structures, similar to a Hash.
|
82
80
|
test_files: []
|