ostruct 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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