deimos-ruby 1.4.0.pre.beta7 → 1.5.0.pre.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +140 -58
- data/README.md +38 -11
- data/Rakefile +2 -2
- data/deimos-ruby.gemspec +3 -2
- data/docs/CONFIGURATION.md +1 -0
- data/docs/DATABASE_BACKEND.md +1 -1
- data/lib/deimos/active_record_consumer.rb +11 -12
- data/lib/deimos/active_record_producer.rb +2 -2
- data/lib/deimos/backends/base.rb +32 -0
- data/lib/deimos/backends/db.rb +6 -1
- data/lib/deimos/backends/kafka.rb +1 -1
- data/lib/deimos/backends/kafka_async.rb +1 -1
- data/lib/deimos/backends/test.rb +20 -0
- data/lib/deimos/base_consumer.rb +7 -7
- data/lib/deimos/batch_consumer.rb +0 -1
- data/lib/deimos/config/configuration.rb +4 -0
- data/lib/deimos/consumer.rb +0 -2
- data/lib/deimos/kafka_source.rb +1 -1
- data/lib/deimos/kafka_topic_info.rb +1 -1
- data/lib/deimos/message.rb +7 -7
- data/lib/deimos/producer.rb +10 -12
- data/lib/deimos/schema_backends/avro_base.rb +108 -0
- data/lib/deimos/schema_backends/avro_local.rb +30 -0
- data/lib/deimos/{schema_coercer.rb → schema_backends/avro_schema_coercer.rb} +39 -51
- data/lib/deimos/schema_backends/avro_schema_registry.rb +34 -0
- data/lib/deimos/schema_backends/avro_validation.rb +21 -0
- data/lib/deimos/schema_backends/base.rb +130 -0
- data/lib/deimos/schema_backends/mock.rb +42 -0
- data/lib/deimos/test_helpers.rb +42 -168
- data/lib/deimos/utils/db_producer.rb +5 -0
- data/lib/deimos/version.rb +1 -1
- data/lib/deimos.rb +22 -6
- data/lib/tasks/deimos.rake +1 -1
- data/spec/active_record_consumer_spec.rb +7 -0
- data/spec/{publish_backend_spec.rb → backends/base_spec.rb} +1 -1
- data/spec/backends/db_spec.rb +5 -0
- data/spec/batch_consumer_spec.rb +0 -8
- data/spec/config/configuration_spec.rb +20 -20
- data/spec/consumer_spec.rb +0 -1
- data/spec/deimos_spec.rb +0 -4
- data/spec/kafka_source_spec.rb +8 -0
- data/spec/producer_spec.rb +23 -37
- data/spec/rake_spec.rb +19 -0
- data/spec/schema_backends/avro_base_shared.rb +174 -0
- data/spec/schema_backends/avro_local_spec.rb +32 -0
- data/spec/schema_backends/avro_schema_registry_spec.rb +32 -0
- data/spec/schema_backends/avro_validation_spec.rb +24 -0
- data/spec/schema_backends/base_spec.rb +29 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/utils/db_producer_spec.rb +10 -0
- metadata +56 -33
- data/lib/deimos/avro_data_coder.rb +0 -89
- data/lib/deimos/avro_data_decoder.rb +0 -36
- data/lib/deimos/avro_data_encoder.rb +0 -51
- data/lib/deimos/monkey_patches/schema_store.rb +0 -19
- data/lib/deimos/publish_backend.rb +0 -30
- data/spec/avro_data_decoder_spec.rb +0 -18
- data/spec/avro_data_encoder_spec.rb +0 -37
- data/spec/updateable_schema_store_spec.rb +0 -36
data/lib/deimos/base_consumer.rb
CHANGED
@@ -6,20 +6,20 @@ module Deimos
|
|
6
6
|
include SharedConfig
|
7
7
|
|
8
8
|
class << self
|
9
|
-
# @return [
|
9
|
+
# @return [Deimos::SchemaBackends::Base]
|
10
10
|
def decoder
|
11
|
-
@decoder ||=
|
12
|
-
|
11
|
+
@decoder ||= Deimos.schema_backend(schema: config[:schema],
|
12
|
+
namespace: config[:namespace])
|
13
13
|
end
|
14
14
|
|
15
|
-
# @return [
|
15
|
+
# @return [Deimos::SchemaBackends::Base]
|
16
16
|
def key_decoder
|
17
|
-
@key_decoder ||=
|
18
|
-
|
17
|
+
@key_decoder ||= Deimos.schema_backend(schema: config[:key_schema],
|
18
|
+
namespace: config[:namespace])
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
# Helper method to decode an
|
22
|
+
# Helper method to decode an encoded key.
|
23
23
|
# @param key [String]
|
24
24
|
# @return [Object] the decoded key.
|
25
25
|
def decode_key(key)
|
@@ -251,6 +251,10 @@ module Deimos
|
|
251
251
|
end
|
252
252
|
|
253
253
|
setting :schema do
|
254
|
+
|
255
|
+
# Backend class to use when encoding/decoding messages.
|
256
|
+
setting :backend, :mock
|
257
|
+
|
254
258
|
# URL of the Confluent schema registry.
|
255
259
|
# @return [String]
|
256
260
|
setting :registry_url, 'http://localhost:8081'
|
data/lib/deimos/consumer.rb
CHANGED
@@ -1,11 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'deimos/avro_data_decoder'
|
4
3
|
require 'deimos/base_consumer'
|
5
4
|
require 'deimos/shared_config'
|
6
5
|
require 'phobos/handler'
|
7
6
|
require 'active_support/all'
|
8
|
-
require 'ddtrace'
|
9
7
|
|
10
8
|
# Class to consume messages coming from the pipeline topic
|
11
9
|
# Note: According to the docs, instances of your handler will be created
|
data/lib/deimos/kafka_source.rb
CHANGED
@@ -41,7 +41,7 @@ module Deimos
|
|
41
41
|
def send_kafka_event_on_destroy
|
42
42
|
return unless self.class.kafka_config[:delete]
|
43
43
|
|
44
|
-
self.class.kafka_producers.each { |p| p.
|
44
|
+
self.class.kafka_producers.each { |p| p.publish_list([self.deletion_payload]) }
|
45
45
|
end
|
46
46
|
|
47
47
|
# Payload to send after we are destroyed.
|
@@ -14,7 +14,7 @@ module Deimos
|
|
14
14
|
# Try to create it - it's fine if it already exists
|
15
15
|
begin
|
16
16
|
self.create(topic: topic)
|
17
|
-
rescue ActiveRecord::RecordNotUnique # rubocop:disable Lint/
|
17
|
+
rescue ActiveRecord::RecordNotUnique # rubocop:disable Lint/SuppressedException
|
18
18
|
# continue on
|
19
19
|
end
|
20
20
|
|
data/lib/deimos/message.rb
CHANGED
@@ -18,23 +18,23 @@ module Deimos
|
|
18
18
|
|
19
19
|
# Add message_id and timestamp default values if they are in the
|
20
20
|
# schema and don't already have values.
|
21
|
-
# @param
|
22
|
-
def add_fields(
|
21
|
+
# @param fields [Array<String>] existing name fields in the schema.
|
22
|
+
def add_fields(fields)
|
23
23
|
return if @payload.except(:payload_key, :partition_key).blank?
|
24
24
|
|
25
|
-
if
|
25
|
+
if fields.include?('message_id')
|
26
26
|
@payload['message_id'] ||= SecureRandom.uuid
|
27
27
|
end
|
28
|
-
if
|
28
|
+
if fields.include?('timestamp')
|
29
29
|
@payload['timestamp'] ||= Time.now.in_time_zone.to_s
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
# @param
|
34
|
-
def coerce_fields(
|
33
|
+
# @param encoder [Deimos::SchemaBackends::Base]
|
34
|
+
def coerce_fields(encoder)
|
35
35
|
return if payload.nil?
|
36
36
|
|
37
|
-
@payload =
|
37
|
+
@payload = encoder.coerce(@payload)
|
38
38
|
end
|
39
39
|
|
40
40
|
# @return [Hash]
|
data/lib/deimos/producer.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'deimos/avro_data_encoder'
|
4
3
|
require 'deimos/message'
|
5
4
|
require 'deimos/shared_config'
|
6
|
-
require 'deimos/schema_coercer'
|
7
5
|
require 'phobos/producer'
|
8
6
|
require 'active_support/notifications'
|
9
7
|
|
@@ -143,23 +141,23 @@ module Deimos
|
|
143
141
|
backend.publish(producer_class: self, messages: batch)
|
144
142
|
end
|
145
143
|
|
146
|
-
# @return [
|
144
|
+
# @return [Deimos::SchemaBackends::Base]
|
147
145
|
def encoder
|
148
|
-
@encoder ||=
|
149
|
-
|
146
|
+
@encoder ||= Deimos.schema_backend(schema: config[:schema],
|
147
|
+
namespace: config[:namespace])
|
150
148
|
end
|
151
149
|
|
152
|
-
# @return [
|
150
|
+
# @return [Deimos::SchemaBackends::Base]
|
153
151
|
def key_encoder
|
154
|
-
@key_encoder ||=
|
155
|
-
|
152
|
+
@key_encoder ||= Deimos.schema_backend(schema: config[:key_schema],
|
153
|
+
namespace: config[:namespace])
|
156
154
|
end
|
157
155
|
|
158
156
|
# Override this in active record producers to add
|
159
157
|
# non-schema fields to check for updates
|
160
158
|
# @return [Array<String>] fields to check for updates
|
161
159
|
def watched_attributes
|
162
|
-
self.encoder.
|
160
|
+
self.encoder.schema_fields.map(&:name)
|
163
161
|
end
|
164
162
|
|
165
163
|
private
|
@@ -169,13 +167,13 @@ module Deimos
|
|
169
167
|
# this violates the Law of Demeter but it has to happen in a very
|
170
168
|
# specific order and requires a bunch of methods on the producer
|
171
169
|
# to work correctly.
|
172
|
-
message.add_fields(encoder.
|
170
|
+
message.add_fields(encoder.schema_fields.map(&:name))
|
173
171
|
message.partition_key = self.partition_key(message.payload)
|
174
172
|
message.key = _retrieve_key(message.payload)
|
175
173
|
# need to do this before _coerce_fields because that might result
|
176
174
|
# in an empty payload which is an *error* whereas this is intended.
|
177
175
|
message.payload = nil if message.payload.blank?
|
178
|
-
message.coerce_fields(encoder
|
176
|
+
message.coerce_fields(encoder)
|
179
177
|
message.encoded_key = _encode_key(message.key)
|
180
178
|
message.topic = self.topic
|
181
179
|
message.encoded_payload = if message.payload.nil?
|
@@ -200,7 +198,7 @@ module Deimos
|
|
200
198
|
end
|
201
199
|
|
202
200
|
if config[:key_field]
|
203
|
-
encoder.encode_key(config[:key_field], key, "#{config[:topic]}-key")
|
201
|
+
encoder.encode_key(config[:key_field], key, topic: "#{config[:topic]}-key")
|
204
202
|
elsif config[:key_schema]
|
205
203
|
key_encoder.encode(key, topic: "#{config[:topic]}-key")
|
206
204
|
else
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'avro'
|
5
|
+
require 'avro_turf'
|
6
|
+
require 'avro_turf/mutable_schema_store'
|
7
|
+
require_relative 'avro_schema_coercer'
|
8
|
+
|
9
|
+
module Deimos
|
10
|
+
module SchemaBackends
|
11
|
+
# Encode / decode using Avro, either locally or via schema registry.
|
12
|
+
class AvroBase < Base
|
13
|
+
attr_accessor :schema_store
|
14
|
+
|
15
|
+
# @override
|
16
|
+
def initialize(schema:, namespace:)
|
17
|
+
super(schema: schema, namespace: namespace)
|
18
|
+
@schema_store = AvroTurf::MutableSchemaStore.new(path: Deimos.config.schema.path)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @override
|
22
|
+
def encode_key(key_id, key, topic: nil)
|
23
|
+
@key_schema ||= _generate_key_schema(key_id)
|
24
|
+
field_name = _field_name_from_schema(@key_schema)
|
25
|
+
payload = { field_name => key }
|
26
|
+
encode(payload, schema: @key_schema['name'], topic: topic)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @override
|
30
|
+
def decode_key(payload, key_id)
|
31
|
+
@key_schema ||= _generate_key_schema(key_id)
|
32
|
+
field_name = _field_name_from_schema(@key_schema)
|
33
|
+
decode(payload, schema: @key_schema['name'])[field_name]
|
34
|
+
end
|
35
|
+
|
36
|
+
# @override
|
37
|
+
def coerce_field(field, value)
|
38
|
+
AvroSchemaCoercer.new(avro_schema).coerce_type(field.type, value)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @override
|
42
|
+
def schema_fields
|
43
|
+
avro_schema.fields.map { |field| SchemaField.new(field.name, field.type) }
|
44
|
+
end
|
45
|
+
|
46
|
+
# @override
|
47
|
+
def validate(payload, schema:)
|
48
|
+
Avro::SchemaValidator.validate!(avro_schema(schema), payload,
|
49
|
+
recursive: true,
|
50
|
+
fail_on_extra_fields: true)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @override
|
54
|
+
def self.mock_backend
|
55
|
+
:avro_validation
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# @param schema [String]
|
61
|
+
# @return [Avro::Schema]
|
62
|
+
def avro_schema(schema=nil)
|
63
|
+
schema ||= @schema
|
64
|
+
@schema_store.find(schema, @namespace)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Generate a key schema from the given value schema and key ID. This
|
68
|
+
# is used when encoding or decoding keys from an existing value schema.
|
69
|
+
# @param key_id [Symbol]
|
70
|
+
# @return [Hash]
|
71
|
+
def _generate_key_schema(key_id)
|
72
|
+
key_field = avro_schema.fields.find { |f| f.name == key_id.to_s }
|
73
|
+
name = _key_schema_name(@schema)
|
74
|
+
key_schema = {
|
75
|
+
'type' => 'record',
|
76
|
+
'name' => name,
|
77
|
+
'namespace' => @namespace,
|
78
|
+
'doc' => "Key for #{@namespace}.#{@schema} - autogenerated by Deimos",
|
79
|
+
'fields' => [
|
80
|
+
{
|
81
|
+
'name' => key_id,
|
82
|
+
'type' => key_field.type.type_sym.to_s
|
83
|
+
}
|
84
|
+
]
|
85
|
+
}
|
86
|
+
@schema_store.add_schema(key_schema)
|
87
|
+
key_schema
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param value_schema [Hash]
|
91
|
+
# @return [String]
|
92
|
+
def _field_name_from_schema(value_schema)
|
93
|
+
raise "Schema #{@schema} not found!" if value_schema.nil?
|
94
|
+
if value_schema['fields'].nil? || value_schema['fields'].empty?
|
95
|
+
raise "Schema #{@schema} has no fields!"
|
96
|
+
end
|
97
|
+
|
98
|
+
value_schema['fields'][0]['name']
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param schema [String]
|
102
|
+
# @return [String]
|
103
|
+
def _key_schema_name(schema)
|
104
|
+
"#{schema}_key"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'avro_base'
|
4
|
+
|
5
|
+
module Deimos
|
6
|
+
module SchemaBackends
|
7
|
+
# Encode / decode using local Avro encoding.
|
8
|
+
class AvroLocal < AvroBase
|
9
|
+
# @override
|
10
|
+
def decode_payload(payload, schema:)
|
11
|
+
avro_turf.decode(payload, schema_name: schema, namespace: @namespace)
|
12
|
+
end
|
13
|
+
|
14
|
+
# @override
|
15
|
+
def encode_payload(payload, schema: nil, topic: nil)
|
16
|
+
avro_turf.encode(payload, schema_name: schema, namespace: @namespace)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @return [AvroTurf]
|
22
|
+
def avro_turf
|
23
|
+
@avro_turf ||= AvroTurf.new(
|
24
|
+
schemas_path: Deimos.config.schema.path,
|
25
|
+
schema_store: @schema_store
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,66 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_support/time'
|
4
|
+
|
3
5
|
module Deimos
|
4
6
|
# Class to coerce values in a payload to match a schema.
|
5
|
-
class
|
7
|
+
class AvroSchemaCoercer
|
6
8
|
# @param schema [Avro::Schema]
|
7
9
|
def initialize(schema)
|
8
10
|
@schema = schema
|
9
11
|
end
|
10
12
|
|
11
|
-
# @param payload [Hash]
|
12
|
-
# @return [HashWithIndifferentAccess]
|
13
|
-
def coerce(payload)
|
14
|
-
result = {}
|
15
|
-
@schema.fields.each do |field|
|
16
|
-
name = field.name
|
17
|
-
next unless payload.key?(name)
|
18
|
-
|
19
|
-
val = payload[name]
|
20
|
-
result[name] = _coerce_type(field.type, val)
|
21
|
-
end
|
22
|
-
result.with_indifferent_access
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
# @param val [String]
|
28
|
-
# @return [Boolean]
|
29
|
-
def _is_integer_string?(val)
|
30
|
-
return false unless val.is_a?(String)
|
31
|
-
|
32
|
-
begin
|
33
|
-
true if Integer(val)
|
34
|
-
rescue StandardError
|
35
|
-
false
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# @param val [String]
|
40
|
-
# @return [Boolean]
|
41
|
-
def _is_float_string?(val)
|
42
|
-
return false unless val.is_a?(String)
|
43
|
-
|
44
|
-
begin
|
45
|
-
true if Float(val)
|
46
|
-
rescue StandardError
|
47
|
-
false
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# @param val [Object]
|
52
|
-
# @return [Boolean]
|
53
|
-
def _is_to_s_defined?(val)
|
54
|
-
return false if val.nil?
|
55
|
-
|
56
|
-
Object.instance_method(:to_s).bind(val).call != val.to_s
|
57
|
-
end
|
58
|
-
|
59
13
|
# @param type [Symbol]
|
60
14
|
# @param val [Object]
|
61
15
|
# @return [Object]
|
62
|
-
def
|
63
|
-
int_classes = [Time,
|
16
|
+
def coerce_type(type, val)
|
17
|
+
int_classes = [Time, ActiveSupport::TimeWithZone]
|
64
18
|
field_type = type.type.to_sym
|
65
19
|
if field_type == :union
|
66
20
|
union_types = type.schemas.map { |s| s.type.to_sym }
|
@@ -104,5 +58,39 @@ module Deimos
|
|
104
58
|
val
|
105
59
|
end
|
106
60
|
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# @param val [String]
|
65
|
+
# @return [Boolean]
|
66
|
+
def _is_integer_string?(val)
|
67
|
+
return false unless val.is_a?(String)
|
68
|
+
|
69
|
+
begin
|
70
|
+
true if Integer(val)
|
71
|
+
rescue StandardError
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param val [String]
|
77
|
+
# @return [Boolean]
|
78
|
+
def _is_float_string?(val)
|
79
|
+
return false unless val.is_a?(String)
|
80
|
+
|
81
|
+
begin
|
82
|
+
true if Float(val)
|
83
|
+
rescue StandardError
|
84
|
+
false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# @param val [Object]
|
89
|
+
# @return [Boolean]
|
90
|
+
def _is_to_s_defined?(val)
|
91
|
+
return false if val.nil?
|
92
|
+
|
93
|
+
Object.instance_method(:to_s).bind(val).call != val.to_s
|
94
|
+
end
|
107
95
|
end
|
108
96
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'avro_base'
|
4
|
+
require_relative 'avro_validation'
|
5
|
+
require 'avro_turf/messaging'
|
6
|
+
|
7
|
+
module Deimos
|
8
|
+
module SchemaBackends
|
9
|
+
# Encode / decode using the Avro schema registry.
|
10
|
+
class AvroSchemaRegistry < AvroBase
|
11
|
+
# @override
|
12
|
+
def decode_payload(payload, schema:)
|
13
|
+
avro_turf_messaging.decode(payload, schema_name: schema)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @override
|
17
|
+
def encode_payload(payload, schema: nil, topic: nil)
|
18
|
+
avro_turf_messaging.encode(payload, schema_name: schema, subject: topic)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# @return [AvroTurf::Messaging]
|
24
|
+
def avro_turf_messaging
|
25
|
+
@avro_turf_messaging ||= AvroTurf::Messaging.new(
|
26
|
+
schema_store: @schema_store,
|
27
|
+
registry_url: Deimos.config.schema.registry_url,
|
28
|
+
schemas_path: Deimos.config.schema.path,
|
29
|
+
namespace: @namespace
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'avro_base'
|
4
|
+
|
5
|
+
module Deimos
|
6
|
+
module SchemaBackends
|
7
|
+
# Leave Ruby hashes as is but validate them against the schema.
|
8
|
+
# Useful for unit tests.
|
9
|
+
class AvroValidation < AvroBase
|
10
|
+
# @override
|
11
|
+
def decode_payload(payload, schema: nil)
|
12
|
+
payload.with_indifferent_access
|
13
|
+
end
|
14
|
+
|
15
|
+
# @override
|
16
|
+
def encode_payload(payload, schema: nil, topic: nil)
|
17
|
+
payload.with_indifferent_access
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
# Represents a field in the schema.
|
5
|
+
class SchemaField
|
6
|
+
attr_accessor :name, :type
|
7
|
+
|
8
|
+
# @param name [String]
|
9
|
+
# @param type [Object]
|
10
|
+
def initialize(name, type)
|
11
|
+
@name = name
|
12
|
+
@type = type
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module SchemaBackends
|
17
|
+
# Base class for encoding / decoding.
|
18
|
+
class Base
|
19
|
+
attr_accessor :schema, :namespace, :key_schema
|
20
|
+
|
21
|
+
# @param schema [String|Symbol]
|
22
|
+
# @param namespace [String]
|
23
|
+
def initialize(schema:, namespace: nil)
|
24
|
+
@schema = schema
|
25
|
+
@namespace = namespace
|
26
|
+
end
|
27
|
+
|
28
|
+
# Encode a payload with a schema. Public method.
|
29
|
+
# @param payload [Hash]
|
30
|
+
# @param schema [Symbol|String]
|
31
|
+
# @param topic [String]
|
32
|
+
# @return [String]
|
33
|
+
def encode(payload, schema: nil, topic: nil)
|
34
|
+
validate(payload, schema: schema || @schema)
|
35
|
+
encode_payload(payload, schema: schema || @schema, topic: topic)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Decode a payload with a schema. Public method.
|
39
|
+
# @param payload [String]
|
40
|
+
# @param schema [Symbol|String]
|
41
|
+
# @return [Hash]
|
42
|
+
def decode(payload, schema: nil)
|
43
|
+
decode_payload(payload, schema: schema || @schema)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Given a hash, coerce its types to our schema. To be defined by subclass.
|
47
|
+
# @param payload [Hash]
|
48
|
+
# @return [Hash]
|
49
|
+
def coerce(payload)
|
50
|
+
result = {}
|
51
|
+
self.schema_fields.each do |field|
|
52
|
+
name = field.name
|
53
|
+
next unless payload.key?(name)
|
54
|
+
|
55
|
+
val = payload[name]
|
56
|
+
result[name] = coerce_field(field, val)
|
57
|
+
end
|
58
|
+
result.with_indifferent_access
|
59
|
+
end
|
60
|
+
|
61
|
+
# Indicate a class which should act as a mocked version of this backend.
|
62
|
+
# This class should perform all validations but not actually do any
|
63
|
+
# encoding.
|
64
|
+
# Note that the "mock" version (e.g. avro_validation) should return
|
65
|
+
# its own symbol when this is called, since it may be called multiple
|
66
|
+
# times depending on the order of RSpec helpers.
|
67
|
+
# @return [Symbol]
|
68
|
+
def self.mock_backend
|
69
|
+
:mock
|
70
|
+
end
|
71
|
+
|
72
|
+
# Encode a payload. To be defined by subclass.
|
73
|
+
# @param payload [Hash]
|
74
|
+
# @param schema [Symbol|String]
|
75
|
+
# @param topic [String]
|
76
|
+
# @return [String]
|
77
|
+
def encode_payload(_payload, schema:, topic: nil)
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
80
|
+
|
81
|
+
# Decode a payload. To be defined by subclass.
|
82
|
+
# @param payload [String]
|
83
|
+
# @param schema [String|Symbol]
|
84
|
+
# @return [Hash]
|
85
|
+
def decode_payload(_payload, schema:)
|
86
|
+
raise NotImplementedError
|
87
|
+
end
|
88
|
+
|
89
|
+
# Validate that a payload matches the schema. To be defined by subclass.
|
90
|
+
# @param payload [Hash]
|
91
|
+
# @param schema [String|Symbol]
|
92
|
+
def validate(_payload, schema:)
|
93
|
+
raise NotImplementedError
|
94
|
+
end
|
95
|
+
|
96
|
+
# List of field names belonging to the schema. To be defined by subclass.
|
97
|
+
# @return [Array<SchemaField>]
|
98
|
+
def schema_fields
|
99
|
+
raise NotImplementedError
|
100
|
+
end
|
101
|
+
|
102
|
+
# Given a value and a field definition (as defined by whatever the
|
103
|
+
# underlying schema library is), coerce the given value to
|
104
|
+
# the given field type.
|
105
|
+
# @param field [SchemaField]
|
106
|
+
# @param value [Object]
|
107
|
+
# @return [Object]
|
108
|
+
def coerce_field(_field, _value)
|
109
|
+
raise NotImplementedError
|
110
|
+
end
|
111
|
+
|
112
|
+
# Encode a message key. To be defined by subclass.
|
113
|
+
# @param key [String|Hash] the value to use as the key.
|
114
|
+
# @param key_id [Symbol|String] the field name of the key.
|
115
|
+
# @param topic [String]
|
116
|
+
# @return [String]
|
117
|
+
def encode_key(_key, _key_id, topic: nil)
|
118
|
+
raise NotImplementedError
|
119
|
+
end
|
120
|
+
|
121
|
+
# Decode a message key. To be defined by subclass.
|
122
|
+
# @param payload [Hash] the message itself.
|
123
|
+
# @param key_id [Symbol|String] the field in the message to decode.
|
124
|
+
# @return [String]
|
125
|
+
def decode_key(_payload, _key_id)
|
126
|
+
raise NotImplementedError
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
module SchemaBackends
|
5
|
+
# Mock implementation of a schema backend that does no encoding or validation.
|
6
|
+
class Mock < Base
|
7
|
+
# @override
|
8
|
+
def decode_payload(payload, schema:)
|
9
|
+
payload.is_a?(String) ? 'payload-decoded' : payload.map { |k, v| [k, "decoded-#{v}"] }
|
10
|
+
end
|
11
|
+
|
12
|
+
# @override
|
13
|
+
def encode_payload(payload, schema:, topic: nil)
|
14
|
+
payload.is_a?(String) ? 'payload-encoded' : payload.map { |k, v| [k, "encoded-#{v}"] }
|
15
|
+
end
|
16
|
+
|
17
|
+
# @override
|
18
|
+
def validate(_payload, schema:)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @override
|
22
|
+
def schema_fields
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
# @override
|
27
|
+
def coerce_field(_type, value)
|
28
|
+
value
|
29
|
+
end
|
30
|
+
|
31
|
+
# @override
|
32
|
+
def encode_key(key_id, key)
|
33
|
+
{ key_id => key }
|
34
|
+
end
|
35
|
+
|
36
|
+
# @override
|
37
|
+
def decode_key(payload, key_id)
|
38
|
+
payload[key_id]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|