avro 1.8.1 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,128 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Licensed to the Apache Software Foundation (ASF) under one
3
+ # or more contributor license agreements. See the NOTICE file
4
+ # distributed with this work for additional information
5
+ # regarding copyright ownership. The ASF licenses this file
6
+ # to you under the Apache License, Version 2.0 (the
7
+ # "License"); you may not use this file except in compliance
8
+ # with the License. You may obtain a copy of the License at
9
+ #
10
+ # https://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'test_help'
19
+
20
+ class TestLogicalTypes < Test::Unit::TestCase
21
+ def test_int_date
22
+ schema = Avro::Schema.parse <<-SCHEMA
23
+ { "type": "int", "logicalType": "date" }
24
+ SCHEMA
25
+
26
+ assert_equal 'date', schema.logical_type
27
+ today = Date.today
28
+ assert_encode_and_decode today, schema
29
+ assert_preencoded Avro::LogicalTypes::IntDate.encode(today), schema, today
30
+ end
31
+
32
+ def test_int_date_conversion
33
+ type = Avro::LogicalTypes::IntDate
34
+
35
+ assert_equal 5, type.encode(Date.new(1970, 1, 6))
36
+ assert_equal 0, type.encode(Date.new(1970, 1, 1))
37
+ assert_equal(-5, type.encode(Date.new(1969, 12, 27)))
38
+
39
+ assert_equal Date.new(1970, 1, 6), type.decode(5)
40
+ assert_equal Date.new(1970, 1, 1), type.decode(0)
41
+ assert_equal Date.new(1969, 12, 27), type.decode(-5)
42
+ end
43
+
44
+ def test_timestamp_millis_long
45
+ schema = Avro::Schema.parse <<-SCHEMA
46
+ { "type": "long", "logicalType": "timestamp-millis" }
47
+ SCHEMA
48
+
49
+ # The Time.at format is (seconds, microseconds) since Epoch.
50
+ time = Time.at(628232400, 12000)
51
+
52
+ assert_equal 'timestamp-millis', schema.logical_type
53
+ assert_encode_and_decode time, schema
54
+ assert_preencoded Avro::LogicalTypes::TimestampMillis.encode(time), schema, time.utc
55
+ end
56
+
57
+ def test_timestamp_millis_long_conversion
58
+ type = Avro::LogicalTypes::TimestampMillis
59
+
60
+ now = Time.now.utc
61
+ now_millis = Time.utc(now.year, now.month, now.day, now.hour, now.min, now.sec, now.usec / 1000 * 1000)
62
+
63
+ assert_equal now_millis, type.decode(type.encode(now_millis))
64
+ assert_equal 1432849613221, type.encode(Time.utc(2015, 5, 28, 21, 46, 53, 221000))
65
+ assert_equal 1432849613221, type.encode(DateTime.new(2015, 5, 28, 21, 46, 53.221))
66
+ assert_equal Time.utc(2015, 5, 28, 21, 46, 53, 221000), type.decode(1432849613221)
67
+ end
68
+
69
+ def test_timestamp_micros_long
70
+ schema = Avro::Schema.parse <<-SCHEMA
71
+ { "type": "long", "logicalType": "timestamp-micros" }
72
+ SCHEMA
73
+
74
+ # The Time.at format is (seconds, microseconds) since Epoch.
75
+ time = Time.at(628232400, 12345)
76
+
77
+ assert_equal 'timestamp-micros', schema.logical_type
78
+ assert_encode_and_decode time, schema
79
+ assert_preencoded Avro::LogicalTypes::TimestampMicros.encode(time), schema, time.utc
80
+ end
81
+
82
+ def test_timestamp_micros_long_conversion
83
+ type = Avro::LogicalTypes::TimestampMicros
84
+
85
+ now = Time.now.utc
86
+
87
+ assert_equal Time.utc(now.year, now.month, now.day, now.hour, now.min, now.sec, now.usec), type.decode(type.encode(now))
88
+ assert_equal 1432849613221843, type.encode(Time.utc(2015, 5, 28, 21, 46, 53, 221843))
89
+ assert_equal 1432849613221843, type.encode(DateTime.new(2015, 5, 28, 21, 46, 53.221843))
90
+ assert_equal Time.utc(2015, 5, 28, 21, 46, 53, 221843), type.decode(1432849613221843)
91
+ end
92
+
93
+ def test_parse_fixed_duration
94
+ schema = Avro::Schema.parse <<-SCHEMA
95
+ { "type": "fixed", "size": 12, "name": "fixed_dur", "logicalType": "duration" }
96
+ SCHEMA
97
+
98
+ assert_equal 'duration', schema.logical_type
99
+ end
100
+
101
+ def encode(datum, schema)
102
+ buffer = StringIO.new("")
103
+ encoder = Avro::IO::BinaryEncoder.new(buffer)
104
+
105
+ datum_writer = Avro::IO::DatumWriter.new(schema)
106
+ datum_writer.write(datum, encoder)
107
+
108
+ buffer.string
109
+ end
110
+
111
+ def decode(encoded, schema)
112
+ buffer = StringIO.new(encoded)
113
+ decoder = Avro::IO::BinaryDecoder.new(buffer)
114
+
115
+ datum_reader = Avro::IO::DatumReader.new(schema, schema)
116
+ datum_reader.read(decoder)
117
+ end
118
+
119
+ def assert_encode_and_decode(datum, schema)
120
+ encoded = encode(datum, schema)
121
+ assert_equal datum, decode(encoded, schema)
122
+ end
123
+
124
+ def assert_preencoded(datum, schema, decoded)
125
+ encoded = encode(datum, schema)
126
+ assert_equal decoded, decode(encoded, schema)
127
+ end
128
+ end
@@ -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
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
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
@@ -6,7 +6,7 @@
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
- # http://www.apache.org/licenses/LICENSE-2.0
9
+ # https://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,
@@ -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, 'OuterRecord'
31
- assert_equal schema.fullname, 'OuterRecord'
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, 'InnerRecord'
36
- assert_equal field.type.fullname, 'InnerRecord'
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, 'OuterRecord'
54
- assert_equal schema.fullname, 'my.name.space.OuterRecord'
55
- assert_equal schema.namespace, 'my.name.space'
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, 'InnerRecord'
58
- assert_equal field.type.fullname, 'my.name.space.InnerRecord'
59
- assert_equal field.type.namespace, 'my.name.space'
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, 'OuterRecord'
75
- assert_equal schema.fullname, 'my.name.space.OuterRecord'
76
- assert_equal schema.namespace, 'my.name.space'
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, 'InnerEnum'
79
- assert_equal field.type.fullname, 'my.name.space.InnerEnum'
80
- assert_equal field.type.namespace, 'my.name.space'
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, 'OuterRecord'
100
- assert_equal schema.fullname, 'outer.OuterRecord'
101
- assert_equal schema.namespace, 'outer'
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, 'MiddleRecord'
104
- assert_equal middle.fullname, 'middle.MiddleRecord'
105
- assert_equal middle.namespace, '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, 'InnerRecord'
108
- assert_equal inner.fullname, 'middle.InnerRecord'
109
- assert_equal inner.namespace, 'middle'
110
- assert_equal inner.fields.first.type, middle
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 schema.to_avro, {
127
+ assert_equal({
124
128
  'type' => 'record', 'name' => 'OuterRecord', 'namespace' => 'my.name.space',
125
129
  'fields' => [
126
130
  {'name' => 'definition', 'type' => {
@@ -129,7 +133,36 @@ 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
+ ]
151
+ }
152
+ end
153
+
154
+ def test_to_avro_includes_aliases
155
+ hash = {
156
+ 'type' => 'record',
157
+ 'name' => 'test_record',
158
+ 'aliases' => %w(alt_record),
159
+ 'fields' => [
160
+ { 'name' => 'f', 'type' => { 'type' => 'fixed', 'size' => 2, 'name' => 'test_fixed', 'aliases' => %w(alt_fixed) } },
161
+ { 'name' => 'e', 'type' => { 'type' => 'enum', 'symbols' => %w(A B), 'name' => 'test_enum', 'aliases' => %w(alt_enum) } }
162
+ ]
132
163
  }
164
+ schema = hash_to_schema(hash)
165
+ assert_equal(schema.to_avro, hash)
133
166
  end
134
167
 
135
168
  def test_unknown_named_type
@@ -143,4 +176,552 @@ class TestSchema < Test::Unit::TestCase
143
176
 
144
177
  assert_equal '"MissingType" is not a schema we know about.', error.message
145
178
  end
179
+
180
+ def test_invalid_name
181
+ error = assert_raise Avro::SchemaParseError do
182
+ Avro::Schema.parse <<-SCHEMA
183
+ {"type": "record", "name": "my-invalid-name", "fields": [
184
+ {"name": "id", "type": "int"}
185
+ ]}
186
+ SCHEMA
187
+ end
188
+
189
+ assert_equal "Name my-invalid-name is invalid for type record!", error.message
190
+ end
191
+
192
+ def test_invalid_name_with_two_periods
193
+ error = assert_raise Avro::SchemaParseError do
194
+ Avro::Schema.parse <<-SCHEMA
195
+ {"type": "record", "name": "my..invalid.name", "fields": [
196
+ {"name": "id", "type": "int"}
197
+ ]}
198
+ SCHEMA
199
+ end
200
+
201
+ assert_equal "Name my..invalid.name is invalid for type record!", error.message
202
+ end
203
+
204
+ def test_invalid_name_with_validation_disabled
205
+ Avro.disable_schema_name_validation = true
206
+ assert_nothing_raised do
207
+ Avro::Schema.parse <<-SCHEMA
208
+ {"type": "record", "name": "my-invalid-name", "fields": [
209
+ {"name": "id", "type": "int"}
210
+ ]}
211
+ SCHEMA
212
+ end
213
+ Avro.disable_schema_name_validation = false
214
+ end
215
+
216
+ def test_to_avro_handles_falsey_defaults
217
+ schema = Avro::Schema.parse <<-SCHEMA
218
+ {"type": "record", "name": "Record", "namespace": "my.name.space",
219
+ "fields": [
220
+ {"name": "is_usable", "type": "boolean", "default": false}
221
+ ]
222
+ }
223
+ SCHEMA
224
+
225
+ assert_equal schema.to_avro, {
226
+ 'type' => 'record', 'name' => 'Record', 'namespace' => 'my.name.space',
227
+ 'fields' => [
228
+ {'name' => 'is_usable', 'type' => 'boolean', 'default' => false}
229
+ ]
230
+ }
231
+ end
232
+
233
+ def test_record_field_doc_attribute
234
+ field_schema_json = Avro::Schema.parse <<-SCHEMA
235
+ {
236
+ "type": "record",
237
+ "name": "Record",
238
+ "namespace": "my.name.space",
239
+ "fields": [
240
+ {
241
+ "name": "name",
242
+ "type": "boolean",
243
+ "doc": "documentation"
244
+ }
245
+ ]
246
+ }
247
+ SCHEMA
248
+
249
+ field_schema_hash =
250
+ {
251
+ 'type' => 'record',
252
+ 'name' => 'Record',
253
+ 'namespace' => 'my.name.space',
254
+ 'fields' => [
255
+ {
256
+ 'name' => 'name',
257
+ 'type' => 'boolean',
258
+ 'doc' => 'documentation'
259
+ }
260
+ ]
261
+ }
262
+
263
+ assert_equal field_schema_hash, field_schema_json.to_avro
264
+ end
265
+
266
+ def test_record_doc_attribute
267
+ record_schema_json = Avro::Schema.parse <<-SCHEMA
268
+ {
269
+ "type": "record",
270
+ "name": "Record",
271
+ "namespace": "my.name.space",
272
+ "doc": "documentation",
273
+ "fields": [
274
+ {
275
+ "name": "name",
276
+ "type": "boolean"
277
+ }
278
+ ]
279
+ }
280
+ SCHEMA
281
+
282
+ record_schema_hash =
283
+ {
284
+ 'type' => 'record',
285
+ 'name' => 'Record',
286
+ 'namespace' => 'my.name.space',
287
+ 'doc' => 'documentation',
288
+ 'fields' => [
289
+ {
290
+ 'name' => 'name',
291
+ 'type' => 'boolean'
292
+ }
293
+ ]
294
+ }
295
+
296
+ assert_equal record_schema_hash, record_schema_json.to_avro
297
+ end
298
+
299
+ def test_enum_doc_attribute
300
+ enum_schema_json = Avro::Schema.parse <<-SCHEMA
301
+ {
302
+ "type": "enum",
303
+ "name": "Enum",
304
+ "namespace": "my.name.space",
305
+ "doc": "documentation",
306
+ "symbols" : [
307
+ "SPADES",
308
+ "HEARTS",
309
+ "DIAMONDS",
310
+ "CLUBS"
311
+ ]
312
+ }
313
+ SCHEMA
314
+
315
+ enum_schema_hash =
316
+ {
317
+ 'type' => 'enum',
318
+ 'name' => 'Enum',
319
+ 'namespace' => 'my.name.space',
320
+ 'doc' => 'documentation',
321
+ 'symbols' => [
322
+ 'SPADES',
323
+ 'HEARTS',
324
+ 'DIAMONDS',
325
+ 'CLUBS'
326
+ ]
327
+ }
328
+ assert_equal enum_schema_hash, enum_schema_json.to_avro
329
+ end
330
+
331
+ def test_enum_default_attribute
332
+ enum_schema = Avro::Schema.parse <<-SCHEMA
333
+ {
334
+ "type": "enum",
335
+ "name": "fruit",
336
+ "default": "apples",
337
+ "symbols": ["apples", "oranges"]
338
+ }
339
+ SCHEMA
340
+
341
+ enum_schema_hash = {
342
+ 'type' => 'enum',
343
+ 'name' => 'fruit',
344
+ 'default' => 'apples',
345
+ 'symbols' => %w(apples oranges)
346
+ }
347
+
348
+ assert_equal(enum_schema.default, "apples")
349
+ assert_equal(enum_schema_hash, enum_schema.to_avro)
350
+ end
351
+
352
+ def test_validate_enum_default
353
+ exception = assert_raise(Avro::SchemaParseError) do
354
+ hash_to_schema(
355
+ type: 'enum',
356
+ name: 'fruit',
357
+ default: 'bananas',
358
+ symbols: %w(apples oranges)
359
+ )
360
+ end
361
+ assert_equal("Default 'bananas' is not a valid symbol for enum fruit",
362
+ exception.to_s)
363
+ end
364
+
365
+ def test_empty_record
366
+ schema = Avro::Schema.parse('{"type":"record", "name":"Empty"}')
367
+ assert_empty(schema.fields)
368
+ end
369
+
370
+ def test_empty_union
371
+ schema = Avro::Schema.parse('[]')
372
+ assert_equal(schema.to_s, '[]')
373
+ end
374
+
375
+ def test_read
376
+ schema = Avro::Schema.parse('"string"')
377
+ writer_schema = Avro::Schema.parse('"int"')
378
+ assert_false(schema.read?(writer_schema))
379
+ assert_true(schema.read?(schema))
380
+ end
381
+
382
+ def test_be_read
383
+ schema = Avro::Schema.parse('"string"')
384
+ writer_schema = Avro::Schema.parse('"int"')
385
+ assert_false(schema.be_read?(writer_schema))
386
+ assert_true(schema.be_read?(schema))
387
+ end
388
+
389
+ def test_mutual_read
390
+ schema = Avro::Schema.parse('"string"')
391
+ writer_schema = Avro::Schema.parse('"int"')
392
+ default1 = Avro::Schema.parse('{"type":"record", "name":"Default", "fields":[{"name":"i", "type":"int", "default": 1}]}')
393
+ default2 = Avro::Schema.parse('{"type":"record", "name":"Default", "fields":[{"name:":"s", "type":"string", "default": ""}]}')
394
+ assert_false(schema.mutual_read?(writer_schema))
395
+ assert_true(schema.mutual_read?(schema))
396
+ assert_true(default1.mutual_read?(default2))
397
+ end
398
+
399
+ def test_validate_defaults
400
+ exception = assert_raise(Avro::SchemaParseError) do
401
+ hash_to_schema(
402
+ type: 'record',
403
+ name: 'fruits',
404
+ fields: [
405
+ {
406
+ name: 'veggies',
407
+ type: 'string',
408
+ default: nil
409
+ }
410
+ ]
411
+ )
412
+ end
413
+ assert_equal('Error validating default for veggies: at . expected type string, got null',
414
+ exception.to_s)
415
+ end
416
+
417
+ def test_field_default_validation_disabled
418
+ Avro.disable_field_default_validation = true
419
+ assert_nothing_raised do
420
+ hash_to_schema(
421
+ type: 'record',
422
+ name: 'fruits',
423
+ fields: [
424
+ {
425
+ name: 'veggies',
426
+ type: 'string',
427
+ default: nil
428
+ }
429
+ ]
430
+ )
431
+ end
432
+ ensure
433
+ Avro.disable_field_default_validation = false
434
+ end
435
+
436
+ def test_field_default_validation_disabled_via_env
437
+ Avro.disable_field_default_validation = false
438
+ ENV['AVRO_DISABLE_FIELD_DEFAULT_VALIDATION'] = "1"
439
+
440
+ assert_nothing_raised do
441
+ hash_to_schema(
442
+ type: 'record',
443
+ name: 'fruits',
444
+ fields: [
445
+ {
446
+ name: 'veggies',
447
+ type: 'string',
448
+ default: nil
449
+ }
450
+ ]
451
+ )
452
+ end
453
+ ensure
454
+ ENV.delete('AVRO_DISABLE_FIELD_DEFAULT_VALIDATION')
455
+ Avro.disable_field_default_validation = false
456
+ end
457
+
458
+ def test_validate_record_valid_default
459
+ assert_nothing_raised(Avro::SchemaParseError) do
460
+ hash_to_schema(
461
+ type: 'record',
462
+ name: 'with_subrecord',
463
+ fields: [
464
+ {
465
+ name: 'sub',
466
+ type: {
467
+ name: 'subrecord',
468
+ type: 'record',
469
+ fields: [
470
+ { type: 'string', name: 'x' }
471
+ ]
472
+ },
473
+ default: {
474
+ x: "y"
475
+ }
476
+ }
477
+ ]
478
+ )
479
+ end
480
+ end
481
+
482
+ def test_validate_record_invalid_default
483
+ exception = assert_raise(Avro::SchemaParseError) do
484
+ hash_to_schema(
485
+ type: 'record',
486
+ name: 'with_subrecord',
487
+ fields: [
488
+ {
489
+ name: 'sub',
490
+ type: {
491
+ name: 'subrecord',
492
+ type: 'record',
493
+ fields: [
494
+ { type: 'string', name: 'x' }
495
+ ]
496
+ },
497
+ default: {
498
+ a: 1
499
+ }
500
+ }
501
+ ]
502
+ )
503
+ end
504
+ assert_equal('Error validating default for sub: at .x expected type string, got null',
505
+ exception.to_s)
506
+ end
507
+
508
+ def test_validate_union_defaults
509
+ exception = assert_raise(Avro::SchemaParseError) do
510
+ hash_to_schema(
511
+ type: 'record',
512
+ name: 'fruits',
513
+ fields: [
514
+ {
515
+ name: 'veggies',
516
+ type: %w(string null),
517
+ default: 5
518
+ }
519
+ ]
520
+ )
521
+ end
522
+ assert_equal('Error validating default for veggies: at . expected type string, got int with value 5',
523
+ exception.to_s)
524
+ end
525
+
526
+ def test_validate_union_default_first_type
527
+ exception = assert_raise(Avro::SchemaParseError) do
528
+ hash_to_schema(
529
+ type: 'record',
530
+ name: 'fruits',
531
+ fields: [
532
+ {
533
+ name: 'veggies',
534
+ type: %w(null string),
535
+ default: 'apple'
536
+ }
537
+ ]
538
+ )
539
+ end
540
+ assert_equal('Error validating default for veggies: at . expected type null, got string with value "apple"',
541
+ exception.to_s)
542
+ end
543
+
544
+ def test_bytes_decimal_to_include_precision_scale
545
+ schema = Avro::Schema.parse <<-SCHEMA
546
+ {
547
+ "type": "bytes",
548
+ "logicalType": "decimal",
549
+ "precision": 9,
550
+ "scale": 2
551
+ }
552
+ SCHEMA
553
+
554
+ schema_hash =
555
+ {
556
+ 'type' => 'bytes',
557
+ 'logicalType' => 'decimal',
558
+ 'precision' => 9,
559
+ 'scale' => 2
560
+ }
561
+
562
+ assert_equal schema_hash, schema.to_avro
563
+ end
564
+
565
+ def test_bytes_decimal_to_without_precision_scale
566
+ schema = Avro::Schema.parse <<-SCHEMA
567
+ {
568
+ "type": "bytes",
569
+ "logicalType": "decimal"
570
+ }
571
+ SCHEMA
572
+
573
+ schema_hash =
574
+ {
575
+ 'type' => 'bytes',
576
+ 'logicalType' => 'decimal'
577
+ }
578
+
579
+ assert_equal schema_hash, schema.to_avro
580
+ end
581
+
582
+ def test_bytes_schema
583
+ schema = Avro::Schema.parse <<-SCHEMA
584
+ {
585
+ "type": "bytes"
586
+ }
587
+ SCHEMA
588
+
589
+ schema_str = 'bytes'
590
+ assert_equal schema_str, schema.to_avro
591
+ end
592
+
593
+ def test_validate_duplicate_symbols
594
+ exception = assert_raise(Avro::SchemaParseError) do
595
+ hash_to_schema(
596
+ type: 'enum',
597
+ name: 'name',
598
+ symbols: ['erica', 'erica']
599
+ )
600
+ end
601
+ assert_equal(
602
+ 'Duplicate symbol: ["erica", "erica"]',
603
+ exception.to_s
604
+ )
605
+ end
606
+
607
+ def test_validate_enum_symbols
608
+ exception = assert_raise(Avro::SchemaParseError) do
609
+ hash_to_schema(
610
+ type: 'enum',
611
+ name: 'things',
612
+ symbols: ['good_symbol', '_GOOD_SYMBOL_2', '8ad_symbol', 'also-bad-symbol', '>=', '$']
613
+ )
614
+ end
615
+
616
+ assert_equal(
617
+ "Invalid symbols for things: 8ad_symbol, also-bad-symbol, >=, $ don't match #{Avro::Schema::EnumSchema::SYMBOL_REGEX.inspect}",
618
+ exception.to_s
619
+ )
620
+ end
621
+
622
+ def test_enum_symbol_validation_disabled_via_env
623
+ Avro.disable_enum_symbol_validation = nil
624
+ ENV['AVRO_DISABLE_ENUM_SYMBOL_VALIDATION'] = '1'
625
+
626
+ hash_to_schema(
627
+ type: 'enum',
628
+ name: 'things',
629
+ symbols: ['good_symbol', '_GOOD_SYMBOL_2', '8ad_symbol', 'also-bad-symbol', '>=', '$'],
630
+ )
631
+ ensure
632
+ ENV.delete('AVRO_DISABLE_ENUM_SYMBOL_VALIDATION')
633
+ Avro.disable_enum_symbol_validation = nil
634
+ end
635
+
636
+ def test_enum_symbol_validation_disabled_via_class_method
637
+ Avro.disable_enum_symbol_validation = true
638
+
639
+ hash_to_schema(
640
+ type: 'enum',
641
+ name: 'things',
642
+ symbols: ['good_symbol', '_GOOD_SYMBOL_2', '8ad_symbol', 'also-bad-symbol', '>=', '$'],
643
+ )
644
+ ensure
645
+ Avro.disable_enum_symbol_validation = nil
646
+ end
647
+
648
+ def test_validate_field_aliases
649
+ exception = assert_raise(Avro::SchemaParseError) do
650
+ hash_to_schema(
651
+ type: 'record',
652
+ name: 'fruits',
653
+ fields: [
654
+ { name: 'banana', type: 'string', aliases: 'banane' }
655
+ ]
656
+ )
657
+ end
658
+
659
+ assert_match(/Invalid aliases value "banane" for "string" banana/, exception.to_s)
660
+ end
661
+
662
+ def test_validate_same_alias_multiple_fields
663
+ exception = assert_raise(Avro::SchemaParseError) do
664
+ hash_to_schema(
665
+ type: 'record',
666
+ name: 'fruits',
667
+ fields: [
668
+ { name: 'banana', type: 'string', aliases: %w(yellow) },
669
+ { name: 'lemo', type: 'string', aliases: %w(yellow) }
670
+ ]
671
+ )
672
+ end
673
+
674
+ assert_match('Alias ["yellow"] already in use', exception.to_s)
675
+ end
676
+
677
+ def test_validate_repeated_aliases
678
+ assert_nothing_raised do
679
+ hash_to_schema(
680
+ type: 'record',
681
+ name: 'fruits',
682
+ fields: [
683
+ { name: 'banana', type: 'string', aliases: %w(yellow yellow) },
684
+ ]
685
+ )
686
+ end
687
+ end
688
+
689
+ def test_validate_record_aliases
690
+ exception = assert_raise(Avro::SchemaParseError) do
691
+ hash_to_schema(
692
+ type: 'record',
693
+ name: 'fruits',
694
+ aliases: ["foods", 2],
695
+ fields: []
696
+ )
697
+ end
698
+
699
+ assert_match(/Invalid aliases value \["foods", 2\] for record fruits/, exception.to_s)
700
+ end
701
+
702
+ def test_validate_enum_aliases
703
+ exception = assert_raise(Avro::SchemaParseError) do
704
+ hash_to_schema(
705
+ type: 'enum',
706
+ name: 'vowels',
707
+ aliases: [1, 2],
708
+ symbols: %w(A E I O U)
709
+ )
710
+ end
711
+
712
+ assert_match(/Invalid aliases value \[1, 2\] for enum vowels/, exception.to_s)
713
+ end
714
+
715
+ def test_validate_fixed_aliases
716
+ exception = assert_raise(Avro::SchemaParseError) do
717
+ hash_to_schema(
718
+ type: 'fixed',
719
+ name: 'uuid',
720
+ size: 36,
721
+ aliases: "unique_id"
722
+ )
723
+ end
724
+
725
+ assert_match(/Invalid aliases value "unique_id" for fixed uuid/, exception.to_s)
726
+ end
146
727
  end