emery 0.0.1

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