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 +4 -4
- data/lib/optify_from_hash/from_hashable.rb +95 -31
- data/rbi/optify_from_hash.rbi +10 -0
- data/sig/optify_from_hash.rbs +8 -0
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23bb51f6e184777cad52c5ee7555c00a74e981b13c34fbc13133e80e4dd8b447
|
|
4
|
+
data.tar.gz: 80d4b0194cabc3202e60199ee7473f176e00795c9d2d07906cafca0cf0d9efe9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
else
|
|
94
|
-
lambda(&:itself)
|
|
95
|
-
end
|
|
143
|
+
result = hash
|
|
144
|
+
.transform_values { |v| _convert_value(v, type_for_values) }
|
|
96
145
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
data/rbi/optify_from_hash.rbi
CHANGED
|
@@ -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
|
data/sig/optify_from_hash.rbs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
93
|
+
version: 0.19.1
|
|
94
94
|
- !ruby/object:Gem::Dependency
|
|
95
95
|
name: test-unit
|
|
96
96
|
requirement: !ruby/object:Gem::Requirement
|