ostruct 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Version](https://badge.fury.io/rb/ostruct.svg)](https://badge.fury.io/rb/ostruct) [![Default Gem](https://img.shields.io/badge/stdgem-default-9c1260.svg)](https://stdgems.org/ostruct/) [![Test](https://github.com/ruby/ostruct/workflows/test/badge.svg)](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: []
|