deimos-ruby 1.4.0.pre.beta7 → 1.5.0.pre.beta2
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/.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
|