avro 1.8.2 → 1.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -74,10 +74,10 @@ module Avro::IPC
74
74
 
75
75
  class ConnectionClosedException < Avro::AvroError; end
76
76
 
77
+ # Base class for the client side of a protocol interaction.
77
78
  class Requestor
78
- """Base class for the client side of a protocol interaction."""
79
- attr_reader :local_protocol, :transport
80
- attr_accessor :remote_protocol, :remote_hash, :send_protocol
79
+ attr_reader :local_protocol, :transport, :remote_protocol, :remote_hash
80
+ attr_accessor :send_protocol
81
81
 
82
82
  def initialize(local_protocol, transport)
83
83
  @local_protocol = local_protocol
@@ -193,9 +193,9 @@ module Avro::IPC
193
193
  # * a one-byte error flag boolean, followed by either:
194
194
  # * if the error flag is false,
195
195
  # the message response, serialized per the message's response schema.
196
- # * if the error flag is true,
196
+ # * if the error flag is true,
197
197
  # the error, serialized per the message's error union schema.
198
- response_metadata = META_READER.read(decoder)
198
+ _response_metadata = META_READER.read(decoder)
199
199
 
200
200
  # remote response schema
201
201
  remote_message_schema = remote_protocol.messages[message_name]
@@ -257,7 +257,7 @@ module Avro::IPC
257
257
  end
258
258
 
259
259
  # read request using remote protocol
260
- request_metadata = META_READER.read(buffer_decoder)
260
+ _request_metadata = META_READER.read(buffer_decoder)
261
261
  remote_message_name = buffer_decoder.read_string
262
262
 
263
263
  # get remote and local request schemas so we can do
@@ -0,0 +1,90 @@
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 'date'
19
+
20
+ module Avro
21
+ module LogicalTypes
22
+ module IntDate
23
+ EPOCH_START = Date.new(1970, 1, 1)
24
+
25
+ def self.encode(date)
26
+ return date.to_i if date.is_a?(Numeric)
27
+
28
+ (date - EPOCH_START).to_i
29
+ end
30
+
31
+ def self.decode(int)
32
+ EPOCH_START + int
33
+ end
34
+ end
35
+
36
+ module TimestampMillis
37
+ def self.encode(value)
38
+ return value.to_i if value.is_a?(Numeric)
39
+
40
+ time = value.to_time
41
+ time.to_i * 1000 + time.usec / 1000
42
+ end
43
+
44
+ def self.decode(int)
45
+ s, ms = int / 1000, int % 1000
46
+ Time.at(s, ms * 1000).utc
47
+ end
48
+ end
49
+
50
+ module TimestampMicros
51
+ def self.encode(value)
52
+ return value.to_i if value.is_a?(Numeric)
53
+
54
+ time = value.to_time
55
+ time.to_i * 1000_000 + time.usec
56
+ end
57
+
58
+ def self.decode(int)
59
+ s, us = int / 1000_000, int % 1000_000
60
+ Time.at(s, us).utc
61
+ end
62
+ end
63
+
64
+ module Identity
65
+ def self.encode(datum)
66
+ datum
67
+ end
68
+
69
+ def self.decode(datum)
70
+ datum
71
+ end
72
+ end
73
+
74
+ TYPES = {
75
+ "int" => {
76
+ "date" => IntDate
77
+ },
78
+ "long" => {
79
+ "timestamp-millis" => TimestampMillis,
80
+ "timestamp-micros" => TimestampMicros
81
+ },
82
+ }.freeze
83
+
84
+ def self.type_adapter(type, logical_type)
85
+ return unless logical_type
86
+
87
+ TYPES.fetch(type, {}.freeze).fetch(logical_type, Identity)
88
+ end
89
+ end
90
+ 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,
@@ -20,7 +20,7 @@ module Avro
20
20
  VALID_TYPE_SCHEMA_TYPES_SYM = Set.new(VALID_TYPE_SCHEMA_TYPES.map(&:to_sym))
21
21
  class ProtocolParseError < Avro::AvroError; end
22
22
 
23
- attr_reader :name, :namespace, :types, :messages, :md5
23
+ attr_reader :name, :namespace, :types, :messages, :md5, :doc
24
24
  def self.parse(protocol_string)
25
25
  json_data = MultiJson.load(protocol_string)
26
26
 
@@ -29,13 +29,14 @@ module Avro
29
29
  namespace = json_data['namespace']
30
30
  types = json_data['types']
31
31
  messages = json_data['messages']
32
- Protocol.new(name, namespace, types, messages)
32
+ doc = json_data['doc']
33
+ Protocol.new(name, namespace, types, messages, doc)
33
34
  else
34
35
  raise ProtocolParseError, "Not a JSON object: #{json_data}"
35
36
  end
36
37
  end
37
38
 
38
- def initialize(name, namespace=nil, types=nil, messages=nil)
39
+ def initialize(name, namespace=nil, types=nil, messages=nil, doc=nil)
39
40
  # Ensure valid ctor args
40
41
  if !name
41
42
  raise ProtocolParseError, 'Protocols must have a non-empty name.'
@@ -55,6 +56,7 @@ module Avro
55
56
  @types = parse_types(types, type_names)
56
57
  @messages = parse_messages(messages, type_names)
57
58
  @md5 = Digest::MD5.digest(to_s)
59
+ @doc = doc
58
60
  end
59
61
 
60
62
  def to_s
@@ -67,7 +69,6 @@ module Avro
67
69
 
68
70
  private
69
71
  def parse_types(types, type_names)
70
- type_objects = []
71
72
  types.collect do |type|
72
73
  # FIXME adding type.name to type_names is not defined in the
73
74
  # spec. Possible bug in the python impl and the spec.
@@ -92,7 +93,8 @@ module Avro
92
93
  request = body['request']
93
94
  response = body['response']
94
95
  errors = body['errors']
95
- message_objects[name] = Message.new(name, request, response, errors, names, namespace)
96
+ doc = body['doc']
97
+ message_objects[name] = Message.new(name, request, response, errors, names, namespace, doc)
96
98
  end
97
99
  message_objects
98
100
  end
@@ -111,14 +113,15 @@ module Avro
111
113
  end
112
114
 
113
115
  class Message
114
- attr_reader :name, :request, :response, :errors, :default_namespace
116
+ attr_reader :name, :request, :response, :errors, :default_namespace, :doc
115
117
 
116
- def initialize(name, request, response, errors=nil, names=nil, default_namespace=nil)
118
+ def initialize(name, request, response, errors=nil, names=nil, default_namespace=nil, doc=nil)
117
119
  @name = name
118
120
  @default_namespace = default_namespace
119
121
  @request = parse_request(request, names)
120
122
  @response = parse_response(response, names)
121
123
  @errors = parse_errors(errors, names) if errors
124
+ @doc = doc
122
125
  end
123
126
 
124
127
  def to_avro(names=Set.new)
@@ -127,6 +130,7 @@ module Avro
127
130
  'response' => response.to_avro(names)
128
131
  }.tap do |hash|
129
132
  hash['errors'] = errors.to_avro(names) if errors
133
+ hash['doc'] = @doc if @doc
130
134
  end
131
135
  end
132
136
 
@@ -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,
@@ -14,6 +14,8 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require 'avro/logical_types'
18
+
17
19
  module Avro
18
20
  class Schema
19
21
  # Sets of strings, for backwards compatibility. See below for sets of symbols,
@@ -40,6 +42,7 @@ module Avro
40
42
  def self.real_parse(json_obj, names=nil, default_namespace=nil)
41
43
  if json_obj.is_a? Hash
42
44
  type = json_obj['type']
45
+ logical_type = json_obj['logicalType']
43
46
  raise SchemaParseError, %Q(No "type" property: #{json_obj}) if type.nil?
44
47
 
45
48
  # Check that the type is valid before calling #to_sym, since symbols are never garbage
@@ -50,7 +53,7 @@ module Avro
50
53
 
51
54
  type_sym = type.to_sym
52
55
  if PRIMITIVE_TYPES_SYM.include?(type_sym)
53
- return PrimitiveSchema.new(type_sym)
56
+ return PrimitiveSchema.new(type_sym, logical_type)
54
57
 
55
58
  elsif NAMED_TYPES_SYM.include? type_sym
56
59
  name = json_obj['name']
@@ -58,13 +61,15 @@ module Avro
58
61
  case type_sym
59
62
  when :fixed
60
63
  size = json_obj['size']
61
- return FixedSchema.new(name, namespace, size, names)
64
+ return FixedSchema.new(name, namespace, size, names, logical_type)
62
65
  when :enum
63
66
  symbols = json_obj['symbols']
64
- return EnumSchema.new(name, namespace, symbols, names)
67
+ doc = json_obj['doc']
68
+ return EnumSchema.new(name, namespace, symbols, names, doc)
65
69
  when :record, :error
66
70
  fields = json_obj['fields']
67
- return RecordSchema.new(name, namespace, fields, names, type_sym)
71
+ doc = json_obj['doc']
72
+ return RecordSchema.new(name, namespace, fields, names, type_sym, doc)
68
73
  else
69
74
  raise SchemaParseError.new("Unknown named type: #{type}")
70
75
  end
@@ -91,52 +96,29 @@ module Avro
91
96
  end
92
97
 
93
98
  # Determine if a ruby datum is an instance of a schema
94
- def self.validate(expected_schema, datum)
95
- case expected_schema.type_sym
96
- when :null
97
- datum.nil?
98
- when :boolean
99
- datum == true || datum == false
100
- when :string, :bytes
101
- datum.is_a? String
102
- when :int
103
- (datum.is_a?(Fixnum) || datum.is_a?(Bignum)) &&
104
- (INT_MIN_VALUE <= datum) && (datum <= INT_MAX_VALUE)
105
- when :long
106
- (datum.is_a?(Fixnum) || datum.is_a?(Bignum)) &&
107
- (LONG_MIN_VALUE <= datum) && (datum <= LONG_MAX_VALUE)
108
- when :float, :double
109
- datum.is_a?(Float) || datum.is_a?(Fixnum) || datum.is_a?(Bignum)
110
- when :fixed
111
- datum.is_a?(String) && datum.bytesize == expected_schema.size
112
- when :enum
113
- expected_schema.symbols.include? datum
114
- when :array
115
- datum.is_a?(Array) &&
116
- datum.all?{|d| validate(expected_schema.items, d) }
117
- when :map
118
- datum.keys.all?{|k| k.is_a? String } &&
119
- datum.values.all?{|v| validate(expected_schema.values, v) }
120
- when :union
121
- expected_schema.schemas.any?{|s| validate(s, datum) }
122
- when :record, :error, :request
123
- datum.is_a?(Hash) &&
124
- expected_schema.fields.all?{|f| validate(f.type, datum[f.name]) }
125
- else
126
- raise "you suck #{expected_schema.inspect} is not allowed."
127
- end
99
+ def self.validate(expected_schema, logical_datum, options = { recursive: true, encoded: false })
100
+ SchemaValidator.validate!(expected_schema, logical_datum, options)
101
+ true
102
+ rescue SchemaValidator::ValidationError
103
+ false
128
104
  end
129
105
 
130
- def initialize(type)
106
+ def initialize(type, logical_type=nil)
131
107
  @type_sym = type.is_a?(Symbol) ? type : type.to_sym
108
+ @logical_type = logical_type
132
109
  end
133
110
 
134
111
  attr_reader :type_sym
112
+ attr_reader :logical_type
135
113
 
136
114
  # Returns the type as a string (rather than a symbol), for backwards compatibility.
137
115
  # Deprecated in favor of {#type_sym}.
138
116
  def type; @type_sym.to_s; end
139
117
 
118
+ def type_adapter
119
+ @type_adapter ||= LogicalTypes.type_adapter(type, logical_type) || LogicalTypes::Identity
120
+ end
121
+
140
122
  # Returns the MD5 fingerprint of the schema as an Integer.
141
123
  def md5_fingerprint
142
124
  parsing_form = SchemaNormalization.to_parsing_form(self)
@@ -149,6 +131,18 @@ module Avro
149
131
  Digest::SHA256.hexdigest(parsing_form).to_i(16)
150
132
  end
151
133
 
134
+ def read?(writers_schema)
135
+ SchemaCompatibility.can_read?(writers_schema, self)
136
+ end
137
+
138
+ def be_read?(other_schema)
139
+ other_schema.read?(self)
140
+ end
141
+
142
+ def mutual_read?(other_schema)
143
+ SchemaCompatibility.mutual_read?(other_schema, self)
144
+ end
145
+
152
146
  def ==(other, seen=nil)
153
147
  other.is_a?(Schema) && type_sym == other.type_sym
154
148
  end
@@ -172,7 +166,9 @@ module Avro
172
166
  end
173
167
 
174
168
  def to_avro(names=nil)
175
- {'type' => type}
169
+ props = {'type' => type}
170
+ props['logicalType'] = logical_type if logical_type
171
+ props
176
172
  end
177
173
 
178
174
  def to_s
@@ -181,9 +177,11 @@ module Avro
181
177
 
182
178
  class NamedSchema < Schema
183
179
  attr_reader :name, :namespace
184
- def initialize(type, name, namespace=nil, names=nil)
185
- super(type)
180
+
181
+ def initialize(type, name, namespace=nil, names=nil, doc=nil, logical_type=nil)
182
+ super(type, logical_type)
186
183
  @name, @namespace = Name.extract_namespace(name, namespace)
184
+ @doc = doc
187
185
  names = Name.add_name(names, self)
188
186
  end
189
187
 
@@ -194,6 +192,7 @@ module Avro
194
192
  end
195
193
  props = {'name' => @name}
196
194
  props.merge!('namespace' => @namespace) if @namespace
195
+ props.merge!('doc' => @doc) if @doc
197
196
  super.merge props
198
197
  end
199
198
 
@@ -203,7 +202,7 @@ module Avro
203
202
  end
204
203
 
205
204
  class RecordSchema < NamedSchema
206
- attr_reader :fields
205
+ attr_reader :fields, :doc
207
206
 
208
207
  def self.make_field_objects(field_data, names, namespace=nil)
209
208
  field_objects, field_names = [], Set.new
@@ -213,7 +212,8 @@ module Avro
213
212
  name = field['name']
214
213
  default = field.key?('default') ? field['default'] : :no_default
215
214
  order = field['order']
216
- new_field = Field.new(type, name, default, order, names, namespace)
215
+ doc = field['doc']
216
+ new_field = Field.new(type, name, default, order, names, namespace, doc)
217
217
  # make sure field name has not been used yet
218
218
  if field_names.include?(new_field.name)
219
219
  raise SchemaParseError, "Field name #{new_field.name.inspect} is already in use"
@@ -227,14 +227,20 @@ module Avro
227
227
  field_objects
228
228
  end
229
229
 
230
- def initialize(name, namespace, fields, names=nil, schema_type=:record)
230
+ def initialize(name, namespace, fields, names=nil, schema_type=:record, doc=nil)
231
231
  if schema_type == :request || schema_type == 'request'
232
232
  @type_sym = schema_type.to_sym
233
233
  @namespace = namespace
234
+ @name = nil
235
+ @doc = nil
234
236
  else
235
- super(schema_type, name, namespace, names)
237
+ super(schema_type, name, namespace, names, doc)
236
238
  end
237
- @fields = RecordSchema.make_field_objects(fields, names, self.namespace)
239
+ @fields = if fields
240
+ RecordSchema.make_field_objects(fields, names, self.namespace)
241
+ else
242
+ {}
243
+ end
238
244
  end
239
245
 
240
246
  def fields_hash
@@ -285,8 +291,7 @@ module Avro
285
291
  def initialize(schemas, names=nil, default_namespace=nil)
286
292
  super(:union)
287
293
 
288
- schema_objects = []
289
- schemas.each_with_index do |schema, i|
294
+ @schemas = schemas.each_with_object([]) do |schema, schema_objects|
290
295
  new_schema = subparse(schema, names, default_namespace)
291
296
  ns_type = new_schema.type_sym
292
297
 
@@ -299,7 +304,6 @@ module Avro
299
304
  else
300
305
  schema_objects << new_schema
301
306
  end
302
- @schemas = schema_objects
303
307
  end
304
308
  end
305
309
 
@@ -309,13 +313,14 @@ module Avro
309
313
  end
310
314
 
311
315
  class EnumSchema < NamedSchema
312
- attr_reader :symbols
313
- def initialize(name, space, symbols, names=nil)
316
+ attr_reader :symbols, :doc
317
+
318
+ def initialize(name, space, symbols, names=nil, doc=nil)
314
319
  if symbols.uniq.length < symbols.length
315
- fail_msg = 'Duplicate symbol: %s' % symbols
320
+ fail_msg = "Duplicate symbol: #{symbols}"
316
321
  raise Avro::SchemaParseError, fail_msg
317
322
  end
318
- super(:enum, name, space, names)
323
+ super(:enum, name, space, names, doc)
319
324
  @symbols = symbols
320
325
  end
321
326
 
@@ -327,11 +332,11 @@ module Avro
327
332
 
328
333
  # Valid primitive types are in PRIMITIVE_TYPES.
329
334
  class PrimitiveSchema < Schema
330
- def initialize(type)
335
+ def initialize(type, logical_type=nil)
331
336
  if PRIMITIVE_TYPES_SYM.include?(type)
332
- super(type)
337
+ super(type, logical_type)
333
338
  elsif PRIMITIVE_TYPES.include?(type)
334
- super(type.to_sym)
339
+ super(type.to_sym, logical_type)
335
340
  else
336
341
  raise AvroError.new("#{type} is not a valid primitive type.")
337
342
  end
@@ -345,12 +350,12 @@ module Avro
345
350
 
346
351
  class FixedSchema < NamedSchema
347
352
  attr_reader :size
348
- def initialize(name, space, size, names=nil)
353
+ def initialize(name, space, size, names=nil, logical_type=nil)
349
354
  # Ensure valid cto args
350
- unless size.is_a?(Fixnum) || size.is_a?(Bignum)
355
+ unless size.is_a?(Integer)
351
356
  raise AvroError, 'Fixed Schema requires a valid integer for size property.'
352
357
  end
353
- super(:fixed, name, space, names)
358
+ super(:fixed, name, space, names, nil, logical_type)
354
359
  @size = size
355
360
  end
356
361
 
@@ -361,21 +366,42 @@ module Avro
361
366
  end
362
367
 
363
368
  class Field < Schema
364
- attr_reader :type, :name, :default, :order
369
+ attr_reader :type, :name, :default, :order, :doc
365
370
 
366
- def initialize(type, name, default=:no_default, order=nil, names=nil, namespace=nil)
371
+ def initialize(type, name, default=:no_default, order=nil, names=nil, namespace=nil, doc=nil)
367
372
  @type = subparse(type, names, namespace)
368
373
  @name = name
369
374
  @default = default
370
375
  @order = order
376
+ @doc = doc
377
+ validate_default! if default? && !Avro.disable_field_default_validation
378
+ end
379
+
380
+ def default?
381
+ @default != :no_default
371
382
  end
372
383
 
373
384
  def to_avro(names=Set.new)
374
385
  {'name' => name, 'type' => type.to_avro(names)}.tap do |avro|
375
- avro['default'] = default unless default == :no_default
386
+ avro['default'] = default if default?
376
387
  avro['order'] = order if order
388
+ avro['doc'] = doc if doc
377
389
  end
378
390
  end
391
+
392
+ private
393
+
394
+ def validate_default!
395
+ type_for_default = if type.type_sym == :union
396
+ type.schemas.first
397
+ else
398
+ type
399
+ end
400
+
401
+ Avro::SchemaValidator.validate!(type_for_default, default)
402
+ rescue Avro::SchemaValidator::ValidationError => e
403
+ raise Avro::SchemaParseError, "Error validating default for #{name}: #{e.message}"
404
+ end
379
405
  end
380
406
  end
381
407