emery 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/emery/dataclass.rb +63 -65
- data/lib/emery/enum.rb +73 -75
- data/lib/emery/jsoner.rb +162 -164
- data/lib/emery/tod.rb +202 -204
- data/lib/emery/type.rb +149 -151
- data/lib/emery.rb +1 -2
- data/test/dataclass_test.rb +80 -83
- data/test/enum_test.rb +29 -33
- data/test/jsoner_test.rb +189 -192
- data/test/tod_test.rb +23 -28
- data/test/type_test.rb +153 -155
- metadata +1 -1
data/lib/emery/type.rb
CHANGED
@@ -1,187 +1,185 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
raise TypeError.new("Type #{type.to_s} does not allow nil value")
|
6
|
-
end
|
1
|
+
module T
|
2
|
+
def T.check_not_nil(type, value)
|
3
|
+
if value == nil
|
4
|
+
raise TypeError.new("Type #{type.to_s} does not allow nil value")
|
7
5
|
end
|
6
|
+
end
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
13
|
-
def check(value)
|
14
|
-
T.check_not_nil(self, value)
|
15
|
-
end
|
8
|
+
class UntypedType
|
9
|
+
def to_s
|
10
|
+
"Untyped"
|
16
11
|
end
|
12
|
+
def check(value)
|
13
|
+
T.check_not_nil(self, value)
|
14
|
+
end
|
15
|
+
end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
end
|
31
|
-
def ==(other)
|
32
|
-
T.instance_of?(Nilable, other) and self.type == other.type
|
17
|
+
class Nilable
|
18
|
+
attr_reader :type
|
19
|
+
def initialize(type)
|
20
|
+
@type = type
|
21
|
+
end
|
22
|
+
def to_s
|
23
|
+
"Nilable[#{type.to_s}]"
|
24
|
+
end
|
25
|
+
def check(value)
|
26
|
+
if value != nil
|
27
|
+
T.check(type, value)
|
33
28
|
end
|
34
29
|
end
|
30
|
+
def ==(other)
|
31
|
+
T.instance_of?(Nilable, other) and self.type == other.type
|
32
|
+
end
|
33
|
+
end
|
35
34
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
49
|
-
end
|
50
|
-
def ==(other)
|
51
|
-
T.instance_of?(AnyType, other) and (self.types - other.types).empty?
|
35
|
+
class AnyType
|
36
|
+
attr_reader :types
|
37
|
+
def initialize(*types)
|
38
|
+
@types = types
|
39
|
+
end
|
40
|
+
def to_s
|
41
|
+
"Any[#{types.map { |t| t.to_s}.join(', ')}]"
|
42
|
+
end
|
43
|
+
def check(value)
|
44
|
+
type = types.find {|t| T.instance_of?(t, value) }
|
45
|
+
if type == nil
|
46
|
+
raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - any of #{@types.map { |t| t.to_s}.join(', ')} required")
|
52
47
|
end
|
53
48
|
end
|
49
|
+
def ==(other)
|
50
|
+
T.instance_of?(AnyType, other) and (self.types - other.types).empty?
|
51
|
+
end
|
52
|
+
end
|
54
53
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
61
|
-
def to_s
|
62
|
-
"Union[#{cases.map { |k, t| "#{k}: #{t}"}.join(', ')}]"
|
63
|
-
end
|
54
|
+
class UnionType < AnyType
|
55
|
+
attr_reader :cases
|
56
|
+
def initialize(cases)
|
57
|
+
@cases = cases
|
58
|
+
super(*cases.values)
|
64
59
|
end
|
60
|
+
def to_s
|
61
|
+
"Union[#{cases.map { |k, t| "#{k}: #{t}"}.join(', ')}]"
|
62
|
+
end
|
63
|
+
end
|
65
64
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
value.each { |item_value| T.check(item_type, item_value) }
|
80
|
-
end
|
81
|
-
def ==(other)
|
82
|
-
T.instance_of?(ArrayType, other) and self.item_type == other.item_type
|
65
|
+
class ArrayType
|
66
|
+
attr_reader :item_type
|
67
|
+
def initialize(item_type)
|
68
|
+
@item_type = item_type
|
69
|
+
end
|
70
|
+
def to_s
|
71
|
+
"Array[#{item_type.to_s}]"
|
72
|
+
end
|
73
|
+
def check(value)
|
74
|
+
T.check_not_nil(self, value)
|
75
|
+
if !value.is_a? Array
|
76
|
+
raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - Array is required")
|
83
77
|
end
|
78
|
+
value.each { |item_value| T.check(item_type, item_value) }
|
84
79
|
end
|
80
|
+
def ==(other)
|
81
|
+
T.instance_of?(ArrayType, other) and self.item_type == other.item_type
|
82
|
+
end
|
83
|
+
end
|
85
84
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
end
|
101
|
-
value.each do |item_key, item_value|
|
102
|
-
T.check(@key_type, item_key)
|
103
|
-
T.check(@value_type, item_value)
|
104
|
-
end
|
85
|
+
class HashType
|
86
|
+
attr_reader :key_type
|
87
|
+
attr_reader :value_type
|
88
|
+
def initialize(key_type, value_type)
|
89
|
+
@key_type = key_type
|
90
|
+
@value_type = value_type
|
91
|
+
end
|
92
|
+
def to_s
|
93
|
+
"Hash[#{@key_type.to_s}, #{@value_type.to_s}]"
|
94
|
+
end
|
95
|
+
def check(value)
|
96
|
+
T.check_not_nil(self, value)
|
97
|
+
if !value.is_a? Hash
|
98
|
+
raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - Hash is required")
|
105
99
|
end
|
106
|
-
|
107
|
-
T.
|
100
|
+
value.each do |item_key, item_value|
|
101
|
+
T.check(@key_type, item_key)
|
102
|
+
T.check(@value_type, item_value)
|
108
103
|
end
|
109
104
|
end
|
105
|
+
def ==(other)
|
106
|
+
T.instance_of?(HashType, other) and self.key_type == other.key_type and self.value_type == other.value_type
|
107
|
+
end
|
108
|
+
end
|
110
109
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
110
|
+
class StringFormatted
|
111
|
+
attr_reader :regex
|
112
|
+
def initialize(regex)
|
113
|
+
@regex = regex
|
114
|
+
end
|
115
|
+
def to_s
|
116
|
+
"String<#@regex>"
|
117
|
+
end
|
118
|
+
def check(value)
|
119
|
+
T.check_not_nil(self, value)
|
120
|
+
if !value.is_a? String
|
121
|
+
raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - String is required for StringFormatted")
|
118
122
|
end
|
119
|
-
|
120
|
-
|
121
|
-
if !value.is_a? String
|
122
|
-
raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - String is required for StringFormatted")
|
123
|
-
end
|
124
|
-
if !@regex.match?(value)
|
125
|
-
raise TypeError.new("Value '#{value.inspect.to_s}' is not in required format '#{@regex}'")
|
126
|
-
end
|
123
|
+
if !@regex.match?(value)
|
124
|
+
raise TypeError.new("Value '#{value.inspect.to_s}' is not in required format '#{@regex}'")
|
127
125
|
end
|
128
126
|
end
|
127
|
+
end
|
129
128
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
end
|
137
|
-
if !value.is_a? type
|
138
|
-
raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - #{type} is required")
|
139
|
-
end
|
129
|
+
def T.check(type, value)
|
130
|
+
if type.methods.include? :check
|
131
|
+
type.check(value)
|
132
|
+
else
|
133
|
+
if type != NilClass
|
134
|
+
T.check_not_nil(type, value)
|
140
135
|
end
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
def T.instance_of?(type, value)
|
145
|
-
begin
|
146
|
-
T.check(type, value)
|
147
|
-
true
|
148
|
-
rescue TypeError
|
149
|
-
false
|
136
|
+
if !value.is_a? type
|
137
|
+
raise TypeError.new("Value '#{value.inspect.to_s}' type is #{value.class} - #{type} is required")
|
150
138
|
end
|
151
139
|
end
|
140
|
+
return value
|
141
|
+
end
|
152
142
|
|
153
|
-
|
154
|
-
|
143
|
+
def T.instance_of?(type, value)
|
144
|
+
begin
|
145
|
+
T.check(type, value)
|
146
|
+
true
|
147
|
+
rescue TypeError
|
148
|
+
false
|
155
149
|
end
|
150
|
+
end
|
156
151
|
|
157
|
-
|
158
|
-
|
159
|
-
|
152
|
+
def T.nilable(value_type)
|
153
|
+
Nilable.new(value_type)
|
154
|
+
end
|
160
155
|
|
161
|
-
|
162
|
-
|
163
|
-
|
156
|
+
def T.array(item_type)
|
157
|
+
ArrayType.new(item_type)
|
158
|
+
end
|
164
159
|
|
165
|
-
|
166
|
-
|
167
|
-
|
160
|
+
def T.hash(key_type, value_type)
|
161
|
+
HashType.new(key_type, value_type)
|
162
|
+
end
|
168
163
|
|
169
|
-
|
170
|
-
|
171
|
-
|
164
|
+
def T.any(*typdefs)
|
165
|
+
AnyType.new(*typdefs)
|
166
|
+
end
|
172
167
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
168
|
+
def T.union(*cases)
|
169
|
+
UnionType.new(*cases)
|
170
|
+
end
|
171
|
+
|
172
|
+
def T.check_var(var_name, type, value)
|
173
|
+
begin
|
174
|
+
check(type, value)
|
175
|
+
return value
|
176
|
+
rescue TypeError => e
|
177
|
+
raise TypeError.new("Variable #{var_name} type check failed, expected type: #{type.to_s}, value: #{value}")
|
180
178
|
end
|
181
179
|
end
|
180
|
+
end
|
182
181
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
end
|
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}$/)
|
data/lib/emery.rb
CHANGED
data/test/dataclass_test.rb
CHANGED
@@ -1,115 +1,112 @@
|
|
1
1
|
require "test/unit/runner/junitxml"
|
2
2
|
|
3
|
-
require 'emery
|
4
|
-
require 'emery/jsoner'
|
3
|
+
require 'emery'
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
5
|
+
class DataClassTypeEquality < Test::Unit::TestCase
|
6
|
+
def test_equals
|
7
|
+
assert_true TheClass == TheClass
|
8
|
+
end
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
end
|
10
|
+
def test_not_equals
|
11
|
+
assert_false TheClass == TheClassWithNested
|
15
12
|
end
|
13
|
+
end
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
class DataClassFields < Test::Unit::TestCase
|
16
|
+
def test_fields_meta
|
17
|
+
assert_equal ({:string => String, :int => Integer}), TheClass.json_attributes, "Attributes with types should be available on data class"
|
18
|
+
end
|
21
19
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
20
|
+
def test_read
|
21
|
+
obj = TheClass.new(string: "the string", int: 123)
|
22
|
+
assert_equal "the string", obj.string, "Immutable field should be readable"
|
23
|
+
assert_equal 123, obj.int, "Mutable field should be readable"
|
24
|
+
end
|
27
25
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
26
|
+
def test_write_mutable
|
27
|
+
obj = TheClass.new(string: "the string", int: 123)
|
28
|
+
obj.int = 124
|
29
|
+
assert_equal 124, obj.int, "Mutable field should be writable"
|
30
|
+
end
|
33
31
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
32
|
+
def test_write_immutable
|
33
|
+
obj = TheClass.new(string: "the string", int: 123)
|
34
|
+
assert_raise NoMethodError do
|
35
|
+
obj.string = "the other string"
|
39
36
|
end
|
40
37
|
end
|
38
|
+
end
|
41
39
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
class DataClassEquality < Test::Unit::TestCase
|
41
|
+
def test_nil
|
42
|
+
assert_not_equal nil, TheClass.new(string: "the string", int: 123), "Object should not be equal to nil"
|
43
|
+
end
|
46
44
|
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
def test_same_fields_values
|
46
|
+
assert_equal TheClass.new(string: "the string", int: 123), TheClass.new(string: "the string", int: 123), "Objects with same fields should be equal"
|
47
|
+
end
|
50
48
|
|
51
|
-
|
52
|
-
|
53
|
-
end
|
49
|
+
def test_different_fields_values
|
50
|
+
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"
|
54
51
|
end
|
52
|
+
end
|
55
53
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
62
60
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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"
|
65
|
+
end
|
68
66
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
67
|
+
def test_deserialize_object_fail
|
68
|
+
assert_raise JsonerError do
|
69
|
+
Jsoner.from_json(TheClass, '"string"')
|
73
70
|
end
|
74
|
-
|
75
71
|
end
|
76
72
|
|
77
|
-
|
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
|
73
|
+
end
|
81
74
|
|
82
|
-
|
83
|
-
|
84
|
-
|
75
|
+
class DataClassSerialization < Test::Unit::TestCase
|
76
|
+
def test_serialize_object
|
77
|
+
assert_equal '{"string":"the string","int":123}', Jsoner.to_json(TheClass, TheClass.new(string: "the string", int: 123)), "nil should be serializable to JSON"
|
85
78
|
end
|
86
79
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
80
|
+
def test_serialize_array_of_objects
|
81
|
+
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
|
+
end
|
83
|
+
end
|
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
|
91
|
+
end
|
94
92
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
end
|
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")
|
100
97
|
end
|
101
98
|
end
|
99
|
+
end
|
102
100
|
|
103
|
-
|
104
|
-
|
101
|
+
class TheClass
|
102
|
+
include DataClass
|
105
103
|
|
106
|
-
|
107
|
-
|
108
|
-
|
104
|
+
val :string, String
|
105
|
+
var :int, Integer
|
106
|
+
end
|
109
107
|
|
110
|
-
|
111
|
-
|
108
|
+
class TheClassWithNested
|
109
|
+
include DataClass
|
112
110
|
|
113
|
-
|
114
|
-
end
|
111
|
+
val :nested, TheClass
|
115
112
|
end
|
data/test/enum_test.rb
CHANGED
@@ -1,47 +1,43 @@
|
|
1
1
|
require "test/unit/runner/junitxml"
|
2
2
|
|
3
|
-
require 'emery
|
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
|
3
|
+
require 'emery'
|
12
4
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
17
|
-
end
|
5
|
+
class TypeCheckEnum < Test::Unit::TestCase
|
6
|
+
def test_success
|
7
|
+
assert_equal SomeEnum::one, T.check(SomeEnum, SomeEnum::one), "Plain enum type should pass type check"
|
18
8
|
end
|
19
9
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
T.check(SomeEnum, data)
|
24
|
-
assert_equal SomeEnum::two, data, "Enum should be parsable from JSON"
|
10
|
+
def test_fail
|
11
|
+
assert_raise TypeError do
|
12
|
+
T.check(SomeEnum, "non existing")
|
25
13
|
end
|
14
|
+
end
|
15
|
+
end
|
26
16
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
17
|
+
class EnumDeserialization < Test::Unit::TestCase
|
18
|
+
def test_deserialize_enum
|
19
|
+
data = Jsoner.from_json(SomeEnum, '"two"')
|
20
|
+
T.check(SomeEnum, data)
|
21
|
+
assert_equal SomeEnum::two, data, "Enum should be parsable from JSON"
|
32
22
|
end
|
33
23
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
assert_equal '"two"', json_str, "Enum should be serializable to JSON"
|
24
|
+
def test_deserialize_enum_non_existing_item
|
25
|
+
assert_raise JsonerError do
|
26
|
+
Jsoner.from_json(SomeEnum, '"non_existing"')
|
38
27
|
end
|
39
28
|
end
|
29
|
+
end
|
40
30
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
define :two, 'two'
|
31
|
+
class EnumSerialization < Test::Unit::TestCase
|
32
|
+
def test_serialize_enum
|
33
|
+
json_str = Jsoner.to_json(SomeEnum, SomeEnum::two)
|
34
|
+
assert_equal '"two"', json_str, "Enum should be serializable to JSON"
|
46
35
|
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SomeEnum
|
39
|
+
include Enum
|
40
|
+
|
41
|
+
define :one, 'one'
|
42
|
+
define :two, 'two'
|
47
43
|
end
|