emery 0.0.19 → 0.1.27
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/emery/codecs.rb +255 -0
- data/lib/emery/dataclass.rb +13 -30
- data/lib/emery/enum.rb +0 -8
- data/lib/emery/jsoner.rb +31 -181
- data/lib/emery/taggedunion.rb +79 -0
- data/lib/emery/type.rb +23 -36
- data/lib/emery.rb +4 -2
- data/test/dataclass_test.rb +26 -29
- data/test/enum_test.rb +1 -1
- data/test/jsoner_test.rb +15 -36
- data/test/taggedunion_test.rb +104 -0
- data/test/type_test.rb +14 -30
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed5b5ad376a8f3ffef14484fd9f299e0461bdb0b567e76b5e8e6fb913c6dcc71
|
4
|
+
data.tar.gz: '09ed0d4e68b06d7353852ce79b7d7fa93de1ea97cdf38f32bf9c8edc08e77fd6'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e1a8443886e984bf7fe51196f7df9b7d7b5a57378d23b699f9adfa14ce691b35e41b71d027526f5fccbc49b54a602f168d19b6e4e4dc37060e985873379ee267
|
7
|
+
data.tar.gz: '0282d146e1a49b4ad3608e0378d8688373e55b6e20c48442f7d783afa75a6747619fdc2a87f27cd7309a47ad60a50b3c15366eb6e98a13f076d13b9f44bcd082'
|
data/lib/emery/codecs.rb
ADDED
@@ -0,0 +1,255 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
module Codecs
|
4
|
+
module BuiltinTypeCodec
|
5
|
+
def self.applicable?(type)
|
6
|
+
[String, Float, Integer, TrueClass, FalseClass, NilClass].include? type
|
7
|
+
end
|
8
|
+
def self.deserialize(type, json_value)
|
9
|
+
T.check(type, json_value)
|
10
|
+
end
|
11
|
+
def self.serialize(type, value)
|
12
|
+
T.check(type, value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module UnknownCodec
|
17
|
+
def self.applicable?(type)
|
18
|
+
type.instance_of? T::UnknownType
|
19
|
+
end
|
20
|
+
def self.deserialize(type, json_value)
|
21
|
+
json_value
|
22
|
+
end
|
23
|
+
def self.serialize(type, value)
|
24
|
+
value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ArrayCodec
|
29
|
+
def self.applicable?(type)
|
30
|
+
type.instance_of? T::ArrayType
|
31
|
+
end
|
32
|
+
def self.deserialize(type, json_value)
|
33
|
+
T.check_not_nil(type, json_value)
|
34
|
+
if !json_value.is_a?(Array)
|
35
|
+
raise JsonerError.new("JSON value type #{json_value.class} is not Array")
|
36
|
+
end
|
37
|
+
json_value.map { |item_json_value| Jsoner.deserialize(type.item_type, item_json_value) }
|
38
|
+
end
|
39
|
+
def self.serialize(type, value)
|
40
|
+
if !value.is_a?(Array)
|
41
|
+
raise JsonerError.new("Value type #{json_value.class} is not Array")
|
42
|
+
end
|
43
|
+
value.map { |item| Jsoner.serialize(type.item_type, item) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module HashCodec
|
48
|
+
def self.applicable?(type)
|
49
|
+
type.instance_of? T::HashType
|
50
|
+
end
|
51
|
+
def self.deserialize(type, json_value)
|
52
|
+
T.check_not_nil(type, json_value)
|
53
|
+
if type.key_type != String
|
54
|
+
raise JsonerError.new("Hash key type #{type.key_type} is not supported for JSON (de)serialization - key should be String")
|
55
|
+
end
|
56
|
+
if !json_value.is_a?(Hash)
|
57
|
+
raise JsonerError.new("JSON value type #{json_value.class} is not Hash")
|
58
|
+
end
|
59
|
+
json_value.map do |key, value|
|
60
|
+
[T.check(type.key_type, key), Jsoner.deserialize(type.value_type, value)]
|
61
|
+
end.to_h
|
62
|
+
end
|
63
|
+
def self.serialize(type, value)
|
64
|
+
if type.key_type != String
|
65
|
+
raise JsonerError.new("Hash key type #{type.key_type} is not supported for JSON (de)serialization - key should be String")
|
66
|
+
end
|
67
|
+
if !value.is_a?(Hash)
|
68
|
+
raise JsonerError.new("Value type #{value.class} is not Hash")
|
69
|
+
end
|
70
|
+
value.map do |key, value|
|
71
|
+
[T.check(type.key_type, key), Jsoner.serialize(type.value_type, value)]
|
72
|
+
end.to_h
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module UnionCodec
|
77
|
+
def self.applicable?(type)
|
78
|
+
type.instance_of? T::UnionType
|
79
|
+
end
|
80
|
+
def self.deserialize(type, json_value)
|
81
|
+
type.types.each do |t|
|
82
|
+
begin
|
83
|
+
return Jsoner.deserialize(t, json_value)
|
84
|
+
rescue JsonerError
|
85
|
+
end
|
86
|
+
end
|
87
|
+
raise JsonerError.new("Value '#{json_value.inspect.to_s}' can not be deserialized as any of #{type.types.map { |t| t.to_s}.join(', ')}")
|
88
|
+
end
|
89
|
+
def self.serialize(type, value)
|
90
|
+
T.check(type, value)
|
91
|
+
t = type.types.find {|t| T.instance_of?(t, value) }
|
92
|
+
Jsoner.serialize(t, value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module NilableCodec
|
97
|
+
def self.applicable?(type)
|
98
|
+
type.instance_of? T::NilableType
|
99
|
+
end
|
100
|
+
def self.deserialize(type, json_value)
|
101
|
+
if json_value != nil
|
102
|
+
Jsoner.deserialize(type.inner_type, json_value)
|
103
|
+
else
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
def self.serialize(type, value)
|
108
|
+
if value != nil
|
109
|
+
Jsoner.serialize(type.inner_type, value)
|
110
|
+
else
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module StringFormattedCodec
|
117
|
+
def self.applicable?(type)
|
118
|
+
type.instance_of? T::StringFormattedType
|
119
|
+
end
|
120
|
+
def self.deserialize(type, json_value)
|
121
|
+
T.check(type, json_value)
|
122
|
+
end
|
123
|
+
def self.serialize(type, value)
|
124
|
+
T.check(type, value)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
module FloatCodec
|
129
|
+
def self.applicable?(type)
|
130
|
+
type == Float
|
131
|
+
end
|
132
|
+
def self.deserialize(type, json_value)
|
133
|
+
T.check(T.union(Float, Integer), json_value)
|
134
|
+
json_value.to_f
|
135
|
+
end
|
136
|
+
def self.serialize(type, value)
|
137
|
+
T.check(Float, value)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
module DateTimeCodec
|
142
|
+
def self.applicable?(type)
|
143
|
+
type == DateTime
|
144
|
+
end
|
145
|
+
def self.deserialize(type, json_value)
|
146
|
+
T.check(String, json_value)
|
147
|
+
begin
|
148
|
+
DateTime.strptime(json_value, '%Y-%m-%dT%H:%M:%S')
|
149
|
+
rescue
|
150
|
+
raise JsonerError.new("Failed to parse DateTime from '#{json_value.inspect.to_s}' format %Y-%m-%dT%H:%M:%S is required")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
def self.serialize(type, value)
|
154
|
+
T.check(DateTime, value)
|
155
|
+
value.strftime('%Y-%m-%dT%H:%M:%S')
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
module DateCodec
|
160
|
+
def self.applicable?(type)
|
161
|
+
type == Date
|
162
|
+
end
|
163
|
+
def self.deserialize(type, json_value)
|
164
|
+
T.check(String, json_value)
|
165
|
+
begin
|
166
|
+
Date.strptime(json_value, '%Y-%m-%d')
|
167
|
+
rescue
|
168
|
+
raise JsonerError.new("Failed to parse Date from '#{json_value.inspect.to_s}' format %Y-%m-%d is required")
|
169
|
+
end
|
170
|
+
end
|
171
|
+
def self.serialize(type, value)
|
172
|
+
T.check(Date, value)
|
173
|
+
value.strftime('%Y-%m-%d')
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
module EnumCodec
|
178
|
+
def self.applicable?(type)
|
179
|
+
type.respond_to? :ancestors and type.ancestors.include? Enum
|
180
|
+
end
|
181
|
+
def self.deserialize(type, json_value)
|
182
|
+
T.check(type, json_value)
|
183
|
+
end
|
184
|
+
|
185
|
+
def self.serialize(type, value)
|
186
|
+
T.check(type, value)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
module DataClassCodec
|
191
|
+
def self.applicable?(type)
|
192
|
+
type.respond_to? :ancestors and type.ancestors.include? DataClass
|
193
|
+
end
|
194
|
+
def self.deserialize(type, json_value)
|
195
|
+
T.check(T.hash(String, NilableUnknown), json_value)
|
196
|
+
parameters = type.typed_attributes.map do |attr, attr_type|
|
197
|
+
attr_value = json_value[attr.to_s]
|
198
|
+
[attr, Jsoner.deserialize(attr_type, attr_value)]
|
199
|
+
end
|
200
|
+
return type.new(parameters.to_h)
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.serialize(type, value)
|
204
|
+
T.check(type, value)
|
205
|
+
attrs = type.typed_attributes.map do |attr, attr_type|
|
206
|
+
[attr, Jsoner.serialize(attr_type, value.send(attr))]
|
207
|
+
end
|
208
|
+
return attrs.to_h
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
module TaggedUnionCodec
|
213
|
+
def self.applicable?(type)
|
214
|
+
type.respond_to? :ancestors and type.ancestors.include? TaggedUnion
|
215
|
+
end
|
216
|
+
def self.deserialize(type, json_value)
|
217
|
+
if !json_value.is_a?(Hash)
|
218
|
+
raise JsonerError.new("JSON value type #{json_value.class} is not Hash but it has to be Hash to represent a union")
|
219
|
+
end
|
220
|
+
if type.discriminator == nil
|
221
|
+
if json_value.keys.length != 1
|
222
|
+
raise JsonerError.new("JSON value #{json_value} should have only one key to represent union type wrapper object, found #{json_value.keys.length}")
|
223
|
+
end
|
224
|
+
tag_key = json_value.keys[0]
|
225
|
+
if not type.typed_tags.key? tag_key.to_sym
|
226
|
+
raise JsonerError.new("JSON key '#{tag_key}' does not match any tag in union type #{self}")
|
227
|
+
end
|
228
|
+
tag_type = type.typed_tags[tag_key.to_sym]
|
229
|
+
tag_json_value = json_value[tag_key]
|
230
|
+
tag_value = Jsoner.deserialize(tag_type, tag_json_value)
|
231
|
+
return type.new({tag_key.to_sym => tag_value})
|
232
|
+
else
|
233
|
+
if not json_value.key? type.discriminator
|
234
|
+
raise JsonerError.new("JSON value #{json_value} does not have discriminator field #{type.discriminator}")
|
235
|
+
end
|
236
|
+
tag_key = json_value[type.discriminator]
|
237
|
+
tag_type = type.typed_tags[tag_key.to_sym]
|
238
|
+
tag_value = Jsoner.deserialize(tag_type, json_value)
|
239
|
+
return type.new({tag_key.to_sym => tag_value})
|
240
|
+
end
|
241
|
+
end
|
242
|
+
def self.serialize(type, value)
|
243
|
+
T.check(type, value)
|
244
|
+
tag_key = value.current_tag
|
245
|
+
tag_type = type.typed_tags[tag_key]
|
246
|
+
tag_json_value = Jsoner.serialize(tag_type, value.send(tag_key))
|
247
|
+
if type.discriminator == nil
|
248
|
+
return { tag_key => tag_json_value }
|
249
|
+
else
|
250
|
+
tag_json_value[type.discriminator] = tag_key
|
251
|
+
return tag_json_value
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
data/lib/emery/dataclass.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module DataClass
|
2
2
|
def initialize(params)
|
3
|
-
self.class.
|
3
|
+
self.class.typed_attributes.each do |attr, attr_type|
|
4
4
|
attr_value = params[attr]
|
5
5
|
self.instance_variable_set("@#{attr}", T.check_var(attr, attr_type, attr_value))
|
6
6
|
end
|
@@ -9,7 +9,7 @@ module DataClass
|
|
9
9
|
def ==(other)
|
10
10
|
begin
|
11
11
|
T.check(self.class, other)
|
12
|
-
self.class.
|
12
|
+
self.class.typed_attributes.keys.each do |attr|
|
13
13
|
if self.instance_variable_get("@#{attr}") != other.instance_variable_get("@#{attr}")
|
14
14
|
return false
|
15
15
|
end
|
@@ -21,13 +21,13 @@ module DataClass
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def copy(params)
|
24
|
-
params.each do |attr
|
25
|
-
if !self.class.
|
24
|
+
params.keys.each do |attr|
|
25
|
+
if !self.class.typed_attributes.key?(attr)
|
26
26
|
raise TypeError.new("Non existing attribute #{attr}")
|
27
27
|
end
|
28
28
|
end
|
29
29
|
new_params =
|
30
|
-
self.class.
|
30
|
+
self.class.typed_attributes.keys.map do |attr|
|
31
31
|
attr_value =
|
32
32
|
if params.key?(attr)
|
33
33
|
params[attr]
|
@@ -48,41 +48,24 @@ module DataClass
|
|
48
48
|
end
|
49
49
|
|
50
50
|
module ClassMethods
|
51
|
-
def
|
52
|
-
@
|
51
|
+
def typed_attributes
|
52
|
+
@typed_attributes
|
53
53
|
end
|
54
54
|
|
55
55
|
def val(name, type)
|
56
|
-
if @
|
57
|
-
@
|
56
|
+
if @typed_attributes == nil
|
57
|
+
@typed_attributes = {}
|
58
58
|
end
|
59
|
-
@
|
59
|
+
@typed_attributes[name] = type
|
60
60
|
attr_reader name
|
61
61
|
end
|
62
62
|
|
63
63
|
def var(name, type)
|
64
|
-
if @
|
65
|
-
@
|
64
|
+
if @typed_attributes == nil
|
65
|
+
@typed_attributes = {}
|
66
66
|
end
|
67
|
-
@
|
67
|
+
@typed_attributes[name] = type
|
68
68
|
attr_accessor name
|
69
69
|
end
|
70
|
-
|
71
|
-
def jsoner_deserialize(json_value)
|
72
|
-
T.check(T.hash(String, NilableUntyped), json_value)
|
73
|
-
parameters = @json_attributes.map do |attr, attr_type|
|
74
|
-
attr_value = json_value[attr.to_s]
|
75
|
-
[attr, Jsoner.deserialize(attr_type, attr_value)]
|
76
|
-
end
|
77
|
-
return self.new parameters.to_h
|
78
|
-
end
|
79
|
-
|
80
|
-
def jsoner_serialize(value)
|
81
|
-
T.check(self, value)
|
82
|
-
attrs = @json_attributes.map do |attr, attr_type|
|
83
|
-
[attr, Jsoner.serialize(attr_type, value.send(attr))]
|
84
|
-
end
|
85
|
-
return attrs.to_h
|
86
|
-
end
|
87
70
|
end
|
88
71
|
end
|
data/lib/emery/enum.rb
CHANGED
data/lib/emery/jsoner.rb
CHANGED
@@ -1,176 +1,36 @@
|
|
1
1
|
require "json"
|
2
|
-
require "date"
|
3
2
|
|
4
3
|
class JsonerError < StandardError
|
5
4
|
end
|
6
5
|
|
7
6
|
module Jsoner
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
T::ArrayType.class_eval do
|
27
|
-
def jsoner_deserialize(json_value)
|
28
|
-
T.check_not_nil(self, json_value)
|
29
|
-
if !json_value.is_a?(Array)
|
30
|
-
raise JsonerError.new("JSON value type #{json_value.class} is not Array")
|
31
|
-
end
|
32
|
-
json_value.map { |item_json_value| Jsoner.deserialize(self.item_type, item_json_value) }
|
33
|
-
end
|
34
|
-
def jsoner_serialize(value)
|
35
|
-
if !value.is_a?(Array)
|
36
|
-
raise JsonerError.new("Value type #{json_value.class} is not Array")
|
37
|
-
end
|
38
|
-
value.map { |item| Jsoner.serialize(self.item_type, item) }
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
T::HashType.class_eval do
|
43
|
-
def jsoner_deserialize(json_value)
|
44
|
-
T.check_not_nil(self, json_value)
|
45
|
-
if self.key_type != String
|
46
|
-
raise JsonerError.new("Hash key type #{self.key_type} is not supported for JSON (de)serialization - key should be String")
|
47
|
-
end
|
48
|
-
if !json_value.is_a?(Hash)
|
49
|
-
raise JsonerError.new("JSON value type #{json_value.class} is not Hash")
|
50
|
-
end
|
51
|
-
json_value.map do |key, value|
|
52
|
-
[T.check(self.key_type, key), Jsoner.deserialize(self.value_type, value)]
|
53
|
-
end.to_h
|
54
|
-
end
|
55
|
-
def jsoner_serialize(value)
|
56
|
-
if self.key_type != String
|
57
|
-
raise JsonerError.new("Hash key type #{self.key_type} is not supported for JSON (de)serialization - key should be String")
|
58
|
-
end
|
59
|
-
if !value.is_a?(Hash)
|
60
|
-
raise JsonerError.new("Value type #{value.class} is not Hash")
|
61
|
-
end
|
62
|
-
value.map do |key, value|
|
63
|
-
[T.check(self.key_type, key), Jsoner.serialize(self.value_type, value)]
|
64
|
-
end.to_h
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
T::AnyType.class_eval do
|
69
|
-
def jsoner_deserialize(json_value)
|
70
|
-
types.each do |type|
|
71
|
-
begin
|
72
|
-
return Jsoner.deserialize(type, json_value)
|
73
|
-
rescue JsonerError
|
74
|
-
end
|
75
|
-
end
|
76
|
-
raise JsonerError.new("Value '#{json_value.inspect.to_s}' can not be deserialized as any of #{@types.map { |t| t.to_s}.join(', ')}")
|
77
|
-
end
|
78
|
-
def jsoner_serialize(value)
|
79
|
-
T.check(self, value)
|
80
|
-
type = types.find {|t| T.instance_of?(t, value) }
|
81
|
-
Jsoner.serialize(type, value)
|
82
|
-
end
|
7
|
+
@@codecs = [
|
8
|
+
Codecs::FloatCodec,
|
9
|
+
Codecs::DateCodec,
|
10
|
+
Codecs::DateTimeCodec,
|
11
|
+
Codecs::StringFormattedCodec,
|
12
|
+
Codecs::UnionCodec,
|
13
|
+
Codecs::NilableCodec,
|
14
|
+
Codecs::ArrayCodec,
|
15
|
+
Codecs::HashCodec,
|
16
|
+
Codecs::UnknownCodec,
|
17
|
+
Codecs::EnumCodec,
|
18
|
+
Codecs::DataClassCodec,
|
19
|
+
Codecs::TaggedUnionCodec,
|
20
|
+
Codecs::BuiltinTypeCodec,
|
21
|
+
]
|
22
|
+
|
23
|
+
def self.insert_codec(codec)
|
24
|
+
@@codecs.insert(0, codec)
|
83
25
|
end
|
84
26
|
|
85
|
-
|
86
|
-
|
87
|
-
if
|
88
|
-
|
27
|
+
def Jsoner.find_codec(type)
|
28
|
+
@@codecs.each do |serializer|
|
29
|
+
if serializer.applicable?(type)
|
30
|
+
return serializer
|
89
31
|
end
|
90
|
-
if json_value.keys.length != 1
|
91
|
-
raise JsonerError.new("JSON value #{json_value} should have only one key to represent union type, found #{json_value.keys.length}")
|
92
|
-
end
|
93
|
-
case_key = json_value.keys[0]
|
94
|
-
if not cases.key? case_key.to_sym
|
95
|
-
raise JsonerError.new("JSON key '#{case_key}' does not match any case in union type #{self}")
|
96
|
-
end
|
97
|
-
type = cases[case_key.to_sym]
|
98
|
-
case_json_value = json_value[case_key]
|
99
|
-
return Jsoner.deserialize(type, case_json_value)
|
100
|
-
end
|
101
|
-
def jsoner_serialize(value)
|
102
|
-
T.check(self, value)
|
103
|
-
type = types.find {|t| T.instance_of?(t, value) }
|
104
|
-
case_key = cases.key(type)
|
105
|
-
result = { case_key => Jsoner.serialize(type, value) }
|
106
|
-
result
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
T::Nilable.class_eval do
|
111
|
-
def jsoner_deserialize(json_value)
|
112
|
-
if json_value != nil
|
113
|
-
Jsoner.deserialize(self.type, json_value)
|
114
|
-
else
|
115
|
-
nil
|
116
|
-
end
|
117
|
-
end
|
118
|
-
def jsoner_serialize(value)
|
119
|
-
if value != nil
|
120
|
-
Jsoner.serialize(self.type, value)
|
121
|
-
else
|
122
|
-
nil
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
module FloatSerializer
|
128
|
-
def self.jsoner_deserialize(json_value)
|
129
|
-
T.check(T.any(Float, Integer), json_value)
|
130
|
-
json_value.to_f
|
131
|
-
end
|
132
|
-
def self.jsoner_serialize(value)
|
133
|
-
T.check(Float, value)
|
134
32
|
end
|
135
|
-
|
136
|
-
|
137
|
-
module DateTimeSerializer
|
138
|
-
def self.jsoner_deserialize(json_value)
|
139
|
-
T.check(String, json_value)
|
140
|
-
begin
|
141
|
-
DateTime.strptime(json_value, '%Y-%m-%dT%H:%M:%S')
|
142
|
-
rescue
|
143
|
-
raise JsonerError.new("Failed to parse DateTime from '#{json_value.inspect.to_s}' format %Y-%m-%dT%H:%M:%S is required")
|
144
|
-
end
|
145
|
-
end
|
146
|
-
def self.jsoner_serialize(value)
|
147
|
-
T.check(DateTime, value)
|
148
|
-
value.strftime('%Y-%m-%dT%H:%M:%S')
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
module DateSerializer
|
153
|
-
def self.jsoner_deserialize(json_value)
|
154
|
-
T.check(String, json_value)
|
155
|
-
begin
|
156
|
-
Date.strptime(json_value, '%Y-%m-%d')
|
157
|
-
rescue
|
158
|
-
raise JsonerError.new("Failed to parse Date from '#{json_value.inspect.to_s}' format %Y-%m-%d is required")
|
159
|
-
end
|
160
|
-
end
|
161
|
-
def self.jsoner_serialize(value)
|
162
|
-
T.check(Date, value)
|
163
|
-
value.strftime('%Y-%m-%d')
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
@@serializers = {
|
168
|
-
Float => FloatSerializer,
|
169
|
-
Date => DateSerializer,
|
170
|
-
DateTime => DateTimeSerializer
|
171
|
-
}
|
172
|
-
def self.add_serializer(type, serializer)
|
173
|
-
@@serializers[type] = serializer
|
33
|
+
return nil
|
174
34
|
end
|
175
35
|
|
176
36
|
def Jsoner.from_json(type, json)
|
@@ -180,16 +40,11 @@ module Jsoner
|
|
180
40
|
|
181
41
|
def Jsoner.deserialize(type, json_value)
|
182
42
|
begin
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
return @@serializers[type].jsoner_deserialize(json_value)
|
187
|
-
else
|
188
|
-
if ![String, Float, Integer, TrueClass, FalseClass, NilClass].include? type
|
189
|
-
raise JsonerError.new("Type #{type} is not supported in Jsoner deserialization")
|
190
|
-
end
|
191
|
-
return T.check(type, json_value)
|
43
|
+
codec = Jsoner.find_codec(type)
|
44
|
+
if codec == nil
|
45
|
+
raise JsonerError.new("Type #{type} is not supported in Jsoner deserialization")
|
192
46
|
end
|
47
|
+
return codec.deserialize(type, json_value)
|
193
48
|
rescue StandardError => error
|
194
49
|
raise JsonerError.new(error.message)
|
195
50
|
end
|
@@ -201,16 +56,11 @@ module Jsoner
|
|
201
56
|
|
202
57
|
def Jsoner.serialize(type, value)
|
203
58
|
begin
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
return @@serializers[type].jsoner_serialize(value)
|
208
|
-
else
|
209
|
-
if ![String, Float, Integer, TrueClass, FalseClass, NilClass].include? type
|
210
|
-
raise JsonerError.new("Type #{type} is not supported in Jsoner serialization")
|
211
|
-
end
|
212
|
-
return T.check(type, value)
|
59
|
+
codec = Jsoner.find_codec(type)
|
60
|
+
if codec == nil
|
61
|
+
raise JsonerError.new("Type #{type} is not supported in Jsoner serialization")
|
213
62
|
end
|
63
|
+
return codec.serialize(type, value)
|
214
64
|
rescue StandardError => error
|
215
65
|
raise JsonerError.new(error.message)
|
216
66
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module TaggedUnion
|
2
|
+
def initialize(params)
|
3
|
+
if params.length != 1
|
4
|
+
raise ArgumentError.new("Tagged union #{self.class} should have one only one tag set, found #{params.length}" )
|
5
|
+
end
|
6
|
+
|
7
|
+
tag = params.keys[0]
|
8
|
+
tag_type = self.class.typed_tags[tag]
|
9
|
+
tag_value = T.check_var(tag, tag_type, params[tag])
|
10
|
+
self.instance_variable_set("@#{tag}", tag_value)
|
11
|
+
self.current_tag = tag
|
12
|
+
self.current_value = tag_value
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
begin
|
17
|
+
T.check(self.class, other)
|
18
|
+
self.class.typed_tags.keys.each do |attr|
|
19
|
+
if self.instance_variable_get("@#{attr}") != other.instance_variable_get("@#{attr}")
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
return true
|
24
|
+
rescue
|
25
|
+
return false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def copy(params)
|
30
|
+
params.keys.each do |attr|
|
31
|
+
if !self.class.typed_tags.key?(attr)
|
32
|
+
raise TypeError.new("Non existing attribute #{attr}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
new_params =
|
36
|
+
self.class.typed_tags.keys.map do |attr|
|
37
|
+
attr_value =
|
38
|
+
if params.key?(attr)
|
39
|
+
params[attr]
|
40
|
+
else
|
41
|
+
self.instance_variable_get("@#{attr}")
|
42
|
+
end
|
43
|
+
[attr, attr_value]
|
44
|
+
end.to_h
|
45
|
+
return self.class.new(new_params)
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_json
|
49
|
+
return Jsoner.serialize(self.class, self)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.included(base)
|
53
|
+
base.extend ClassMethods
|
54
|
+
attr_accessor :current_tag
|
55
|
+
attr_accessor :current_value
|
56
|
+
end
|
57
|
+
|
58
|
+
module ClassMethods
|
59
|
+
def typed_tags
|
60
|
+
@typed_tags
|
61
|
+
end
|
62
|
+
|
63
|
+
def tag(name, type)
|
64
|
+
if @typed_tags == nil
|
65
|
+
@typed_tags = {}
|
66
|
+
end
|
67
|
+
@typed_tags[name] = type
|
68
|
+
attr_accessor name
|
69
|
+
end
|
70
|
+
|
71
|
+
def discriminator
|
72
|
+
@discriminator
|
73
|
+
end
|
74
|
+
|
75
|
+
def with_discriminator(value)
|
76
|
+
@discriminator = value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/emery/type.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'emery/enum'
|
2
|
+
|
1
3
|
module T
|
2
4
|
def T.check_not_nil(type, value)
|
3
5
|
if value == nil
|
@@ -5,40 +7,40 @@ module T
|
|
5
7
|
end
|
6
8
|
end
|
7
9
|
|
8
|
-
class
|
10
|
+
class UnknownType
|
9
11
|
def to_s
|
10
|
-
"
|
12
|
+
"Unknown"
|
11
13
|
end
|
12
14
|
def check(value)
|
13
15
|
T.check_not_nil(self, value)
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
17
|
-
class
|
18
|
-
attr_reader :
|
19
|
-
def initialize(
|
20
|
-
@
|
19
|
+
class NilableType
|
20
|
+
attr_reader :inner_type
|
21
|
+
def initialize(inner_type)
|
22
|
+
@inner_type = inner_type
|
21
23
|
end
|
22
24
|
def to_s
|
23
|
-
"Nilable[#{
|
25
|
+
"Nilable[#{inner_type.to_s}]"
|
24
26
|
end
|
25
27
|
def check(value)
|
26
28
|
if value != nil
|
27
|
-
T.check(
|
29
|
+
T.check(inner_type, value)
|
28
30
|
end
|
29
31
|
end
|
30
32
|
def ==(other)
|
31
|
-
T.instance_of?(
|
33
|
+
T.instance_of?(NilableType, other) and self.inner_type == other.inner_type
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
35
|
-
class
|
37
|
+
class UnionType
|
36
38
|
attr_reader :types
|
37
39
|
def initialize(*types)
|
38
40
|
@types = types
|
39
41
|
end
|
40
42
|
def to_s
|
41
|
-
"
|
43
|
+
"Union[#{types.map { |t| t.to_s}.join(', ')}]"
|
42
44
|
end
|
43
45
|
def check(value)
|
44
46
|
type = types.find {|t| T.instance_of?(t, value) }
|
@@ -47,18 +49,7 @@ module T
|
|
47
49
|
end
|
48
50
|
end
|
49
51
|
def ==(other)
|
50
|
-
T.instance_of?(
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
class UnionType < AnyType
|
55
|
-
attr_reader :cases
|
56
|
-
def initialize(cases)
|
57
|
-
@cases = cases
|
58
|
-
super(*cases.values)
|
59
|
-
end
|
60
|
-
def to_s
|
61
|
-
"Union[#{cases.map { |k, t| "#{k}: #{t}"}.join(', ')}]"
|
52
|
+
T.instance_of?(UnionType, other) and (self.types - other.types).empty?
|
62
53
|
end
|
63
54
|
end
|
64
55
|
|
@@ -107,13 +98,13 @@ module T
|
|
107
98
|
end
|
108
99
|
end
|
109
100
|
|
110
|
-
class
|
101
|
+
class StringFormattedType
|
111
102
|
attr_reader :regex
|
112
103
|
def initialize(regex)
|
113
104
|
@regex = regex
|
114
105
|
end
|
115
106
|
def to_s
|
116
|
-
"
|
107
|
+
"StringFormatted<#@regex>"
|
117
108
|
end
|
118
109
|
def check(value)
|
119
110
|
T.check_not_nil(self, value)
|
@@ -150,7 +141,7 @@ module T
|
|
150
141
|
end
|
151
142
|
|
152
143
|
def T.nilable(value_type)
|
153
|
-
|
144
|
+
NilableType.new(value_type)
|
154
145
|
end
|
155
146
|
|
156
147
|
def T.array(item_type)
|
@@ -161,12 +152,8 @@ module T
|
|
161
152
|
HashType.new(key_type, value_type)
|
162
153
|
end
|
163
154
|
|
164
|
-
def T.
|
165
|
-
|
166
|
-
end
|
167
|
-
|
168
|
-
def T.union(*cases)
|
169
|
-
UnionType.new(*cases)
|
155
|
+
def T.union(*typdefs)
|
156
|
+
UnionType.new(*typdefs)
|
170
157
|
end
|
171
158
|
|
172
159
|
def T.check_var(var_name, type, value)
|
@@ -179,7 +166,7 @@ module T
|
|
179
166
|
end
|
180
167
|
end
|
181
168
|
|
182
|
-
Boolean = T.
|
183
|
-
|
184
|
-
|
185
|
-
UUID = T::
|
169
|
+
Boolean = T.union(TrueClass, FalseClass)
|
170
|
+
Unknown = T::UnknownType.new
|
171
|
+
NilableUnknown = T.nilable(Unknown)
|
172
|
+
UUID = T::StringFormattedType.new(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
|
data/lib/emery.rb
CHANGED
data/test/dataclass_test.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require "test/unit/runner/junitxml"
|
2
2
|
|
3
|
-
require
|
3
|
+
require "emery"
|
4
4
|
|
5
5
|
class DataClassTypeEquality < Test::Unit::TestCase
|
6
6
|
def test_equals
|
@@ -14,7 +14,7 @@ end
|
|
14
14
|
|
15
15
|
class DataClassFields < Test::Unit::TestCase
|
16
16
|
def test_fields_meta
|
17
|
-
assert_equal ({:string => String, :int => Integer}), TheClass.
|
17
|
+
assert_equal ({:string => String, :int => Integer}), TheClass.typed_attributes, "Attributes with types should be available on data class"
|
18
18
|
end
|
19
19
|
|
20
20
|
def test_read
|
@@ -51,49 +51,46 @@ class DataClassEquality < Test::Unit::TestCase
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
class
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
assert_equal
|
59
|
-
|
60
|
-
|
61
|
-
def test_deserialize_nested_object
|
62
|
-
data = Jsoner.from_json(TheClassWithNested, '{"nested": {"string": "the string", "int": 123}}')
|
63
|
-
T.check(TheClassWithNested, data)
|
64
|
-
assert_equal TheClassWithNested.new(nested: TheClass.new(string: "the string", int: 123)), data, "Should parse nested data class object"
|
54
|
+
class DataClassCopy < Test::Unit::TestCase
|
55
|
+
def test_copy
|
56
|
+
a = TheClass.new(string: "the string", int: 123)
|
57
|
+
b = a.copy(string: "the other string")
|
58
|
+
assert_equal "the string", a.string
|
59
|
+
assert_equal "the other string", b.string
|
65
60
|
end
|
66
61
|
|
67
|
-
def
|
68
|
-
assert_raise
|
69
|
-
|
62
|
+
def test_copy_non_existing_field
|
63
|
+
assert_raise TypeError do
|
64
|
+
a = TheClass.new(string: "the string", int: 123)
|
65
|
+
a.copy(non_existing: "the other string")
|
70
66
|
end
|
71
67
|
end
|
72
|
-
|
73
68
|
end
|
74
69
|
|
75
|
-
class
|
70
|
+
class DataClassJson < Test::Unit::TestCase
|
76
71
|
def test_serialize_object
|
77
72
|
assert_equal '{"string":"the string","int":123}', Jsoner.to_json(TheClass, TheClass.new(string: "the string", int: 123)), "nil should be serializable to JSON"
|
78
73
|
end
|
79
74
|
|
75
|
+
def test_deserialize_object
|
76
|
+
data = Jsoner.from_json(TheClass, '{"string": "the string", "int": 123}')
|
77
|
+
T.check(TheClass, data)
|
78
|
+
assert_equal TheClass.new(string: "the string", int: 123), data, "Should parse data class object"
|
79
|
+
end
|
80
|
+
|
80
81
|
def test_serialize_array_of_objects
|
81
82
|
assert_equal '[{"string":"the string","int":123},{"string":"the string 2","int":456}]', Jsoner.to_json(T.array(TheClass), [TheClass.new(string: "the string", int: 123), TheClass.new(string: "the string 2", int: 456)]), "Array of objects should be serializable to JSON"
|
82
83
|
end
|
83
|
-
end
|
84
84
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
assert_equal "the string", a.string
|
90
|
-
assert_equal "the other string", b.string
|
85
|
+
def test_deserialize_nested_object
|
86
|
+
data = Jsoner.from_json(TheClassWithNested, '{"nested": {"string": "the string", "int": 123}}')
|
87
|
+
T.check(TheClassWithNested, data)
|
88
|
+
assert_equal TheClassWithNested.new(nested: TheClass.new(string: "the string", int: 123)), data, "Should parse nested data class object"
|
91
89
|
end
|
92
90
|
|
93
|
-
def
|
94
|
-
assert_raise
|
95
|
-
|
96
|
-
a.copy(non_existing: "the other string")
|
91
|
+
def test_deserialize_object_fail
|
92
|
+
assert_raise JsonerError do
|
93
|
+
Jsoner.from_json(TheClass, '"string"')
|
97
94
|
end
|
98
95
|
end
|
99
96
|
end
|
data/test/enum_test.rb
CHANGED
data/test/jsoner_test.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require "test/unit/runner/junitxml"
|
2
2
|
|
3
|
-
require
|
3
|
+
require "emery"
|
4
4
|
|
5
5
|
class PlainTypesDeserialization < Test::Unit::TestCase
|
6
6
|
def test_deserialize_integer
|
@@ -162,7 +162,7 @@ class PlainTypesSerialization < Test::Unit::TestCase
|
|
162
162
|
end
|
163
163
|
|
164
164
|
def test_serialize_nil
|
165
|
-
json_str = Jsoner.to_json(T.nilable(
|
165
|
+
json_str = Jsoner.to_json(T.nilable(Unknown), nil)
|
166
166
|
assert_equal "null", json_str, "nil should be serializable to JSON"
|
167
167
|
end
|
168
168
|
|
@@ -219,67 +219,46 @@ class HashSerialization < Test::Unit::TestCase
|
|
219
219
|
end
|
220
220
|
end
|
221
221
|
|
222
|
-
class
|
222
|
+
class UnknownDeserialization < Test::Unit::TestCase
|
223
223
|
def test_deserialize_hash
|
224
|
-
data = Jsoner.from_json(
|
224
|
+
data = Jsoner.from_json(Unknown, '{"one":123,"two":"some string"}')
|
225
225
|
assert_equal ({"one" => 123, "two" => "some string"}), data
|
226
226
|
end
|
227
227
|
|
228
228
|
def test_deserialize_array
|
229
|
-
data = Jsoner.from_json(
|
229
|
+
data = Jsoner.from_json(Unknown, '[123,"some string"]')
|
230
230
|
assert_equal [123, "some string"], data
|
231
231
|
end
|
232
232
|
end
|
233
233
|
|
234
|
-
class
|
234
|
+
class UnknownSerialization < Test::Unit::TestCase
|
235
235
|
def test_serialize_hash
|
236
|
-
assert_equal '{"one":123,"two":"some string"}', Jsoner.to_json(
|
236
|
+
assert_equal '{"one":123,"two":"some string"}', Jsoner.to_json(Unknown, { "one" => 123, "two" => "some string"})
|
237
237
|
end
|
238
238
|
|
239
239
|
def test_serialize_array
|
240
|
-
assert_equal '[123,"some string"]', Jsoner.to_json(
|
240
|
+
assert_equal '[123,"some string"]', Jsoner.to_json(Unknown, [123, "some string"])
|
241
241
|
end
|
242
242
|
end
|
243
243
|
|
244
|
-
class
|
244
|
+
class UnionDeserialization < Test::Unit::TestCase
|
245
245
|
def test_deserialize
|
246
|
-
data = Jsoner.from_json(T.
|
247
|
-
T.check(T.
|
246
|
+
data = Jsoner.from_json(T.union(String, Integer), '"the string"')
|
247
|
+
T.check(T.union(String, Integer), data)
|
248
248
|
assert_equal "the string", data, "Should parse any type"
|
249
249
|
end
|
250
250
|
|
251
251
|
def test_deserialize_fail
|
252
252
|
assert_raise JsonerError do
|
253
|
-
Jsoner.from_json(T.
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
class AnySerialization < Test::Unit::TestCase
|
259
|
-
def test_serialize
|
260
|
-
data = Jsoner.to_json(T.any(String, Integer), "the string")
|
261
|
-
T.check(T.any(String, Integer), data)
|
262
|
-
assert_equal '"the string"', data, "Should serialize any type"
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
class UnionDeserialization < Test::Unit::TestCase
|
267
|
-
def test_deserialize
|
268
|
-
data = Jsoner.from_json(T.union(str: String, int: Integer), '{"str":"the string"}')
|
269
|
-
T.check(T.union(str: String, int: Integer), data)
|
270
|
-
assert_equal "the string", data, "Should parse union type"
|
271
|
-
end
|
272
|
-
|
273
|
-
def test_deserialize_any_fail
|
274
|
-
assert_raise JsonerError do
|
275
|
-
Jsoner.from_json(T.union(str: String, int: Integer), '{"bool":true}')
|
253
|
+
Jsoner.from_json(T.union(Integer, Float), '"the string"')
|
276
254
|
end
|
277
255
|
end
|
278
256
|
end
|
279
257
|
|
280
258
|
class UnionSerialization < Test::Unit::TestCase
|
281
259
|
def test_serialize
|
282
|
-
data = Jsoner.to_json(T.union(
|
283
|
-
|
260
|
+
data = Jsoner.to_json(T.union(String, Integer), "the string")
|
261
|
+
T.check(T.union(String, Integer), data)
|
262
|
+
assert_equal '"the string"', data, "Should serialize any type"
|
284
263
|
end
|
285
264
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require "test/unit/runner/junitxml"
|
2
|
+
|
3
|
+
require "emery"
|
4
|
+
|
5
|
+
class TaggedUnionTypeEquality < Test::Unit::TestCase
|
6
|
+
def test_equals
|
7
|
+
assert_true Shape == Shape
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_not_equals
|
11
|
+
assert_false Shape == SmoothShape
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class TaggedUnionTags < Test::Unit::TestCase
|
16
|
+
def test_tags_meta
|
17
|
+
assert_equal ({:circle => Circle, :square => Square}), Shape.typed_tags, "Tags with types should be available on tagged union"
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_read_tag
|
21
|
+
u = Shape.new(circle: Circle.new(radius: 123))
|
22
|
+
assert_equal :circle, u.current_tag, "Current tag should be readable"
|
23
|
+
assert_equal Circle.new(radius: 123), u.current_value, "Current value should be readable"
|
24
|
+
assert_equal Circle.new(radius: 123), u.circle, "Current union tag should allow to read it's data"
|
25
|
+
assert_equal nil, u.square, "Non current tag should be nil"
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_set_more_then_one_tag
|
29
|
+
assert_raise ArgumentError do
|
30
|
+
Shape.new(circle: Circle.new(radius: 123), square: Square.new(side: 123))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class TypeCheckTaggedUnion < Test::Unit::TestCase
|
36
|
+
def test_success
|
37
|
+
assert_equal(Shape.new(circle: Circle.new(radius: 123)), T.check(Shape, Shape.new(circle: Circle.new(radius: 123))))
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_fail
|
41
|
+
assert_raise TypeError do
|
42
|
+
T.check(Shape, 123)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class TaggedUnionJson < Test::Unit::TestCase
|
48
|
+
def test_serialize_wrapper
|
49
|
+
data = Jsoner.to_json(Shape, Shape.new(circle: Circle.new(radius: 123)))
|
50
|
+
assert_equal '{"circle":{"radius":123}}', data, "Should serialize tagged union type to wrapper object"
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_deserialize_wrapper
|
54
|
+
data = Jsoner.from_json(Shape, '{"circle":{"radius":123}}')
|
55
|
+
T.check(Shape, data)
|
56
|
+
assert_equal Shape.new(circle: Circle.new(radius: 123)), data, "Should deserialize tagged union type from wrapper object"
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_deserialize_wrapper_fail
|
60
|
+
assert_raise JsonerError do
|
61
|
+
Jsoner.from_json(Shape, '{"non_exisiting":{"radius":123}}')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_serialize_discriminator
|
66
|
+
data = Jsoner.to_json(SmoothShape, SmoothShape.new(circle: Circle.new(radius: 123)))
|
67
|
+
assert_equal '{"radius":123,"_type":"circle"}', data, "Should serialize tagged union to object with discriminator"
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_deserialize_discriminator
|
71
|
+
data = Jsoner.from_json(SmoothShape, '{"_type":"circle","radius":123}')
|
72
|
+
T.check(SmoothShape, data)
|
73
|
+
assert_equal SmoothShape.new(circle: Circle.new(radius: 123)), data, "Should deserialize tagged union type from object with discriminator"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class Circle
|
78
|
+
include DataClass
|
79
|
+
val :radius, Integer
|
80
|
+
end
|
81
|
+
|
82
|
+
class Square
|
83
|
+
include DataClass
|
84
|
+
val :side, Integer
|
85
|
+
end
|
86
|
+
|
87
|
+
class Shape
|
88
|
+
include TaggedUnion
|
89
|
+
tag :circle, Circle
|
90
|
+
tag :square, Square
|
91
|
+
end
|
92
|
+
|
93
|
+
class Oval
|
94
|
+
include DataClass
|
95
|
+
val :height, Integer
|
96
|
+
val :width, Integer
|
97
|
+
end
|
98
|
+
|
99
|
+
class SmoothShape
|
100
|
+
include TaggedUnion
|
101
|
+
with_discriminator "_type"
|
102
|
+
tag :circle, Circle
|
103
|
+
tag :oval, Oval
|
104
|
+
end
|
data/test/type_test.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "test/unit/runner/junitxml"
|
2
2
|
require "date"
|
3
3
|
|
4
|
-
require
|
4
|
+
require "emery"
|
5
5
|
|
6
6
|
class TypeEquality < Test::Unit::TestCase
|
7
7
|
def test_plain_equals
|
@@ -21,7 +21,7 @@ class TypeEquality < Test::Unit::TestCase
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def test_untyped_equals
|
24
|
-
assert_true
|
24
|
+
assert_true Unknown == Unknown
|
25
25
|
end
|
26
26
|
|
27
27
|
def test_nilable_equals
|
@@ -56,16 +56,16 @@ class TypeEquality < Test::Unit::TestCase
|
|
56
56
|
assert_false T.hash(String, Integer) == T.hash(String, String)
|
57
57
|
end
|
58
58
|
|
59
|
-
def
|
60
|
-
assert_true T.
|
59
|
+
def test_union_equals
|
60
|
+
assert_true T.union(String, Integer) == T.union(String, Integer)
|
61
61
|
end
|
62
62
|
|
63
|
-
def
|
64
|
-
assert_true T.
|
63
|
+
def test_union_equals_other_order
|
64
|
+
assert_true T.union(String, Integer) == T.union(Integer, String)
|
65
65
|
end
|
66
66
|
|
67
|
-
def
|
68
|
-
assert_false T.
|
67
|
+
def test_union_of_other_type
|
68
|
+
assert_false T.union(String, Integer) == T.union(Integer, Float)
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
@@ -82,12 +82,8 @@ class TypeToString < Test::Unit::TestCase
|
|
82
82
|
assert_equal "Hash[String, Integer]", T.hash(String, Integer).to_s
|
83
83
|
end
|
84
84
|
|
85
|
-
def test_any
|
86
|
-
assert_equal "Any[String, Integer]", T.any(String, Integer).to_s
|
87
|
-
end
|
88
|
-
|
89
85
|
def test_union
|
90
|
-
assert_equal "Union[
|
86
|
+
assert_equal "Union[String, Integer]", T.union(String, Integer).to_s
|
91
87
|
end
|
92
88
|
end
|
93
89
|
|
@@ -146,12 +142,12 @@ class TypeCheck < Test::Unit::TestCase
|
|
146
142
|
end
|
147
143
|
|
148
144
|
def test_untyped_success
|
149
|
-
assert_equal "bla", T.check(
|
145
|
+
assert_equal "bla", T.check(Unknown, "bla"), "Untyped should accept strings"
|
150
146
|
end
|
151
147
|
|
152
148
|
def test_untyped_nil
|
153
149
|
assert_raise TypeError do
|
154
|
-
T.check(
|
150
|
+
T.check(Unknown, nil)
|
155
151
|
end
|
156
152
|
end
|
157
153
|
|
@@ -189,30 +185,18 @@ class TypeCheckHash < Test::Unit::TestCase
|
|
189
185
|
end
|
190
186
|
|
191
187
|
def test_hash_string_to_untyped
|
192
|
-
assert_equal({"key" => "the value"}, T.check(T.hash(String,
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
class TypeCheckAny < Test::Unit::TestCase
|
197
|
-
def test_success
|
198
|
-
assert_equal(123, T.check(T.any(String, Integer), 123), "Any of String, Integer should allow Integer value")
|
199
|
-
end
|
200
|
-
|
201
|
-
def test_fail
|
202
|
-
assert_raise TypeError do
|
203
|
-
T.check(T.any(String, Integer), true)
|
204
|
-
end
|
188
|
+
assert_equal({"key" => "the value"}, T.check(T.hash(String, Unknown), { "key" => "the value"}), "Hash of String -> Untyped should allow String -> String value")
|
205
189
|
end
|
206
190
|
end
|
207
191
|
|
208
192
|
class TypeCheckUnion < Test::Unit::TestCase
|
209
193
|
def test_success
|
210
|
-
assert_equal(123, T.check(T.union(
|
194
|
+
assert_equal(123, T.check(T.union(String, Integer), 123), "Any of String, Integer should allow Integer value")
|
211
195
|
end
|
212
196
|
|
213
197
|
def test_fail
|
214
198
|
assert_raise TypeError do
|
215
|
-
T.check(T.union(
|
199
|
+
T.check(T.union(String, Integer), true)
|
216
200
|
end
|
217
201
|
end
|
218
202
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: emery
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.27
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Sapronov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -31,13 +31,16 @@ extensions: []
|
|
31
31
|
extra_rdoc_files: []
|
32
32
|
files:
|
33
33
|
- lib/emery.rb
|
34
|
+
- lib/emery/codecs.rb
|
34
35
|
- lib/emery/dataclass.rb
|
35
36
|
- lib/emery/enum.rb
|
36
37
|
- lib/emery/jsoner.rb
|
38
|
+
- lib/emery/taggedunion.rb
|
37
39
|
- lib/emery/type.rb
|
38
40
|
- test/dataclass_test.rb
|
39
41
|
- test/enum_test.rb
|
40
42
|
- test/jsoner_test.rb
|
43
|
+
- test/taggedunion_test.rb
|
41
44
|
- test/type_test.rb
|
42
45
|
homepage: https://github.com/vsapronov/emery
|
43
46
|
licenses:
|