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 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: