dry-struct 0.6.0 → 1.4.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.
@@ -1,10 +1,13 @@
1
- require 'dry/core/class_attributes'
2
- require 'dry/core/inflector'
3
- require 'dry/core/descendants_tracker'
1
+ # frozen_string_literal: true
4
2
 
5
- require 'dry/struct/errors'
6
- require 'dry/struct/constructor'
7
- require 'dry/struct/sum'
3
+ require "weakref"
4
+ require "dry/core/class_attributes"
5
+ require "dry/core/inflector"
6
+ require "dry/core/descendants_tracker"
7
+
8
+ require "dry/struct/errors"
9
+ require "dry/struct/constructor"
10
+ require "dry/struct/sum"
8
11
 
9
12
  module Dry
10
13
  class Struct
@@ -12,21 +15,15 @@ module Dry
12
15
  module ClassInterface
13
16
  include Core::ClassAttributes
14
17
 
15
- include Dry::Types::Type
16
- include Dry::Types::Builder
18
+ include Types::Type
19
+ include Types::Builder
17
20
 
18
21
  # @param [Class] klass
19
22
  def inherited(klass)
20
23
  super
21
24
 
22
- base = self
23
-
24
- klass.class_eval do
25
- @meta = base.meta
26
-
27
- unless equal?(Value)
28
- extend Dry::Core::DescendantsTracker
29
- end
25
+ unless klass.name.eql?("Dry::Struct::Value")
26
+ klass.extend(Core::DescendantsTracker)
30
27
  end
31
28
  end
32
29
 
@@ -34,7 +31,7 @@ module Dry
34
31
  # and modifies {.schema} accordingly.
35
32
  #
36
33
  # @param [Symbol] name name of the defined attribute
37
- # @param [Dry::Types::Definition, nil] type or superclass of nested type
34
+ # @param [Dry::Types::Type, nil] type or superclass of nested type
38
35
  # @return [Dry::Struct]
39
36
  # @yield
40
37
  # If a block is given, it will be evaluated in the context of
@@ -53,10 +50,7 @@ module Dry
53
50
  # end
54
51
  #
55
52
  # Language.schema
56
- # #=> {
57
- # :name=>#<Dry::Types::Definition primitive=String options={} meta={}>,
58
- # :details=>Language::Details
59
- # }
53
+ # # => #<Dry::Types[Constructor<Schema<keys={name: Constrained<Nominal<String> rule=[type?(String)]> details: Language::Details}> fn=Kernel.Hash>]>
60
54
  #
61
55
  # ruby = Language.new(name: 'Ruby', details: { type: 'OO' })
62
56
  # ruby.name #=> 'Ruby'
@@ -66,19 +60,19 @@ module Dry
66
60
  # @example with a nested array of structs
67
61
  # class Language < Dry::Struct
68
62
  # attribute :name, Types::String
69
- # array :versions, Types::String
70
- # array :celebrities, Types::Array.of(Dry::Struct) do
63
+ # attribute :versions, Types::Array.of(Types::String)
64
+ # attribute :celebrities, Types::Array.of(Dry::Struct) do
71
65
  # attribute :name, Types::String
72
66
  # attribute :pseudonym, Types::String
73
67
  # end
74
68
  # end
75
69
  #
76
70
  # Language.schema
77
- # #=> {
78
- # :name=>#<Dry::Types::Definition primitive=String options={} meta={}>,
79
- # :versions=>#<Dry::Types::Array::Member primitive=Array options={:member=>#<Dry::Types::Definition primitive=String options={} meta={}>} meta={}>,
80
- # :celebrities=>#<Dry::Types::Array::Member primitive=Array options={:member=>Language::Celebrity} meta={}>
81
- # }
71
+ # => #<Dry::Types[Constructor<Schema<keys={
72
+ # name: Constrained<Nominal<String> rule=[type?(String)]>
73
+ # versions: Constrained<Array<Constrained<Nominal<String> rule=[type?(String)]>> rule=[type?(Array)]>
74
+ # celebrities: Constrained<Array<Language::Celebrity> rule=[type?(Array)]>
75
+ # }> fn=Kernel.Hash>]>
82
76
  #
83
77
  # ruby = Language.new(
84
78
  # name: 'Ruby',
@@ -99,41 +93,76 @@ module Dry
99
93
  # ruby.celebrities[0].pseudonym #=> 'Matz'
100
94
  # ruby.celebrities[1].name #=> 'Aaron Patterson'
101
95
  # ruby.celebrities[1].pseudonym #=> 'tenderlove'
102
- def attribute(name, type = nil, &block)
96
+ def attribute(name, type = Undefined, &block)
103
97
  attributes(name => build_type(name, type, &block))
104
98
  end
105
99
 
100
+ # Add atributes from another struct
101
+ #
102
+ # @example
103
+ # class Address < Dry::Struct
104
+ # attribute :city, Types::String
105
+ # attribute :country, Types::String
106
+ # end
107
+ #
108
+ # class User < Dry::Struct
109
+ # attribute :name, Types::String
110
+ # attributes_from Address
111
+ # end
112
+ #
113
+ # User.new(name: 'Quispe', city: 'La Paz', country: 'Bolivia')
114
+ #
115
+ # @example with nested structs
116
+ # class User < Dry::Struct
117
+ # attribute :name, Types::String
118
+ # attribute :address do
119
+ # attributes_from Address
120
+ # end
121
+ # end
122
+ #
123
+ # @param struct [Dry::Struct]
124
+ def attributes_from(struct)
125
+ extracted_schema = struct.schema.keys.map { |key|
126
+ if key.required?
127
+ [key.name, key.type]
128
+ else
129
+ [:"#{key.name}?", key.type]
130
+ end
131
+ }.to_h
132
+ attributes(extracted_schema)
133
+ end
134
+
106
135
  # Adds an omittable (key is not required on initialization) attribute for this {Struct}
107
136
  #
108
137
  # @example
109
138
  # class User < Dry::Struct
110
- # attribute :name, Types::Strict::String
111
- # attribute? :email, Types::Strict::String
139
+ # attribute :name, Types::String
140
+ # attribute? :email, Types::String
112
141
  # end
113
142
  #
114
- # User.new(name: 'John') # => #<User name="John">
143
+ # User.new(name: 'John') # => #<User name="John" email=nil>
115
144
  #
116
145
  # @param [Symbol] name name of the defined attribute
117
- # @param [Dry::Types::Definition, nil] type or superclass of nested type
146
+ # @param [Dry::Types::Type, nil] type or superclass of nested type
118
147
  # @return [Dry::Struct]
119
148
  #
120
149
  def attribute?(*args, &block)
121
150
  if args.size == 1 && block.nil?
122
- Dry::Core::Deprecations.warn(
123
- 'Dry::Struct.attribute? is deprecated for checking attribute presence, '\
124
- 'use has_attribute? instead',
151
+ Core::Deprecations.warn(
152
+ "Dry::Struct.attribute? is deprecated for checking attribute presence, "\
153
+ "use has_attribute? instead",
125
154
  tag: :'dry-struct'
126
155
  )
127
156
 
128
157
  has_attribute?(args[0])
129
158
  else
130
- name, type = args
159
+ name, * = args
131
160
 
132
- attribute(name, build_type(name, type, &block).meta(omittable: true))
161
+ attribute(:"#{name}?", build_type(*args, &block))
133
162
  end
134
163
  end
135
164
 
136
- # @param [Hash{Symbol => Dry::Types::Definition}] new_schema
165
+ # @param [Hash{Symbol => Dry::Types::Type}] new_schema
137
166
  # @return [Dry::Struct]
138
167
  # @raise [RepeatedAttributeError] when trying to define attribute with the
139
168
  # same name as previously defined one
@@ -147,27 +176,25 @@ module Dry
147
176
  # end
148
177
  #
149
178
  # Book.schema
150
- # #=> {title: #<Dry::Types::Definition primitive=String options={}>,
151
- # # author: #<Dry::Types::Definition primitive=String options={}>}
179
+ # # => #<Dry::Types[Constructor<Schema<keys={
180
+ # # title: Constrained<Nominal<String> rule=[type?(String)]>
181
+ # # author: Constrained<Nominal<String> rule=[type?(String)]>
182
+ # # }> fn=Kernel.Hash>]>
152
183
  def attributes(new_schema)
153
- check_schema_duplication(new_schema)
184
+ keys = new_schema.keys.map { |k| k.to_s.chomp("?").to_sym }
185
+ check_schema_duplication(keys)
154
186
 
155
- input input.schema(new_schema)
187
+ schema schema.schema(new_schema)
156
188
 
157
- new_schema.each_key do |key|
158
- next if instance_methods.include?(key)
159
- class_eval(<<-RUBY)
160
- def #{ key }
161
- @attributes[#{ key.inspect }]
162
- end
163
- RUBY
164
- end
189
+ define_accessors(keys)
165
190
 
166
191
  @attribute_names = nil
167
192
 
168
- descendants.
169
- select { |d| d.superclass == self }.
170
- each { |d| d.attributes(new_schema.reject { |k, _| d.schema.key?(k) }) }
193
+ direct_descendants = descendants.select { |d| d.superclass == self }
194
+ direct_descendants.each do |d|
195
+ inherited_attrs = new_schema.reject { |k, _| d.has_attribute?(k.to_s.chomp("?").to_sym) }
196
+ d.attributes(inherited_attrs)
197
+ end
171
198
 
172
199
  self
173
200
  end
@@ -180,13 +207,13 @@ module Dry
180
207
  # class Book < Dry::Struct
181
208
  # transform_types { |t| t.meta(struct: :Book) }
182
209
  #
183
- # attribute :title, Types::Strict::String
210
+ # attribute :title, Types::String
184
211
  # end
185
212
  #
186
- # Book.schema[:title].meta # => { struct: :Book }
213
+ # Book.schema.key(:title).meta # => { struct: :Book }
187
214
  #
188
215
  def transform_types(proc = nil, &block)
189
- input input.with_type_transform(proc || block)
216
+ schema schema.with_type_transform(proc || block)
190
217
  end
191
218
 
192
219
  # Add an arbitrary transformation for input hash keys.
@@ -197,54 +224,80 @@ module Dry
197
224
  # class Book < Dry::Struct
198
225
  # transform_keys(&:to_sym)
199
226
  #
200
- # attribute :title, Types::Strict::String
227
+ # attribute :title, Types::String
201
228
  # end
202
229
  #
203
230
  # Book.new('title' => "The Old Man and the Sea")
204
231
  # # => #<Book title="The Old Man and the Sea">
205
232
  def transform_keys(proc = nil, &block)
206
- input input.with_key_transform(proc || block)
233
+ schema schema.with_key_transform(proc || block)
207
234
  end
208
235
 
209
- # @param [Hash{Symbol => Dry::Types::Definition, Dry::Struct}] new_schema
236
+ # @param [Hash{Symbol => Dry::Types::Type, Dry::Struct}] new_keys
210
237
  # @raise [RepeatedAttributeError] when trying to define attribute with the
211
238
  # same name as previously defined one
212
- def check_schema_duplication(new_schema)
213
- shared_keys = new_schema.keys & (schema.keys - superclass.schema.keys)
239
+ def check_schema_duplication(new_keys)
240
+ overlapping_keys = new_keys & (attribute_names - superclass.attribute_names)
214
241
 
215
- raise RepeatedAttributeError, shared_keys.first if shared_keys.any?
242
+ if overlapping_keys.any?
243
+ raise RepeatedAttributeError, overlapping_keys.first
244
+ end
216
245
  end
217
246
  private :check_schema_duplication
218
247
 
219
248
  # @param [Hash{Symbol => Object},Dry::Struct] attributes
220
249
  # @raise [Struct::Error] if the given attributes don't conform {#schema}
221
- def new(attributes = default_attributes)
222
- if attributes.instance_of?(self)
223
- attributes
250
+ def new(attributes = default_attributes, safe = false, &block)
251
+ if attributes.is_a?(Struct)
252
+ if equal?(attributes.class)
253
+ attributes
254
+ else
255
+ # This implicit coercion is arguable but makes sense overall
256
+ # in cases there you pass child struct to the base struct constructor
257
+ # User.new(super_user)
258
+ #
259
+ # We may deprecate this behavior in future forcing people to be explicit
260
+ new(attributes.to_h, safe, &block)
261
+ end
262
+ elsif safe
263
+ load(schema.call_safe(attributes) { |output = attributes| return yield output })
224
264
  else
225
- super(input[attributes])
265
+ load(schema.call_unsafe(attributes))
226
266
  end
227
- rescue Types::SchemaError, Types::MissingKeyError, Types::UnknownKeysError => error
228
- raise Struct::Error, "[#{self}.new] #{error}"
267
+ rescue Types::CoercionError => e
268
+ raise Error, "[#{self}.new] #{e}", e.backtrace
229
269
  end
230
270
 
231
- # Calls type constructor. The behavior is identical to `.new` but returns
232
- # the input back if it's a subclass of the struct.
233
- #
234
- # @param [Hash{Symbol => Object},Dry::Struct] attributes
235
- # @return [Dry::Struct]
236
- def call(attributes = default_attributes)
237
- return attributes if attributes.is_a?(self)
238
- new(attributes)
271
+ # @api private
272
+ def call_safe(input, &block)
273
+ if input.is_a?(self)
274
+ input
275
+ else
276
+ new(input, true, &block)
277
+ end
278
+ end
279
+
280
+ # @api private
281
+ def call_unsafe(input)
282
+ if input.is_a?(self)
283
+ input
284
+ else
285
+ new(input)
286
+ end
287
+ end
288
+
289
+ # @api private
290
+ def load(attributes)
291
+ struct = allocate
292
+ struct.__send__(:initialize, attributes)
293
+ struct
239
294
  end
240
- alias_method :[], :call
241
295
 
242
296
  # @param [#call,nil] constructor
243
- # @param [Hash] _options
244
297
  # @param [#call,nil] block
245
298
  # @return [Dry::Struct::Constructor]
246
- def constructor(constructor = nil, **_options, &block)
247
- Struct::Constructor.new(self, fn: constructor || block)
299
+ def constructor(constructor = nil, **, &block)
300
+ Constructor[self, fn: constructor || block]
248
301
  end
249
302
 
250
303
  # @param [Hash{Symbol => Object},Dry::Struct] input
@@ -252,10 +305,10 @@ module Dry
252
305
  # @yieldreturn [Dry::Types::ResultResult]
253
306
  # @return [Dry::Types::Result]
254
307
  def try(input)
255
- Types::Result::Success.new(self[input])
256
- rescue Struct::Error => e
257
- failure = Types::Result::Failure.new(input, e.message)
258
- block_given? ? yield(failure) : failure
308
+ success(self[input])
309
+ rescue Error => e
310
+ failure_result = failure(input, e.message)
311
+ block_given? ? yield(failure_result) : failure_result
259
312
  end
260
313
 
261
314
  # @param [Hash{Symbol => Object},Dry::Struct] input
@@ -263,7 +316,7 @@ module Dry
263
316
  # @private
264
317
  def try_struct(input)
265
318
  if input.is_a?(self)
266
- Types::Result::Success.new(input)
319
+ input
267
320
  else
268
321
  yield
269
322
  end
@@ -278,7 +331,7 @@ module Dry
278
331
  # @param [({Symbol => Object})] args
279
332
  # @return [Dry::Types::Result::Failure]
280
333
  def failure(*args)
281
- result(::Dry::Types::Result::Failure, *args)
334
+ result(Types::Result::Failure, *args)
282
335
  end
283
336
 
284
337
  # @param [Class] klass
@@ -292,11 +345,12 @@ module Dry
292
345
  false
293
346
  end
294
347
 
295
- # @param [Object, Dry::Struct] value
348
+ # @param [Object, Dry::Struct] other
296
349
  # @return [Boolean]
297
- def valid?(value)
298
- self === value
350
+ def ===(other)
351
+ other.is_a?(self)
299
352
  end
353
+ alias_method :primitive?, :===
300
354
 
301
355
  # @return [true]
302
356
  def constrained?
@@ -313,6 +367,11 @@ module Dry
313
367
  false
314
368
  end
315
369
 
370
+ # @return [Proc]
371
+ def to_proc
372
+ @to_proc ||= proc { |input| call(input) }
373
+ end
374
+
316
375
  # Checks if this {Struct} has the given attribute
317
376
  #
318
377
  # @param [Symbol] key Attribute name
@@ -321,25 +380,22 @@ module Dry
321
380
  schema.key?(key)
322
381
  end
323
382
 
324
- # @return [Hash{Symbol => Dry::Types::Definition, Dry::Struct}]
325
- def schema
326
- input.member_types
327
- end
328
-
329
383
  # Gets the list of attribute names
330
384
  #
331
385
  # @return [Array<Symbol>]
332
386
  def attribute_names
333
- @attribute_names ||= schema.keys
387
+ @attribute_names ||= schema.map(&:name)
334
388
  end
335
389
 
336
390
  # @return [{Symbol => Object}]
337
391
  def meta(meta = Undefined)
338
392
  if meta.equal?(Undefined)
339
- @meta
393
+ schema.meta
394
+ elsif meta.empty?
395
+ self
340
396
  else
341
- Class.new(self) do
342
- @meta = @meta.merge(meta) unless meta.empty?
397
+ ::Class.new(self) do
398
+ schema schema.meta(meta) unless meta.empty?
343
399
  end
344
400
  end
345
401
  end
@@ -348,13 +404,28 @@ module Dry
348
404
  # @param [Dry::Types::Type] type
349
405
  # @return [Dry::Types::Sum]
350
406
  def |(type)
351
- if type.is_a?(Class) && type <= Struct
352
- Struct::Sum.new(self, type)
407
+ if type.is_a?(::Class) && type <= Struct
408
+ Sum.new(self, type)
353
409
  else
354
410
  super
355
411
  end
356
412
  end
357
413
 
414
+ # Make the struct abstract. This class will be used as a default
415
+ # parent class for nested structs
416
+ def abstract
417
+ abstract_class self
418
+ end
419
+
420
+ # Dump to the AST
421
+ #
422
+ # @return [Array]
423
+ #
424
+ # @api public
425
+ def to_ast(meta: true)
426
+ [:struct, [::WeakRef.new(self), schema.to_ast(meta: meta)]]
427
+ end
428
+
358
429
  # Stores an object for building nested struct classes
359
430
  # @return [StructBuilder]
360
431
  def struct_builder
@@ -367,32 +438,32 @@ module Dry
367
438
  #
368
439
  # @return [Hash{Symbol => Object}]
369
440
  def default_attributes(default_schema = schema)
370
- default_schema.each_with_object({}) do |(name, type), result|
371
- result[name] = default_attributes(type.schema) if struct?(type)
441
+ default_schema.each_with_object({}) do |key, result|
442
+ result[key.name] = default_attributes(key.schema) if struct?(key.type)
372
443
  end
373
444
  end
374
445
  private :default_attributes
375
446
 
376
447
  # Checks if the given type is a Dry::Struct
377
448
  #
378
- # @param [Dry::Types::Definition, Dry::Struct] type
449
+ # @param [Dry::Types::Type] type
379
450
  # @return [Boolean]
380
451
  def struct?(type)
381
- type.is_a?(Class) && type <= Struct
452
+ type.is_a?(::Class) && type <= Struct
382
453
  end
383
454
  private :struct?
384
455
 
385
456
  # Constructs a type
386
457
  #
387
458
  # @return [Dry::Types::Type, Dry::Struct]
388
- def build_type(name, type, &block)
459
+ def build_type(name, type = Undefined, &block)
389
460
  type_object =
390
- if type.is_a?(String)
391
- Dry::Types[type]
392
- elsif block.nil? && type.nil?
461
+ if type.is_a?(::String)
462
+ Types[type]
463
+ elsif block.nil? && Undefined.equal?(type)
393
464
  raise(
394
- ArgumentError,
395
- 'you must supply a type or a block to `Dry::Struct.attribute`'
465
+ ::ArgumentError,
466
+ "you must supply a type or a block to `Dry::Struct.attribute`"
396
467
  )
397
468
  else
398
469
  type
@@ -405,6 +476,19 @@ module Dry
405
476
  end
406
477
  end
407
478
  private :build_type
479
+
480
+ def define_accessors(keys)
481
+ keys.each do |key|
482
+ next if instance_methods.include?(key)
483
+
484
+ class_eval(<<-RUBY)
485
+ def #{key}
486
+ @attributes[#{key.inspect}]
487
+ end
488
+ RUBY
489
+ end
490
+ end
491
+ private :define_accessors
408
492
  end
409
493
  end
410
494
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weakref"
4
+ require "dry/types/compiler"
5
+
6
+ module Dry
7
+ class Struct
8
+ class Compiler < Types::Compiler
9
+ def visit_struct(node)
10
+ struct, _ = node
11
+
12
+ struct.__getobj__
13
+ rescue ::WeakRef::RefError
14
+ if struct.weakref_alive?
15
+ raise
16
+ else
17
+ raise RecycledStructError
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,29 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  class Struct
3
- class Constructor
4
- include Dry::Equalizer(:type)
5
- include Dry::Types::Type
6
-
7
- # @return [#call]
8
- attr_reader :fn
9
-
10
- # @return [#call]
11
- attr_reader :type
12
-
13
- # @param [Struct] type
14
- # @param [Hash] options
15
- # @param [#call, nil] block
16
- def initialize(type, options = {}, &block)
17
- @type = type
18
- @fn = options.fetch(:fn, block)
19
- end
20
-
21
- # @param [Object] input
22
- # @return [Object]
23
- def call(input)
24
- type[fn[input]]
25
- end
26
- alias_method :[], :call
5
+ class Constructor < Types::Constructor
6
+ alias_method :primitive, :type
27
7
  end
28
8
  end
29
9
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  class Struct
3
5
  # Raised when given input doesn't conform schema and constructor type
4
6
  Error = Class.new(TypeError)
5
7
 
6
8
  # Raised when defining duplicate attributes
7
- class RepeatedAttributeError < ArgumentError
9
+ class RepeatedAttributeError < ::ArgumentError
8
10
  # @param [Symbol] key
9
11
  # attribute name that is the same as previously defined one
10
12
  def initialize(key)
@@ -13,9 +15,17 @@ module Dry
13
15
  end
14
16
 
15
17
  # Raised when a struct doesn't have an attribute
16
- class MissingAttributeError < KeyError
18
+ class MissingAttributeError < ::KeyError
17
19
  def initialize(key)
18
- super("Missing attribute: #{ key.inspect }")
20
+ super("Missing attribute: #{key.inspect}")
21
+ end
22
+ end
23
+
24
+ # When struct class stored in ast was garbage collected because no alive objects exists
25
+ # This shouldn't happen in a working application
26
+ class RecycledStructError < ::RuntimeError
27
+ def initialize
28
+ super("Reference to struct class was garbage collected")
19
29
  end
20
30
  end
21
31
  end
@@ -1,16 +1,18 @@
1
- require 'pp'
1
+ # frozen_string_literal: true
2
+
3
+ require "pp"
2
4
 
3
5
  module Dry
4
6
  class Struct
5
7
  def pretty_print(pp)
6
8
  klass = self.class
7
- pp.group(1, "#<#{ klass.name || klass.inspect }", '>') do
8
- pp.seplist(@attributes.keys, proc { pp.text ',' }) do |column_name|
9
+ pp.group(1, "#<#{klass.name || klass.inspect}", ">") do
10
+ pp.seplist(@attributes.keys, proc { pp.text "," }) do |column_name|
9
11
  column_value = @attributes[column_name]
10
- pp.breakable ' '
12
+ pp.breakable " "
11
13
  pp.group(1) do
12
14
  pp.text column_name
13
- pp.text '='
15
+ pp.text "="
14
16
  pp.pp column_value
15
17
  end
16
18
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Dry::Struct.register_extension(:pretty_print) do
2
- require 'dry/struct/extensions/pretty_print'
4
+ require "dry/struct/extensions/pretty_print"
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  class Struct
3
5
  # Helper for {Struct#to_hash} implementation
@@ -6,12 +8,10 @@ module Dry
6
8
  # @param [#to_hash, #map, Object] value
7
9
  # @return [Hash, Array]
8
10
  def self.[](value)
9
- if value.respond_to?(:to_hash)
10
- if RUBY_VERSION >= '2.4'
11
- value.to_hash.transform_values { |v| self[v] }
12
- else
13
- value.to_hash.each_with_object({}) { |(k, v), h| h[k] = self[v] }
14
- end
11
+ if value.is_a?(Struct)
12
+ value.to_h.transform_values { |current| self[current] }
13
+ elsif value.respond_to?(:to_hash)
14
+ value.to_hash.transform_values { |current| self[current] }
15
15
  elsif value.respond_to?(:to_ary)
16
16
  value.to_ary.map { |item| self[item] }
17
17
  else