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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/ostruct.rb +157 -82
  4. data/ostruct.gemspec +11 -3
  5. metadata +7 -9
  6. data/.travis.yml +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d125c51b4a6541c0dcb5422065b685c156e847218380e2c01f1ef8fac08e40dc
4
- data.tar.gz: 7c5aa451998687071f22794e2e24ea644e7a6698e4eebe608ccd42a6cdf3199c
3
+ metadata.gz: 44161760b6aef61a50eddb473940b3a23c75682afbd4e9573124ee4ae0afbf50
4
+ data.tar.gz: 757b50ba4015029f3fcacaa00e94b685eff032d65ea894e0523de585903e3775
5
5
  SHA512:
6
- metadata.gz: c58234c3f25e5eee4f47faf91c0016051e9bd72016d38a54248b1695424016d4cb46be9a09ef29837cd268cac57f0c7eebe1e5c330d218c580b304f1210764bc
7
- data.tar.gz: 3698d2ab77b8c9904b43143795226e9f3abce36a55fc2923fc7ed65f4cf48d60790ec55e2f43d6bfb75c0f010aa23b32c08d73c00f6468420f4fc8131dce22dc
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 ccompanying values. This is accomplished by using Ruby's metaprogramming to define methods on the class itself.
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
 
@@ -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
- # == Implementation
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.each_pair do |k, v|
95
- k = k.to_sym
96
- @table[k] = v
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 initialize_copy(orig) # :nodoc:
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 = @table.dup
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
- def to_h
116
- @table.dup
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 = name.to_sym
177
- unless singleton_class.method_defined?(name)
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
- # ::Kernel.warn("do not use OpenStruct#new_ostruct_member", uplevel: 1)
186
- alias new_ostruct_member new_ostruct_member! # :nodoc:
187
- protected :new_ostruct_member
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.each_key {|key| new_ostruct_member!(key)}
244
+ @table.freeze
191
245
  super
192
246
  end
193
247
 
194
- def respond_to_missing?(mid, include_private = false) # :nodoc:
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} for 1)", caller(1)
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
- modifiable?[new_ostruct_member!(name)] = value
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, ...) -> object
300
+ # ostruct.dig(name, *identifiers) -> object
253
301
  #
254
- # Extracts the nested value specified by the sequence of +name+
255
- # objects by calling +dig+ at each step, returning +nil+ if any
256
- # intermediate step is +nil+.
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(:address, "zip") # => 12345
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") # => 70
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, detail, '>'].join
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
- # Two OpenStruct objects with the same content will have the same hash code
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
@@ -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 = "ostruct"
5
- spec.version = "0.1.0"
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", ".travis.yml", "Gemfile", "LICENSE.txt", "README.md", "Rakefile", "bin/console", "bin/setup", "lib/ostruct.rb", "ostruct.gemspec"]
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.1.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: 2018-12-04 00:00:00.000000000 Z
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: '0'
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
- rubyforge_project:
78
- rubygems_version: 2.7.6
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: []
@@ -1,6 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.5.1
5
- - ruby-head
6
- before_install: gem install bundler -v 1.15.4