emery 0.0.2 → 0.0.3
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 +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
|