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