optify-from_hash 0.2.2 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f6e409cfccf83b515e84d3cc19713e1897ed53cf4340d66de2843433c355445
4
- data.tar.gz: 630ed4471403551570ba2f18d3ec687ba4790dec90332496d6f9d39100f353f5
3
+ metadata.gz: 23bb51f6e184777cad52c5ee7555c00a74e981b13c34fbc13133e80e4dd8b447
4
+ data.tar.gz: 80d4b0194cabc3202e60199ee7473f176e00795c9d2d07906cafca0cf0d9efe9
5
5
  SHA512:
6
- metadata.gz: b9414cd9843a25f5b1ed1ab7f1560d5e73c0f85b3900859daa786c03f02bfd04dbc4947120bf519c8ff5f80b3712947b3b53a20c778b1e8e354c259b7c266e19
7
- data.tar.gz: de4275bb6b2300dfa3e154dbc04ec49120d28ec3f20e17e537c51e1cde5a065772eaad10269abdeb8fa7a528043b376a49d2c2655d810b47373137f322a39fdc
6
+ metadata.gz: d51c60abe1041073c0090b6759824963af316248b858608ce7be0f8cf101f74d8207e2d25dbaab274868548c34dd2f4f067cf7807d732f34d0c91fae157afd59
7
+ data.tar.gz: db3e5143e8ca29bf947043d9cfaffd6d840bf8dc7295d546daefa7a65373b53a0606c48ea770338aaa7079e2652e94b5f24cc4baf877a05b2b2cd3cac41e6d1d
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'json'
5
+ require 'set'
5
6
  require 'sorbet-runtime'
6
7
  require 'tapioca'
7
8
 
@@ -12,6 +13,41 @@ module Optify
12
13
  extend T::Helpers
13
14
  abstract!
14
15
 
16
+ @key_to_type = {} #: Hash[Symbol, T::Types::Base]
17
+
18
+ class << self
19
+ #: Hash[Symbol, T::Types::Base]
20
+ attr_reader :key_to_type
21
+ end
22
+
23
+ #: [T < Optify::FromHashable] (Class[T]) -> void
24
+ def self.inherited(subclass)
25
+ super
26
+
27
+ # Trace the execution after the subclass finishes loading to capture its methods.
28
+ TracePoint.trace(:end) do |tp|
29
+ if tp.self == subclass
30
+ subclass.instance_variable_set(:@key_to_type, _build_key_to_type(subclass))
31
+ tp.disable
32
+ end
33
+ end
34
+ end
35
+
36
+ #: [Type < Optify::FromHashable] (Class[Type]) -> Hash[Symbol, T::Types::Base]
37
+ private_class_method def self._build_key_to_type(subclass)
38
+ result = {}
39
+
40
+ subclass.public_instance_methods(false).each do |method_name|
41
+ method = subclass.instance_method(method_name)
42
+ sig = T::Utils.signature_for_method(method)
43
+ next if sig.nil?
44
+
45
+ result[method_name] = sig.return_type
46
+ end
47
+
48
+ result.freeze
49
+ end
50
+
15
51
  # Create a new immutable instance of the class from a hash.
16
52
  #
17
53
  # @param hash The hash to create the instance from.
@@ -21,26 +57,43 @@ module Optify
21
57
  instance = new
22
58
 
23
59
  hash.each do |key, value|
24
- begin
25
- method = instance_method(key)
26
- rescue StandardError
27
- raise ArgumentError,
28
- "Error converting hash to `#{name}` because of key \"#{key}\". Perhaps \"#{key}\" is not a valid attribute for `#{name}`."
29
- end
30
-
31
- sig = T::Utils.signature_for_method(method)
32
- raise "A Sorbet signature is required for `#{name}.#{key}`." if sig.nil?
33
-
34
- sig_return_type = sig.return_type
35
- value = _convert_value(value, sig_return_type)
60
+ value_type = _get_value_type(key)
61
+ value = _convert_value(value, value_type)
36
62
  instance.instance_variable_set("@#{key}", value)
37
63
  end
38
64
 
39
65
  instance.freeze
40
66
  end
41
67
 
68
+ #: (untyped) -> T::Types::Base
69
+ private_class_method def self._get_value_type(key)
70
+ key = key.to_sym
71
+ @key_to_type.fetch(key) do
72
+ parent = superclass #: untyped
73
+ while parent != Object
74
+ if parent.respond_to?(:key_to_type)
75
+ result = parent.key_to_type[key]
76
+ return result if result
77
+ end
78
+ parent = parent.superclass
79
+ end
80
+ raise ArgumentError,
81
+ "Error converting hash to `#{name}` because no type was found for key \"#{key}\". " \
82
+ "Perhaps \"#{key}\" is not a valid attribute for `#{name}`. " \
83
+ "Types exist for #{@key_to_type.keys.sort!}"
84
+ end
85
+ end
86
+
87
+ #: (Array[untyped], untyped) -> (Array[untyped] | Set[untyped])
88
+ private_class_method def self._convert_array(value, unwrapped_type)
89
+ inner_type = unwrapped_type.type
90
+ return value.map { |v| _convert_value(v, inner_type) }.freeze if unwrapped_type.is_a?(T::Types::TypedArray)
91
+
92
+ value.each_with_object(Set.new) { |v, set| set.add(_convert_value(v, inner_type)) }.freeze
93
+ end
94
+
42
95
  #: (untyped, T::Types::Base) -> untyped
43
- def self._convert_value(value, type)
96
+ private_class_method def self._convert_value(value, type)
44
97
  if type.is_a?(T::Types::Untyped)
45
98
  # No preferred type is given, so return the value as is.
46
99
  return value
@@ -51,9 +104,7 @@ module Optify
51
104
 
52
105
  case value
53
106
  when Array
54
- inner_type = unwrapped_type #: as untyped
55
- .type
56
- return value.map { |v| _convert_value(v, inner_type) }.freeze
107
+ return _convert_array(value, unwrapped_type)
57
108
  when Hash
58
109
  # Handle `T.nilable(T::Hash[...])` and `T.any(...)`.
59
110
  # We used to use `type = type.unwrap_nilable if type.respond_to?(:unwrap_nilable)`, but it's not needed now that we handle
@@ -77,7 +128,7 @@ module Optify
77
128
  end
78
129
 
79
130
  #: (Hash[untyped, untyped], T::Types::Base) -> untyped
80
- def self._convert_hash(hash, type)
131
+ private_class_method def self._convert_hash(hash, type)
81
132
  if type.respond_to?(:raw_type)
82
133
  # There is an object for the hash.
83
134
  # It could be a custom class, a String, or maybe something else.
@@ -87,15 +138,14 @@ module Optify
87
138
  elsif type.is_a?(T::Types::TypedHash)
88
139
  # The hash should be a hash, but the values might be objects to convert.
89
140
  type_for_keys = type.keys
141
+ type_for_values = type.values
90
142
 
91
- convert_key = if type_for_keys.is_a?(T::Types::Simple) && type_for_keys.raw_type == Symbol
92
- lambda(&:to_sym)
93
- else
94
- lambda(&:itself)
95
- end
143
+ result = hash
144
+ .transform_values { |v| _convert_value(v, type_for_values) }
96
145
 
97
- type_for_values = type.values
98
- return hash.map { |k, v| [convert_key.call(k), _convert_value(v, type_for_values)] }.to_h
146
+ return result.transform_keys!(&:to_sym) if type_for_keys.is_a?(T::Types::Simple) && type_for_keys.raw_type == Symbol
147
+
148
+ return result
99
149
  end
100
150
 
101
151
  raise TypeError, "Could not convert hash #{hash} to `#{type}`."
@@ -103,7 +153,7 @@ module Optify
103
153
 
104
154
  # Unwrap `T.nilable(...)` to get the inner type, or return the type as-is.
105
155
  #: (T::Types::Base) -> T::Types::Base
106
- def self._unwrap_nilable(type)
156
+ private_class_method def self._unwrap_nilable(type)
107
157
  if type.respond_to?(:unwrap_nilable)
108
158
  type #: as untyped
109
159
  .unwrap_nilable
@@ -112,8 +162,6 @@ module Optify
112
162
  end
113
163
  end
114
164
 
115
- private_class_method :_convert_hash, :_convert_value, :_unwrap_nilable
116
-
117
165
  # Compare this object with another object for equality.
118
166
  # @param other The object to compare.
119
167
  # @return [Boolean] true if the objects are equal; otherwise, false.
@@ -127,6 +175,23 @@ module Optify
127
175
  end
128
176
  end
129
177
 
178
+ # Support equality by value so that instances can be used in Sets and as Hash keys.
179
+ #: (untyped) -> bool
180
+ def eql?(other)
181
+ return true if other.equal?(self)
182
+ return false if self.class != other.class
183
+
184
+ instance_variables.all? do |name|
185
+ instance_variable_get(name).eql?(other.instance_variable_get(name))
186
+ end
187
+ end
188
+
189
+ # @return [Integer] a hash value based on the object's class and instance variables.
190
+ #: () -> Integer
191
+ def hash
192
+ [self.class, *instance_variables.sort.map { |name| instance_variable_get(name) }].hash
193
+ end
194
+
130
195
  # Convert this object to a JSON string.
131
196
  #: (?JSON::State?) -> String
132
197
  def to_json(state = nil)
@@ -153,9 +218,10 @@ module Optify
153
218
  end
154
219
 
155
220
  #: (untyped) -> untyped
156
- def self._convert_value_for_to_h(value)
221
+ private_class_method def self._convert_value_for_to_h(value)
157
222
  case value
158
- when Array
223
+ # Treat sets like arrays for JSON serialization; otherwise, the elements are not shown.
224
+ when Array, Set
159
225
  value.map { |v| _convert_value_for_to_h(v) }
160
226
  when Hash
161
227
  value.transform_values { |v| _convert_value_for_to_h(v) }
@@ -169,7 +235,5 @@ module Optify
169
235
  end
170
236
  end
171
237
  end
172
-
173
- private_class_method :_convert_value_for_to_h
174
238
  end
175
239
  end
@@ -5,6 +5,8 @@
5
5
  module Optify
6
6
  # A base class for classes that can be created from a hash.
7
7
  class FromHashable
8
+ extend T::Sig
9
+ extend T::Helpers
8
10
  abstract!
9
11
 
10
12
  # Create a new instance of the class from a hash.
@@ -31,5 +33,13 @@ module Optify
31
33
  # @return [Boolean] true if the objects are equal; otherwise, false.
32
34
  sig { params(other: T.untyped).returns(T::Boolean) }
33
35
  def ==(other); end
36
+
37
+ # Support equality by value so that instances can be used in Sets and as Hash keys.
38
+ sig { params(other: T.untyped).returns(T::Boolean) }
39
+ def eql?(other); end
40
+
41
+ # @return a hash value based on the object's class and instance variables.
42
+ sig { returns(Integer) }
43
+ def hash; end
34
44
  end
35
45
  end
@@ -4,6 +4,8 @@ end
4
4
 
5
5
  # A base class for classes that can be created from a hash.
6
6
  class Optify::FromHashable
7
+ extend T::Helpers
8
+
7
9
  # Create a new instance of the class from a hash.
8
10
  #
9
11
  # @param hash The hash to create the instance from.
@@ -24,4 +26,10 @@ class Optify::FromHashable
24
26
  # @param other The object to compare.
25
27
  # @return [Boolean] true if the objects are equal; otherwise, false.
26
28
  def ==: (untyped other) -> bool
29
+
30
+ # Support equality by value so that instances can be used in Sets and as Hash keys.
31
+ def eql?: (untyped other) -> bool
32
+
33
+ # @return a hash value based on the object's class and instance variables.
34
+ def hash: () -> Integer
27
35
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optify-from_hash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin D. Harris
@@ -49,14 +49,14 @@ dependencies:
49
49
  requirements:
50
50
  - - "~>"
51
51
  - !ruby/object:Gem::Version
52
- version: 4.0.0.dev.4
52
+ version: 4.0.3
53
53
  type: :development
54
54
  prerelease: false
55
55
  version_requirements: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: 4.0.0.dev.4
59
+ version: 4.0.3
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: sorbet
62
62
  requirement: !ruby/object:Gem::Requirement
@@ -83,14 +83,14 @@ dependencies:
83
83
  requirements:
84
84
  - - "~>"
85
85
  - !ruby/object:Gem::Version
86
- version: 0.17.7
86
+ version: 0.19.1
87
87
  type: :development
88
88
  prerelease: false
89
89
  version_requirements: !ruby/object:Gem::Requirement
90
90
  requirements:
91
91
  - - "~>"
92
92
  - !ruby/object:Gem::Version
93
- version: 0.17.7
93
+ version: 0.19.1
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: test-unit
96
96
  requirement: !ruby/object:Gem::Requirement