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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f535777bdcdeeb081d54dfa26b70b4c7d1b723f736dcf30cdea65773d046524
4
- data.tar.gz: 2147536d60c93be8a684f8057d56bf0f4da6e7f35c81b6ada80ffbcb69ca6a7e
3
+ metadata.gz: ed5b5ad376a8f3ffef14484fd9f299e0461bdb0b567e76b5e8e6fb913c6dcc71
4
+ data.tar.gz: '09ed0d4e68b06d7353852ce79b7d7fa93de1ea97cdf38f32bf9c8edc08e77fd6'
5
5
  SHA512:
6
- metadata.gz: 1883cb44758c3e2b2490a8eefded6a6c806ab25fd34045d058130cbd186304808168f67e509ed1fc05a5463e395df680f1b70dd2cd94a2363b905a344187716d
7
- data.tar.gz: d1c401503e30f85992058692441d5cdf4e59cb5d3c151380cf326b28d5cc88e18eb04bef8a84a429982dcc535994afbd7160a760784993db76e24eeed1cf9e80
6
+ metadata.gz: e1a8443886e984bf7fe51196f7df9b7d7b5a57378d23b699f9adfa14ce691b35e41b71d027526f5fccbc49b54a602f168d19b6e4e4dc37060e985873379ee267
7
+ data.tar.gz: '0282d146e1a49b4ad3608e0378d8688373e55b6e20c48442f7d783afa75a6747619fdc2a87f27cd7309a47ad60a50b3c15366eb6e98a13f076d13b9f44bcd082'
@@ -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
@@ -1,6 +1,6 @@
1
1
  module DataClass
2
2
  def initialize(params)
3
- self.class.json_attributes.each do |attr, attr_type|
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.json_attributes.keys.each do |attr|
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, attr_value|
25
- if !self.class.json_attributes.key?(attr)
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.json_attributes.map do |attr, attr_type|
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 json_attributes
52
- @json_attributes
51
+ def typed_attributes
52
+ @typed_attributes
53
53
  end
54
54
 
55
55
  def val(name, type)
56
- if @json_attributes == nil
57
- @json_attributes = {}
56
+ if @typed_attributes == nil
57
+ @typed_attributes = {}
58
58
  end
59
- @json_attributes[name] = type
59
+ @typed_attributes[name] = type
60
60
  attr_reader name
61
61
  end
62
62
 
63
63
  def var(name, type)
64
- if @json_attributes == nil
65
- @json_attributes = {}
64
+ if @typed_attributes == nil
65
+ @typed_attributes = {}
66
66
  end
67
- @json_attributes[name] = type
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
@@ -21,14 +21,6 @@ module Enum
21
21
  end
22
22
  end
23
23
 
24
- def jsoner_deserialize(json_value)
25
- T.check(self, json_value)
26
- end
27
-
28
- def jsoner_serialize(value)
29
- T.check(self, value)
30
- end
31
-
32
24
  def define(key, value)
33
25
  @_enum_hash ||= {}
34
26
  @_enums_by_value ||= {}
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
- T::StringFormatted.class_eval do
9
- def jsoner_deserialize(json_value)
10
- T.check(self, json_value)
11
- end
12
- def jsoner_serialize(value)
13
- T.check(self, value)
14
- end
15
- end
16
-
17
- T::UntypedType.class_eval do
18
- def jsoner_deserialize(json_value)
19
- json_value
20
- end
21
- def jsoner_serialize(value)
22
- value
23
- end
24
- end
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
- T::UnionType.class_eval do
86
- def jsoner_deserialize(json_value)
87
- if !json_value.is_a?(Hash)
88
- raise JsonerError.new("JSON value type #{json_value.class} is not Hash")
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
- end
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
- if type.methods.include? :jsoner_deserialize
184
- return type.jsoner_deserialize(json_value)
185
- elsif @@serializers.include? type
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
- if type.methods.include? :jsoner_serialize
205
- return type.jsoner_serialize(value)
206
- elsif @@serializers.include? type
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 UntypedType
10
+ class UnknownType
9
11
  def to_s
10
- "Untyped"
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 Nilable
18
- attr_reader :type
19
- def initialize(type)
20
- @type = type
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[#{type.to_s}]"
25
+ "Nilable[#{inner_type.to_s}]"
24
26
  end
25
27
  def check(value)
26
28
  if value != nil
27
- T.check(type, value)
29
+ T.check(inner_type, value)
28
30
  end
29
31
  end
30
32
  def ==(other)
31
- T.instance_of?(Nilable, other) and self.type == other.type
33
+ T.instance_of?(NilableType, other) and self.inner_type == other.inner_type
32
34
  end
33
35
  end
34
36
 
35
- class AnyType
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
- "Any[#{types.map { |t| t.to_s}.join(', ')}]"
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?(AnyType, other) and (self.types - other.types).empty?
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 StringFormatted
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
- "String<#@regex>"
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
- Nilable.new(value_type)
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.any(*typdefs)
165
- AnyType.new(*typdefs)
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.any(TrueClass, FalseClass)
183
- Untyped = T::UntypedType.new
184
- NilableUntyped = T.nilable(Untyped)
185
- UUID = T::StringFormatted.new(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
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
@@ -1,4 +1,6 @@
1
1
  require 'emery/type'
2
- require 'emery/jsoner'
3
2
  require 'emery/enum'
4
- require 'emery/dataclass'
3
+ require 'emery/dataclass'
4
+ require 'emery/taggedunion'
5
+ require 'emery/codecs'
6
+ require 'emery/jsoner'
@@ -1,6 +1,6 @@
1
1
  require "test/unit/runner/junitxml"
2
2
 
3
- require 'emery'
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.json_attributes, "Attributes with types should be available on data class"
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 DataClassDeserialization < Test::Unit::TestCase
55
- def test_deserialize_object
56
- data = Jsoner.from_json(TheClass, '{"string": "the string", "int": 123}')
57
- T.check(TheClass, data)
58
- assert_equal TheClass.new(string: "the string", int: 123), data, "Should parse data class object"
59
- end
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 test_deserialize_object_fail
68
- assert_raise JsonerError do
69
- Jsoner.from_json(TheClass, '"string"')
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 DataClassSerialization < Test::Unit::TestCase
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
- class DataClassCopy < Test::Unit::TestCase
86
- def test_copy
87
- a = TheClass.new(string: "the string", int: 123)
88
- b = a.copy(string: "the other string")
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 test_copy_non_existing_field
94
- assert_raise TypeError do
95
- a = TheClass.new(string: "the string", int: 123)
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
@@ -1,6 +1,6 @@
1
1
  require "test/unit/runner/junitxml"
2
2
 
3
- require 'emery'
3
+ require "emery"
4
4
 
5
5
  class TypeCheckEnum < Test::Unit::TestCase
6
6
  def test_success
data/test/jsoner_test.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require "test/unit/runner/junitxml"
2
2
 
3
- require 'emery'
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(Untyped), nil)
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 UntypedDeserialization < Test::Unit::TestCase
222
+ class UnknownDeserialization < Test::Unit::TestCase
223
223
  def test_deserialize_hash
224
- data = Jsoner.from_json(Untyped, '{"one":123,"two":"some string"}')
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(Untyped, '[123,"some string"]')
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 UntypedSerialization < Test::Unit::TestCase
234
+ class UnknownSerialization < Test::Unit::TestCase
235
235
  def test_serialize_hash
236
- assert_equal '{"one":123,"two":"some string"}', Jsoner.to_json(Untyped, {"one" => 123, "two" => "some string"})
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(Untyped, [123, "some string"])
240
+ assert_equal '[123,"some string"]', Jsoner.to_json(Unknown, [123, "some string"])
241
241
  end
242
242
  end
243
243
 
244
- class AnyDeserialization < Test::Unit::TestCase
244
+ class UnionDeserialization < Test::Unit::TestCase
245
245
  def test_deserialize
246
- data = Jsoner.from_json(T.any(String, Integer), '"the string"')
247
- T.check(T.any(String, Integer), data)
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.any(Integer, Float), '"the string"')
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(str: String, int: Integer), "the string")
283
- assert_equal '{"str":"the string"}', data, "Should serialize union type with wrapping object"
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 'emery'
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 Untyped == Untyped
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 test_any_equals
60
- assert_true T.any(String, Integer) == T.any(String, Integer)
59
+ def test_union_equals
60
+ assert_true T.union(String, Integer) == T.union(String, Integer)
61
61
  end
62
62
 
63
- def test_any_equals_other_order
64
- assert_true T.any(String, Integer) == T.any(Integer, String)
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 test_any_of_other_type
68
- assert_false T.any(String, Integer) == T.any(Integer, Float)
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[str: String, int: Integer]", T.union(str: String, int: Integer).to_s
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(Untyped, "bla"), "Untyped should accept strings"
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(Untyped, nil)
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, Untyped), {"key" => "the value"}), "Hash of String -> Untyped should allow String -> String value")
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(str: String, int: Integer), 123))
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(str: String, int: Integer), true)
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.0.19
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-06-09 00:00:00.000000000 Z
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: