emery 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/emery/type.rb ADDED
@@ -0,0 +1,176 @@
1
+ module Emery
2
+ module T
3
+ def T.check_not_nil(type, value)
4
+ if value == nil
5
+ raise TypeError.new("Type #{type.to_s} does not allow nil value")
6
+ end
7
+ end
8
+
9
+ class UntypedType
10
+ def to_s
11
+ "Untyped"
12
+ end
13
+ def check(value)
14
+ T.check_not_nil(self, value)
15
+ end
16
+ end
17
+
18
+ class Nilable
19
+ attr_reader :type
20
+ def initialize(type)
21
+ @type = type
22
+ end
23
+ def to_s
24
+ "Nilable[#{type.to_s}]"
25
+ end
26
+ def check(value)
27
+ if value != nil
28
+ T.check(type, value)
29
+ end
30
+ end
31
+ def ==(other)
32
+ T.instance_of?(Nilable, other) and self.type == other.type
33
+ end
34
+ end
35
+
36
+ class AnyType
37
+ attr_reader :types
38
+ def initialize(*types)
39
+ @types = types
40
+ end
41
+ def to_s
42
+ "Any[#{types.map { |t| t.to_s}.join(', ')}]"
43
+ end
44
+ def check(value)
45
+ types.each do |type|
46
+ begin
47
+ T.check(type, value)
48
+ return
49
+ rescue TypeError
50
+ end
51
+ end
52
+ raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - any of #{@types.map { |t| t.to_s}.join(', ')} required")
53
+ end
54
+ def ==(other)
55
+ T.instance_of?(AnyType, other) and (self.types - other.types).empty?
56
+ end
57
+ end
58
+
59
+ class ArrayType
60
+ attr_reader :item_type
61
+ def initialize(item_type)
62
+ @item_type = item_type
63
+ end
64
+ def to_s
65
+ "Array[#{item_type.to_s}]"
66
+ end
67
+ def check(value)
68
+ T.check_not_nil(self, value)
69
+ if !value.is_a? Array
70
+ raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - Array is required")
71
+ end
72
+ value.each { |item_value| T.check(item_type, item_value) }
73
+ end
74
+ def ==(other)
75
+ T.instance_of?(ArrayType, other) and self.item_type == other.item_type
76
+ end
77
+ end
78
+
79
+ class HashType
80
+ attr_reader :key_type
81
+ attr_reader :value_type
82
+ def initialize(key_type, value_type)
83
+ @key_type = key_type
84
+ @value_type = value_type
85
+ end
86
+ def to_s
87
+ "Hash[#{@key_type.to_s}, #{@value_type.to_s}]"
88
+ end
89
+ def check(value)
90
+ T.check_not_nil(self, value)
91
+ if !value.is_a? Hash
92
+ raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - Hash is required")
93
+ end
94
+ value.each do |item_key, item_value|
95
+ T.check(@key_type, item_key)
96
+ T.check(@value_type, item_value)
97
+ end
98
+ end
99
+ def ==(other)
100
+ T.instance_of?(HashType, other) and self.key_type == other.key_type and self.value_type == other.value_type
101
+ end
102
+ end
103
+
104
+ class StringFormatted
105
+ attr_reader :regex
106
+ def initialize(regex)
107
+ @regex = regex
108
+ end
109
+ def to_s
110
+ "String<#@regex>"
111
+ end
112
+ def check(value)
113
+ T.check_not_nil(self, value)
114
+ if !value.is_a? String
115
+ raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - String is required for StringFormatted")
116
+ end
117
+ if !@regex.match?(value)
118
+ raise TypeError.new("Value '#{value.inspect.to_s}' is not in required format '#{@regex}'")
119
+ end
120
+ end
121
+ end
122
+
123
+ def T.check(type, value)
124
+ if type.methods.include? :check
125
+ type.check(value)
126
+ else
127
+ if type != NilClass
128
+ T.check_not_nil(type, value)
129
+ end
130
+ if !value.is_a? type
131
+ raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - #{type} is required")
132
+ end
133
+ end
134
+ return value
135
+ end
136
+
137
+ def T.instance_of?(type, value)
138
+ begin
139
+ T.check(type, value)
140
+ true
141
+ rescue TypeError
142
+ false
143
+ end
144
+ end
145
+
146
+ def T.nilable(value_type)
147
+ Nilable.new(value_type)
148
+ end
149
+
150
+ def T.array(item_type)
151
+ ArrayType.new(item_type)
152
+ end
153
+
154
+ def T.hash(key_type, value_type)
155
+ HashType.new(key_type, value_type)
156
+ end
157
+
158
+ def T.any(*typdefs)
159
+ AnyType.new(*typdefs)
160
+ end
161
+
162
+ def T.check_var(var_name, type, value)
163
+ begin
164
+ check(type, value)
165
+ return value
166
+ rescue TypeError => e
167
+ raise TypeError.new("Variable #{var_name} type check failed, expected type: #{type.to_s}, value: #{value}")
168
+ end
169
+ end
170
+ end
171
+
172
+ Boolean = T.any(TrueClass, FalseClass)
173
+ Untyped = T::UntypedType.new
174
+ NilableUntyped = T.nilable(Untyped)
175
+ UUID = T::StringFormatted.new(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
176
+ end
@@ -0,0 +1,115 @@
1
+ require "test/unit/runner/junitxml"
2
+
3
+ require 'emery/dataclass'
4
+ require 'emery/jsoner'
5
+
6
+ module Emery
7
+ class DataClassTypeEquality < Test::Unit::TestCase
8
+ def test_equals
9
+ assert_true TheClass == TheClass
10
+ end
11
+
12
+ def test_not_equals
13
+ assert_false TheClass == TheClassWithNested
14
+ end
15
+ end
16
+
17
+ class DataClassFields < Test::Unit::TestCase
18
+ def test_fields_meta
19
+ assert_equal ({:string => String, :int => Integer}), TheClass.json_attributes, "Attributes with types should be available on data class"
20
+ end
21
+
22
+ def test_read
23
+ obj = TheClass.new(string: "the string", int: 123)
24
+ assert_equal "the string", obj.string, "Immutable field should be readable"
25
+ assert_equal 123, obj.int, "Mutable field should be readable"
26
+ end
27
+
28
+ def test_write_mutable
29
+ obj = TheClass.new(string: "the string", int: 123)
30
+ obj.int = 124
31
+ assert_equal 124, obj.int, "Mutable field should be writable"
32
+ end
33
+
34
+ def test_write_immutable
35
+ obj = TheClass.new(string: "the string", int: 123)
36
+ assert_raise NoMethodError do
37
+ obj.string = "the other string"
38
+ end
39
+ end
40
+ end
41
+
42
+ class DataClassEquality < Test::Unit::TestCase
43
+ def test_nil
44
+ assert_not_equal nil, TheClass.new(string: "the string", int: 123), "Object should not be equal to nil"
45
+ end
46
+
47
+ def test_same_fields_values
48
+ assert_equal TheClass.new(string: "the string", int: 123), TheClass.new(string: "the string", int: 123), "Objects with same fields should be equal"
49
+ end
50
+
51
+ def test_different_fields_values
52
+ assert_not_equal TheClass.new(string: "the string 2", int: 123), TheClass.new(string: "the string", int: 123), "Objects with different fields should not be equal"
53
+ end
54
+ end
55
+
56
+ class DataClassDeserialization < Test::Unit::TestCase
57
+ def test_deserialize_object
58
+ data = Jsoner.from_json(TheClass, '{"string": "the string", "int": 123}')
59
+ T.check(TheClass, data)
60
+ assert_equal TheClass.new(string: "the string", int: 123), data, "Should parse data class object"
61
+ end
62
+
63
+ def test_deserialize_nested_object
64
+ data = Jsoner.from_json(TheClassWithNested, '{"nested": {"string": "the string", "int": 123}}')
65
+ T.check(TheClassWithNested, data)
66
+ assert_equal TheClassWithNested.new(nested: TheClass.new(string: "the string", int: 123)), data, "Should parse nested data class object"
67
+ end
68
+
69
+ def test_deserialize_object_fail
70
+ assert_raise JsonerError do
71
+ Jsoner.from_json(TheClass, '"string"')
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ class DataClassSerialization < Test::Unit::TestCase
78
+ def test_serialize_object
79
+ assert_equal '{"string":"the string","int":123}', Jsoner.to_json(TheClass, TheClass.new(string: "the string", int: 123)), "nil should be serializable to JSON"
80
+ end
81
+
82
+ def test_serialize_array_of_objects
83
+ 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"
84
+ end
85
+ end
86
+
87
+ class DataClassCopy < Test::Unit::TestCase
88
+ def test_copy
89
+ a = TheClass.new(string: "the string", int: 123)
90
+ b = a.copy(string: "the other string")
91
+ assert_equal "the string", a.string
92
+ assert_equal "the other string", b.string
93
+ end
94
+
95
+ def test_copy_non_existing_field
96
+ assert_raise TypeError do
97
+ a = TheClass.new(string: "the string", int: 123)
98
+ a.copy(non_existing: "the other string")
99
+ end
100
+ end
101
+ end
102
+
103
+ class TheClass
104
+ include DataClass
105
+
106
+ val :string, String
107
+ var :int, Integer
108
+ end
109
+
110
+ class TheClassWithNested
111
+ include DataClass
112
+
113
+ val :nested, TheClass
114
+ end
115
+ end
data/test/enum_test.rb ADDED
@@ -0,0 +1,47 @@
1
+ require "test/unit/runner/junitxml"
2
+
3
+ require 'emery/type'
4
+ require 'emery/enum'
5
+ require 'emery/jsoner'
6
+
7
+ module Emery
8
+ class TypeCheckEnum < Test::Unit::TestCase
9
+ def test_success
10
+ assert_equal SomeEnum::one, T.check(SomeEnum, SomeEnum::one), "Plain enum type should pass type check"
11
+ end
12
+
13
+ def test_fail
14
+ assert_raise TypeError do
15
+ T.check(SomeEnum, "non existing")
16
+ end
17
+ end
18
+ end
19
+
20
+ class EnumDeserialization < Test::Unit::TestCase
21
+ def test_deserialize_enum
22
+ data = Jsoner.from_json(SomeEnum, '"two"')
23
+ T.check(SomeEnum, data)
24
+ assert_equal SomeEnum::two, data, "Enum should be parsable from JSON"
25
+ end
26
+
27
+ def test_deserialize_enum_non_existing_item
28
+ assert_raise JsonerError do
29
+ Jsoner.from_json(SomeEnum, '"non_existing"')
30
+ end
31
+ end
32
+ end
33
+
34
+ class EnumSerialization < Test::Unit::TestCase
35
+ def test_serialize_enum
36
+ json_str = Jsoner.to_json(SomeEnum, SomeEnum::two)
37
+ assert_equal '"two"', json_str, "Enum should be serializable to JSON"
38
+ end
39
+ end
40
+
41
+ class SomeEnum
42
+ include Enum
43
+
44
+ define :one, 'one'
45
+ define :two, 'two'
46
+ end
47
+ end
@@ -0,0 +1,239 @@
1
+ require "test/unit/runner/junitxml"
2
+
3
+ require 'emery/type'
4
+ require 'emery/jsoner'
5
+
6
+ module Emery
7
+ class PlainTypesDeserialization < Test::Unit::TestCase
8
+ def test_deserialize_integer
9
+ data = Jsoner.from_json(Integer, '123')
10
+ T.check(Integer, data)
11
+ assert_equal 123, data, "Integer should be parsable from JSON"
12
+ end
13
+
14
+ def test_deserialize_integer_fail
15
+ assert_raise JsonerError do
16
+ Jsoner.from_json(Integer, '"abc"')
17
+ end
18
+ end
19
+
20
+ def test_deserialize_float
21
+ data = Jsoner.from_json(Float, '1.23')
22
+ T.check(Float, data)
23
+ assert_equal 1.23, data, "Float should be parsable from JSON"
24
+ end
25
+
26
+ def test_deserialize_float_from_integer
27
+ data = Jsoner.from_json(Float, '123')
28
+ T.check(Float, data)
29
+ assert_equal 123.0, data, "Float should be parsable from JSON"
30
+ end
31
+
32
+ def test_deserialize_float_fail
33
+ assert_raise JsonerError do
34
+ Jsoner.from_json(Float, '"abc"')
35
+ end
36
+ end
37
+
38
+ def test_deserialize_boolean
39
+ data = Jsoner.from_json(Boolean, 'true')
40
+ T.check(Boolean, data)
41
+ assert_equal true, data, "Boolean should be parsable from JSON"
42
+ end
43
+
44
+ def test_deserialize_boolean_fail
45
+ assert_raise JsonerError do
46
+ Jsoner.from_json(Boolean, '1')
47
+ end
48
+ end
49
+
50
+ def test_deserialize_string
51
+ data = Jsoner.from_json(String, '"the string"')
52
+ T.check(String, data)
53
+ assert_equal "the string", data, "String should be parsable from JSON"
54
+ end
55
+
56
+ def test_deserialize_string_fail
57
+ assert_raise JsonerError do
58
+ Jsoner.from_json(String, '123')
59
+ end
60
+ end
61
+
62
+ def test_deserialize_uuid
63
+ data = Jsoner.from_json(UUID, '"58d5e212-165b-4ca0-909b-c86b9cee0111"')
64
+ T.check(UUID, data)
65
+ assert_equal "58d5e212-165b-4ca0-909b-c86b9cee0111", data, "String should be parsable from JSON"
66
+ end
67
+
68
+ def test_deserialize_uuid_fail
69
+ assert_raise JsonerError do
70
+ Jsoner.from_json(UUID, '"abc"')
71
+ end
72
+ end
73
+
74
+ def test_deserialize_date
75
+ data = Jsoner.from_json(Date, '"2019-11-30"')
76
+ T.check(Date, data)
77
+ assert_equal Date.new(2019, 11, 30), data, "Date should be parsable from JSON"
78
+ end
79
+
80
+ def test_deserialize_date_wrong_format
81
+ assert_raise JsonerError do
82
+ Jsoner.from_json(Date, '"11/30/2019"')
83
+ end
84
+ end
85
+
86
+ def test_deserialize_datetime
87
+ data = Jsoner.from_json(DateTime, '"2019-11-30T17:45:55+00:00"')
88
+ T.check(DateTime, data)
89
+ assert_equal DateTime.new(2019, 11, 30, 17, 45, 55), data, "DateTime should be parsable from JSON"
90
+ end
91
+
92
+ def test_deserialize_datetime_wrong_format
93
+ assert_raise JsonerError do
94
+ Jsoner.from_json(DateTime, '"2019-11-30 17:45:55"')
95
+ end
96
+ end
97
+
98
+ def test_deserialize_nilable_nil
99
+ data = Jsoner.from_json(T.nilable(String), 'null')
100
+ T.check(T.nilable(String), data)
101
+ assert_equal nil, data, "Nilable null value should be parsable from JSON"
102
+ end
103
+
104
+ def test_deserialize_nilable_string
105
+ data = Jsoner.from_json(T.nilable(String), '"some string"')
106
+ T.check(T.nilable(String), data)
107
+ assert_equal "some string", data, "Nilable non-null value should be parsable from JSON"
108
+ end
109
+
110
+ def test_deserialize_non_nilable
111
+ assert_raise JsonerError do
112
+ Jsoner.from_json(String, 'null')
113
+ end
114
+ end
115
+ end
116
+
117
+ class PlainTypesSerialization < Test::Unit::TestCase
118
+ def test_serialize_integer
119
+ assert_equal "123", Jsoner.to_json(Integer,123), "Integer should be serializable to JSON"
120
+ end
121
+
122
+ def test_serialize_integer_fail
123
+ assert_raise JsonerError do
124
+ Jsoner.to_json(Integer,123.4)
125
+ end
126
+ end
127
+
128
+ def test_serialize_float
129
+ assert_equal "12.3", Jsoner.to_json(Float,12.3), "Float should be serializable to JSON"
130
+ end
131
+
132
+ def test_serialize_float_fail
133
+ assert_raise JsonerError do
134
+ Jsoner.to_json(Float,123)
135
+ end
136
+ end
137
+
138
+ def test_serialize_boolean
139
+ assert_equal "true", Jsoner.to_json(Boolean, true), "Boolean should be serializable to JSON"
140
+ end
141
+
142
+ def test_serialize_string
143
+ assert_equal '"the string"', Jsoner.to_json(String,"the string"), "String should be serializable to JSON"
144
+ end
145
+
146
+ def test_serialize_date
147
+ assert_equal '"2019-11-30"', Jsoner.to_json(Date, Date.new(2019, 11, 30)), "Date should be serializable to JSON"
148
+ end
149
+
150
+ def test_serialize_date_fail
151
+ assert_raise JsonerError do
152
+ Jsoner.to_json(Date, 123)
153
+ end
154
+ end
155
+
156
+ def test_serialize_datetime
157
+ assert_equal '"2019-11-30T17:45:55"', Jsoner.to_json(DateTime, DateTime.new(2019, 11, 30, 17, 45, 55)), "DateTime should be serializable to JSON"
158
+ end
159
+
160
+ def test_serialize_nil
161
+ json_str = Jsoner.to_json(T.nilable(Untyped), nil)
162
+ assert_equal "null", json_str, "nil should be serializable to JSON"
163
+ end
164
+
165
+ def test_serialize_uuid
166
+ assert_equal '"58d5e212-165b-4ca0-909b-c86b9cee0111"', Jsoner.to_json(UUID, "58d5e212-165b-4ca0-909b-c86b9cee0111"), "UUID should be serializable to JSON"
167
+ end
168
+ end
169
+
170
+ class ArrayDeserialization < Test::Unit::TestCase
171
+ def test_deserialize_array
172
+ data = Jsoner.from_json(T.array(String), '["the string", "the other string"]')
173
+ T.check(T.array(String), data)
174
+ assert_equal ["the string", "the other string"], data, "Should parse array of strings"
175
+ end
176
+
177
+ def test_deserialize_array_of_datetime
178
+ data = Jsoner.from_json(T.array(DateTime), '["2019-11-30T17:45:55+00:00"]')
179
+ T.check(T.array(DateTime), data)
180
+ assert_equal [DateTime.new(2019, 11, 30, 17, 45, 55)], data, "Should parse array of DateTime"
181
+ end
182
+ end
183
+
184
+ class ArraySerialization < Test::Unit::TestCase
185
+ def test_serialize_array
186
+ assert_equal '["the string","the other string"]', Jsoner.to_json(T.array(String), ["the string", "the other string"]), "Array should be serializable to JSON"
187
+ end
188
+
189
+ def test_serialize_array_of_datetime
190
+ assert_equal '["2019-11-30T17:45:55"]', Jsoner.to_json(T.array(DateTime), [DateTime.new(2019, 11, 30, 17, 45, 55)]), "Array should be serializable to JSON"
191
+ end
192
+ end
193
+
194
+ class HashDeserialization < Test::Unit::TestCase
195
+ def test_deserialize_hash
196
+ data = Jsoner.from_json(T.hash(String, Integer), '{"one": 123, "two": 456}')
197
+ T.check(T.hash(String, Integer), data)
198
+ assert_equal ({"one" => 123, "two" => 456}), data, "Should parse hash"
199
+ end
200
+
201
+ def test_deserialize_hash_string_to_datetime
202
+ data = Jsoner.from_json(T.hash(String, DateTime), '{"one": "2019-11-30T17:45:55+00:00"}')
203
+ T.check(T.hash(String, DateTime), data)
204
+ assert_equal ({"one" => DateTime.new(2019, 11, 30, 17, 45, 55)}), data, "Should parse hash"
205
+ end
206
+ end
207
+
208
+ class HashSerialization < Test::Unit::TestCase
209
+ def test_serialize_hash
210
+ assert_equal '{"one":123,"two":456}', Jsoner.to_json(T.hash(String, Integer), {"one" => 123, "two" => 456}), "Hash should be serializable to JSON"
211
+ end
212
+
213
+ def test_serialize_hash_string_to_datetime
214
+ assert_equal '{"one":"2019-11-30T17:45:55"}', Jsoner.to_json(T.hash(String, DateTime), {"one" => DateTime.new(2019, 11, 30, 17, 45, 55)}), "Hash should be serializable to JSON"
215
+ end
216
+ end
217
+
218
+ class AnyDeserialization < Test::Unit::TestCase
219
+ def test_deserialize_any
220
+ data = Jsoner.from_json(T.any(String, Integer), '"the string"')
221
+ T.check(T.any(String, Integer), data)
222
+ assert_equal "the string", data, "Should parse any type"
223
+ end
224
+
225
+ def test_deserialize_any_fail
226
+ assert_raise JsonerError do
227
+ Jsoner.from_json(T.any(Integer, Float), '"the string"')
228
+ end
229
+ end
230
+ end
231
+
232
+ class AnySerialization < Test::Unit::TestCase
233
+ def test_serialize_any
234
+ data = Jsoner.to_json(T.any(String, Integer), "the string")
235
+ T.check(T.any(String, Integer), data)
236
+ assert_equal '"the string"', data, "Should serialize any type"
237
+ end
238
+ end
239
+ end