avro 1.8.2 → 1.9.0
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/Manifest +6 -0
- data/avro.gemspec +21 -21
- data/lib/avro.rb +11 -0
- data/lib/avro/io.rb +44 -64
- data/lib/avro/ipc.rb +8 -8
- data/lib/avro/logical_types.rb +90 -0
- data/lib/avro/protocol.rb +11 -7
- data/lib/avro/schema.rb +89 -63
- data/lib/avro/schema_compatibility.rb +170 -0
- data/lib/avro/schema_validator.rb +242 -0
- data/test/random_data.rb +21 -2
- data/test/test_datafile.rb +3 -3
- data/test/test_io.rb +73 -6
- data/test/test_logical_types.rb +128 -0
- data/test/test_protocol.rb +36 -3
- data/test/test_schema.rb +323 -27
- data/test/test_schema_compatibility.rb +475 -0
- data/test/test_schema_validator.rb +554 -0
- data/test/tool.rb +0 -1
- metadata +20 -8
data/test/test_protocol.rb
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
# to you under the Apache License, Version 2.0 (the
|
|
6
6
|
# "License"); you may not use this file except in compliance
|
|
7
7
|
# with the License. You may obtain a copy of the License at
|
|
8
|
-
#
|
|
8
|
+
#
|
|
9
9
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
#
|
|
10
|
+
#
|
|
11
11
|
# Unless required by applicable law or agreed to in writing, software
|
|
12
12
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
13
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
@@ -139,7 +139,7 @@ EOS
|
|
|
139
139
|
|
|
140
140
|
}
|
|
141
141
|
EOS
|
|
142
|
-
ExampleProtocol.new(<<-EOS, true)
|
|
142
|
+
ExampleProtocol.new(<<-EOS, true),
|
|
143
143
|
{"namespace": "org.apache.avro.test",
|
|
144
144
|
"protocol": "BulkData",
|
|
145
145
|
|
|
@@ -160,6 +160,29 @@ EOS
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
}
|
|
163
|
+
EOS
|
|
164
|
+
ExampleProtocol.new(<<-EOS, true),
|
|
165
|
+
{
|
|
166
|
+
"namespace": "com.acme",
|
|
167
|
+
"protocol": "HelloWorld",
|
|
168
|
+
"doc": "protocol_documentation",
|
|
169
|
+
|
|
170
|
+
"types": [
|
|
171
|
+
{"name": "Greeting", "type": "record", "fields": [
|
|
172
|
+
{"name": "message", "type": "string"}]},
|
|
173
|
+
{"name": "Curse", "type": "error", "fields": [
|
|
174
|
+
{"name": "message", "type": "string"}]}
|
|
175
|
+
],
|
|
176
|
+
|
|
177
|
+
"messages": {
|
|
178
|
+
"hello": {
|
|
179
|
+
"doc": "message_documentation",
|
|
180
|
+
"request": [{"name": "greeting", "type": "Greeting" }],
|
|
181
|
+
"response": "Greeting",
|
|
182
|
+
"errors": ["Curse"]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
163
186
|
EOS
|
|
164
187
|
]
|
|
165
188
|
|
|
@@ -196,4 +219,14 @@ EOS
|
|
|
196
219
|
assert_equal type.namespace, 'com.acme'
|
|
197
220
|
end
|
|
198
221
|
end
|
|
222
|
+
|
|
223
|
+
def test_protocol_doc_attribute
|
|
224
|
+
original = Protocol.parse(EXAMPLES.last.protocol_string)
|
|
225
|
+
assert_equal 'protocol_documentation', original.doc
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_protocol_message_doc_attribute
|
|
229
|
+
original = Protocol.parse(EXAMPLES.last.protocol_string)
|
|
230
|
+
assert_equal 'message_documentation', original.messages['hello'].doc
|
|
231
|
+
end
|
|
199
232
|
end
|
data/test/test_schema.rb
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
require 'test_help'
|
|
18
18
|
|
|
19
19
|
class TestSchema < Test::Unit::TestCase
|
|
20
|
+
def hash_to_schema(hash)
|
|
21
|
+
Avro::Schema.parse(hash.to_json)
|
|
22
|
+
end
|
|
23
|
+
|
|
20
24
|
def test_default_namespace
|
|
21
25
|
schema = Avro::Schema.parse <<-SCHEMA
|
|
22
26
|
{"type": "record", "name": "OuterRecord", "fields": [
|
|
@@ -27,13 +31,13 @@ class TestSchema < Test::Unit::TestCase
|
|
|
27
31
|
]}
|
|
28
32
|
SCHEMA
|
|
29
33
|
|
|
30
|
-
assert_equal schema.name
|
|
31
|
-
assert_equal schema.fullname
|
|
34
|
+
assert_equal 'OuterRecord', schema.name
|
|
35
|
+
assert_equal 'OuterRecord', schema.fullname
|
|
32
36
|
assert_nil schema.namespace
|
|
33
37
|
|
|
34
38
|
schema.fields.each do |field|
|
|
35
|
-
assert_equal field.type.name
|
|
36
|
-
assert_equal field.type.fullname
|
|
39
|
+
assert_equal 'InnerRecord', field.type.name
|
|
40
|
+
assert_equal 'InnerRecord', field.type.fullname
|
|
37
41
|
assert_nil field.type.namespace
|
|
38
42
|
end
|
|
39
43
|
end
|
|
@@ -50,13 +54,13 @@ class TestSchema < Test::Unit::TestCase
|
|
|
50
54
|
]}
|
|
51
55
|
SCHEMA
|
|
52
56
|
|
|
53
|
-
assert_equal schema.name
|
|
54
|
-
assert_equal
|
|
55
|
-
assert_equal
|
|
57
|
+
assert_equal 'OuterRecord', schema.name
|
|
58
|
+
assert_equal 'my.name.space.OuterRecord', schema.fullname
|
|
59
|
+
assert_equal 'my.name.space', schema.namespace
|
|
56
60
|
schema.fields.each do |field|
|
|
57
|
-
assert_equal field.type.name
|
|
58
|
-
assert_equal
|
|
59
|
-
assert_equal
|
|
61
|
+
assert_equal 'InnerRecord', field.type.name
|
|
62
|
+
assert_equal 'my.name.space.InnerRecord', field.type.fullname
|
|
63
|
+
assert_equal 'my.name.space', field.type.namespace
|
|
60
64
|
end
|
|
61
65
|
end
|
|
62
66
|
|
|
@@ -71,13 +75,13 @@ class TestSchema < Test::Unit::TestCase
|
|
|
71
75
|
]}
|
|
72
76
|
SCHEMA
|
|
73
77
|
|
|
74
|
-
assert_equal schema.name
|
|
75
|
-
assert_equal
|
|
76
|
-
assert_equal
|
|
78
|
+
assert_equal 'OuterRecord', schema.name
|
|
79
|
+
assert_equal 'my.name.space.OuterRecord', schema.fullname
|
|
80
|
+
assert_equal 'my.name.space', schema.namespace
|
|
77
81
|
schema.fields.each do |field|
|
|
78
|
-
assert_equal field.type.name
|
|
79
|
-
assert_equal
|
|
80
|
-
assert_equal
|
|
82
|
+
assert_equal 'InnerEnum', field.type.name
|
|
83
|
+
assert_equal 'my.name.space.InnerEnum', field.type.fullname
|
|
84
|
+
assert_equal 'my.name.space', field.type.namespace
|
|
81
85
|
end
|
|
82
86
|
end
|
|
83
87
|
|
|
@@ -96,18 +100,18 @@ class TestSchema < Test::Unit::TestCase
|
|
|
96
100
|
]}
|
|
97
101
|
SCHEMA
|
|
98
102
|
|
|
99
|
-
assert_equal schema.name
|
|
100
|
-
assert_equal
|
|
101
|
-
assert_equal schema.namespace
|
|
103
|
+
assert_equal 'OuterRecord', schema.name
|
|
104
|
+
assert_equal 'outer.OuterRecord', schema.fullname
|
|
105
|
+
assert_equal 'outer', schema.namespace
|
|
102
106
|
middle = schema.fields.first.type
|
|
103
|
-
assert_equal middle.name
|
|
104
|
-
assert_equal middle.
|
|
105
|
-
assert_equal middle
|
|
107
|
+
assert_equal 'MiddleRecord', middle.name
|
|
108
|
+
assert_equal 'middle.MiddleRecord', middle.fullname
|
|
109
|
+
assert_equal 'middle', middle.namespace
|
|
106
110
|
inner = middle.fields.first.type
|
|
107
|
-
assert_equal inner.name
|
|
108
|
-
assert_equal
|
|
109
|
-
assert_equal inner.namespace
|
|
110
|
-
assert_equal inner.fields.first.type
|
|
111
|
+
assert_equal 'InnerRecord', inner.name
|
|
112
|
+
assert_equal 'middle.InnerRecord', inner.fullname
|
|
113
|
+
assert_equal 'middle', inner.namespace
|
|
114
|
+
assert_equal middle, inner.fields.first.type
|
|
111
115
|
end
|
|
112
116
|
|
|
113
117
|
def test_to_avro_includes_namespaces
|
|
@@ -120,7 +124,7 @@ class TestSchema < Test::Unit::TestCase
|
|
|
120
124
|
]}
|
|
121
125
|
SCHEMA
|
|
122
126
|
|
|
123
|
-
assert_equal
|
|
127
|
+
assert_equal({
|
|
124
128
|
'type' => 'record', 'name' => 'OuterRecord', 'namespace' => 'my.name.space',
|
|
125
129
|
'fields' => [
|
|
126
130
|
{'name' => 'definition', 'type' => {
|
|
@@ -129,6 +133,21 @@ class TestSchema < Test::Unit::TestCase
|
|
|
129
133
|
}},
|
|
130
134
|
{'name' => 'reference', 'type' => 'my.name.space.InnerFixed'}
|
|
131
135
|
]
|
|
136
|
+
}, schema.to_avro)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def test_to_avro_includes_logical_type
|
|
140
|
+
schema = Avro::Schema.parse <<-SCHEMA
|
|
141
|
+
{"type": "record", "name": "has_logical", "fields": [
|
|
142
|
+
{"name": "dt", "type": {"type": "int", "logicalType": "date"}}]
|
|
143
|
+
}
|
|
144
|
+
SCHEMA
|
|
145
|
+
|
|
146
|
+
assert_equal schema.to_avro, {
|
|
147
|
+
'type' => 'record', 'name' => 'has_logical',
|
|
148
|
+
'fields' => [
|
|
149
|
+
{'name' => 'dt', 'type' => {'type' => 'int', 'logicalType' => 'date'}}
|
|
150
|
+
]
|
|
132
151
|
}
|
|
133
152
|
end
|
|
134
153
|
|
|
@@ -160,4 +179,281 @@ class TestSchema < Test::Unit::TestCase
|
|
|
160
179
|
]
|
|
161
180
|
}
|
|
162
181
|
end
|
|
182
|
+
|
|
183
|
+
def test_record_field_doc_attribute
|
|
184
|
+
field_schema_json = Avro::Schema.parse <<-SCHEMA
|
|
185
|
+
{
|
|
186
|
+
"type": "record",
|
|
187
|
+
"name": "Record",
|
|
188
|
+
"namespace": "my.name.space",
|
|
189
|
+
"fields": [
|
|
190
|
+
{
|
|
191
|
+
"name": "name",
|
|
192
|
+
"type": "boolean",
|
|
193
|
+
"doc": "documentation"
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
SCHEMA
|
|
198
|
+
|
|
199
|
+
field_schema_hash =
|
|
200
|
+
{
|
|
201
|
+
'type' => 'record',
|
|
202
|
+
'name' => 'Record',
|
|
203
|
+
'namespace' => 'my.name.space',
|
|
204
|
+
'fields' => [
|
|
205
|
+
{
|
|
206
|
+
'name' => 'name',
|
|
207
|
+
'type' => 'boolean',
|
|
208
|
+
'doc' => 'documentation'
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
assert_equal field_schema_hash, field_schema_json.to_avro
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def test_record_doc_attribute
|
|
217
|
+
record_schema_json = Avro::Schema.parse <<-SCHEMA
|
|
218
|
+
{
|
|
219
|
+
"type": "record",
|
|
220
|
+
"name": "Record",
|
|
221
|
+
"namespace": "my.name.space",
|
|
222
|
+
"doc": "documentation",
|
|
223
|
+
"fields": [
|
|
224
|
+
{
|
|
225
|
+
"name": "name",
|
|
226
|
+
"type": "boolean"
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
SCHEMA
|
|
231
|
+
|
|
232
|
+
record_schema_hash =
|
|
233
|
+
{
|
|
234
|
+
'type' => 'record',
|
|
235
|
+
'name' => 'Record',
|
|
236
|
+
'namespace' => 'my.name.space',
|
|
237
|
+
'doc' => 'documentation',
|
|
238
|
+
'fields' => [
|
|
239
|
+
{
|
|
240
|
+
'name' => 'name',
|
|
241
|
+
'type' => 'boolean'
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
assert_equal record_schema_hash, record_schema_json.to_avro
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def test_enum_doc_attribute
|
|
250
|
+
enum_schema_json = Avro::Schema.parse <<-SCHEMA
|
|
251
|
+
{
|
|
252
|
+
"type": "enum",
|
|
253
|
+
"name": "Enum",
|
|
254
|
+
"namespace": "my.name.space",
|
|
255
|
+
"doc": "documentation",
|
|
256
|
+
"symbols" : [
|
|
257
|
+
"SPADES",
|
|
258
|
+
"HEARTS",
|
|
259
|
+
"DIAMONDS",
|
|
260
|
+
"CLUBS"
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
SCHEMA
|
|
264
|
+
|
|
265
|
+
enum_schema_hash =
|
|
266
|
+
{
|
|
267
|
+
'type' => 'enum',
|
|
268
|
+
'name' => 'Enum',
|
|
269
|
+
'namespace' => 'my.name.space',
|
|
270
|
+
'doc' => 'documentation',
|
|
271
|
+
'symbols' => [
|
|
272
|
+
'SPADES',
|
|
273
|
+
'HEARTS',
|
|
274
|
+
'DIAMONDS',
|
|
275
|
+
'CLUBS'
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
assert_equal enum_schema_hash, enum_schema_json.to_avro
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_empty_record
|
|
282
|
+
schema = Avro::Schema.parse('{"type":"record", "name":"Empty"}')
|
|
283
|
+
assert_empty(schema.fields)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def test_empty_union
|
|
287
|
+
schema = Avro::Schema.parse('[]')
|
|
288
|
+
assert_equal(schema.to_s, '[]')
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def test_read
|
|
292
|
+
schema = Avro::Schema.parse('"string"')
|
|
293
|
+
writer_schema = Avro::Schema.parse('"int"')
|
|
294
|
+
assert_false(schema.read?(writer_schema))
|
|
295
|
+
assert_true(schema.read?(schema))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def test_be_read
|
|
299
|
+
schema = Avro::Schema.parse('"string"')
|
|
300
|
+
writer_schema = Avro::Schema.parse('"int"')
|
|
301
|
+
assert_false(schema.be_read?(writer_schema))
|
|
302
|
+
assert_true(schema.be_read?(schema))
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def test_mutual_read
|
|
306
|
+
schema = Avro::Schema.parse('"string"')
|
|
307
|
+
writer_schema = Avro::Schema.parse('"int"')
|
|
308
|
+
default1 = Avro::Schema.parse('{"type":"record", "name":"Default", "fields":[{"name":"i", "type":"int", "default": 1}]}')
|
|
309
|
+
default2 = Avro::Schema.parse('{"type":"record", "name":"Default", "fields":[{"name:":"s", "type":"string", "default": ""}]}')
|
|
310
|
+
assert_false(schema.mutual_read?(writer_schema))
|
|
311
|
+
assert_true(schema.mutual_read?(schema))
|
|
312
|
+
assert_true(default1.mutual_read?(default2))
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def test_validate_defaults
|
|
316
|
+
exception = assert_raise(Avro::SchemaParseError) do
|
|
317
|
+
hash_to_schema(
|
|
318
|
+
type: 'record',
|
|
319
|
+
name: 'fruits',
|
|
320
|
+
fields: [
|
|
321
|
+
{
|
|
322
|
+
name: 'veggies',
|
|
323
|
+
type: 'string',
|
|
324
|
+
default: nil
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
assert_equal('Error validating default for veggies: at . expected type string, got null',
|
|
330
|
+
exception.to_s)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def test_field_default_validation_disabled
|
|
334
|
+
Avro.disable_field_default_validation = true
|
|
335
|
+
assert_nothing_raised do
|
|
336
|
+
hash_to_schema(
|
|
337
|
+
type: 'record',
|
|
338
|
+
name: 'fruits',
|
|
339
|
+
fields: [
|
|
340
|
+
{
|
|
341
|
+
name: 'veggies',
|
|
342
|
+
type: 'string',
|
|
343
|
+
default: nil
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
ensure
|
|
349
|
+
Avro.disable_field_default_validation = false
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def test_field_default_validation_disabled_via_env
|
|
353
|
+
Avro.disable_field_default_validation = false
|
|
354
|
+
ENV['AVRO_DISABLE_FIELD_DEFAULT_VALIDATION'] = "1"
|
|
355
|
+
|
|
356
|
+
assert_nothing_raised do
|
|
357
|
+
hash_to_schema(
|
|
358
|
+
type: 'record',
|
|
359
|
+
name: 'fruits',
|
|
360
|
+
fields: [
|
|
361
|
+
{
|
|
362
|
+
name: 'veggies',
|
|
363
|
+
type: 'string',
|
|
364
|
+
default: nil
|
|
365
|
+
}
|
|
366
|
+
]
|
|
367
|
+
)
|
|
368
|
+
end
|
|
369
|
+
ensure
|
|
370
|
+
ENV.delete('AVRO_DISABLE_FIELD_DEFAULT_VALIDATION')
|
|
371
|
+
Avro.disable_field_default_validation = false
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def test_validate_record_valid_default
|
|
375
|
+
assert_nothing_raised(Avro::SchemaParseError) do
|
|
376
|
+
hash_to_schema(
|
|
377
|
+
type: 'record',
|
|
378
|
+
name: 'with_subrecord',
|
|
379
|
+
fields: [
|
|
380
|
+
{
|
|
381
|
+
name: 'sub',
|
|
382
|
+
type: {
|
|
383
|
+
name: 'subrecord',
|
|
384
|
+
type: 'record',
|
|
385
|
+
fields: [
|
|
386
|
+
{ type: 'string', name: 'x' }
|
|
387
|
+
]
|
|
388
|
+
},
|
|
389
|
+
default: {
|
|
390
|
+
x: "y"
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
]
|
|
394
|
+
)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def test_validate_record_invalid_default
|
|
399
|
+
exception = assert_raise(Avro::SchemaParseError) do
|
|
400
|
+
hash_to_schema(
|
|
401
|
+
type: 'record',
|
|
402
|
+
name: 'with_subrecord',
|
|
403
|
+
fields: [
|
|
404
|
+
{
|
|
405
|
+
name: 'sub',
|
|
406
|
+
type: {
|
|
407
|
+
name: 'subrecord',
|
|
408
|
+
type: 'record',
|
|
409
|
+
fields: [
|
|
410
|
+
{ type: 'string', name: 'x' }
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
default: {
|
|
414
|
+
a: 1
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
]
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
assert_equal('Error validating default for sub: at .x expected type string, got null',
|
|
421
|
+
exception.to_s)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def test_validate_union_defaults
|
|
425
|
+
exception = assert_raise(Avro::SchemaParseError) do
|
|
426
|
+
hash_to_schema(
|
|
427
|
+
type: 'record',
|
|
428
|
+
name: 'fruits',
|
|
429
|
+
fields: [
|
|
430
|
+
{
|
|
431
|
+
name: 'veggies',
|
|
432
|
+
type: %w(string null),
|
|
433
|
+
default: 5
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
)
|
|
437
|
+
end
|
|
438
|
+
assert_equal('Error validating default for veggies: at . expected type string, got int with value 5',
|
|
439
|
+
exception.to_s)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def test_validate_union_default_first_type
|
|
443
|
+
exception = assert_raise(Avro::SchemaParseError) do
|
|
444
|
+
hash_to_schema(
|
|
445
|
+
type: 'record',
|
|
446
|
+
name: 'fruits',
|
|
447
|
+
fields: [
|
|
448
|
+
{
|
|
449
|
+
name: 'veggies',
|
|
450
|
+
type: %w(null string),
|
|
451
|
+
default: 'apple'
|
|
452
|
+
}
|
|
453
|
+
]
|
|
454
|
+
)
|
|
455
|
+
end
|
|
456
|
+
assert_equal('Error validating default for veggies: at . expected type null, got string with value "apple"',
|
|
457
|
+
exception.to_s)
|
|
458
|
+
end
|
|
163
459
|
end
|