deimos-temp-fork 0.0.1
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 +7 -0
- data/.circleci/config.yml +83 -0
- data/.gitignore +41 -0
- data/.gitmodules +0 -0
- data/.rspec +1 -0
- data/.rubocop.yml +333 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +349 -0
- data/CODE_OF_CONDUCT.md +77 -0
- data/Dockerfile +23 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +286 -0
- data/Guardfile +22 -0
- data/LICENSE.md +195 -0
- data/README.md +1099 -0
- data/Rakefile +13 -0
- data/bin/deimos +4 -0
- data/deimos-ruby.gemspec +44 -0
- data/docker-compose.yml +71 -0
- data/docs/ARCHITECTURE.md +140 -0
- data/docs/CONFIGURATION.md +236 -0
- data/docs/DATABASE_BACKEND.md +147 -0
- data/docs/INTEGRATION_TESTS.md +52 -0
- data/docs/PULL_REQUEST_TEMPLATE.md +35 -0
- data/docs/UPGRADING.md +128 -0
- data/lib/deimos-temp-fork.rb +95 -0
- data/lib/deimos/active_record_consume/batch_consumption.rb +164 -0
- data/lib/deimos/active_record_consume/batch_slicer.rb +27 -0
- data/lib/deimos/active_record_consume/message_consumption.rb +79 -0
- data/lib/deimos/active_record_consume/schema_model_converter.rb +52 -0
- data/lib/deimos/active_record_consumer.rb +67 -0
- data/lib/deimos/active_record_producer.rb +87 -0
- data/lib/deimos/backends/base.rb +32 -0
- data/lib/deimos/backends/db.rb +41 -0
- data/lib/deimos/backends/kafka.rb +33 -0
- data/lib/deimos/backends/kafka_async.rb +33 -0
- data/lib/deimos/backends/test.rb +20 -0
- data/lib/deimos/batch_consumer.rb +7 -0
- data/lib/deimos/config/configuration.rb +381 -0
- data/lib/deimos/config/phobos_config.rb +137 -0
- data/lib/deimos/consume/batch_consumption.rb +150 -0
- data/lib/deimos/consume/message_consumption.rb +94 -0
- data/lib/deimos/consumer.rb +104 -0
- data/lib/deimos/instrumentation.rb +76 -0
- data/lib/deimos/kafka_message.rb +60 -0
- data/lib/deimos/kafka_source.rb +128 -0
- data/lib/deimos/kafka_topic_info.rb +102 -0
- data/lib/deimos/message.rb +79 -0
- data/lib/deimos/metrics/datadog.rb +47 -0
- data/lib/deimos/metrics/mock.rb +39 -0
- data/lib/deimos/metrics/provider.rb +36 -0
- data/lib/deimos/monkey_patches/phobos_cli.rb +35 -0
- data/lib/deimos/monkey_patches/phobos_producer.rb +51 -0
- data/lib/deimos/poll_info.rb +9 -0
- data/lib/deimos/producer.rb +224 -0
- data/lib/deimos/railtie.rb +8 -0
- data/lib/deimos/schema_backends/avro_base.rb +140 -0
- data/lib/deimos/schema_backends/avro_local.rb +30 -0
- data/lib/deimos/schema_backends/avro_schema_coercer.rb +119 -0
- 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 +150 -0
- data/lib/deimos/schema_backends/mock.rb +42 -0
- data/lib/deimos/shared_config.rb +63 -0
- data/lib/deimos/test_helpers.rb +360 -0
- data/lib/deimos/tracing/datadog.rb +35 -0
- data/lib/deimos/tracing/mock.rb +40 -0
- data/lib/deimos/tracing/provider.rb +29 -0
- data/lib/deimos/utils/db_poller.rb +150 -0
- data/lib/deimos/utils/db_producer.rb +243 -0
- data/lib/deimos/utils/deadlock_retry.rb +68 -0
- data/lib/deimos/utils/inline_consumer.rb +150 -0
- data/lib/deimos/utils/lag_reporter.rb +175 -0
- data/lib/deimos/utils/schema_controller_mixin.rb +115 -0
- data/lib/deimos/version.rb +5 -0
- data/lib/generators/deimos/active_record/templates/migration.rb.tt +28 -0
- data/lib/generators/deimos/active_record/templates/model.rb.tt +5 -0
- data/lib/generators/deimos/active_record_generator.rb +79 -0
- data/lib/generators/deimos/db_backend/templates/migration +25 -0
- data/lib/generators/deimos/db_backend/templates/rails3_migration +31 -0
- data/lib/generators/deimos/db_backend_generator.rb +48 -0
- data/lib/generators/deimos/db_poller/templates/migration +11 -0
- data/lib/generators/deimos/db_poller/templates/rails3_migration +16 -0
- data/lib/generators/deimos/db_poller_generator.rb +48 -0
- data/lib/tasks/deimos.rake +34 -0
- data/spec/active_record_batch_consumer_spec.rb +481 -0
- data/spec/active_record_consume/batch_slicer_spec.rb +42 -0
- data/spec/active_record_consume/schema_model_converter_spec.rb +105 -0
- data/spec/active_record_consumer_spec.rb +154 -0
- data/spec/active_record_producer_spec.rb +85 -0
- data/spec/backends/base_spec.rb +10 -0
- data/spec/backends/db_spec.rb +54 -0
- data/spec/backends/kafka_async_spec.rb +11 -0
- data/spec/backends/kafka_spec.rb +11 -0
- data/spec/batch_consumer_spec.rb +256 -0
- data/spec/config/configuration_spec.rb +248 -0
- data/spec/consumer_spec.rb +209 -0
- data/spec/deimos_spec.rb +169 -0
- data/spec/generators/active_record_generator_spec.rb +56 -0
- data/spec/handlers/my_batch_consumer.rb +10 -0
- data/spec/handlers/my_consumer.rb +10 -0
- data/spec/kafka_listener_spec.rb +55 -0
- data/spec/kafka_source_spec.rb +381 -0
- data/spec/kafka_topic_info_spec.rb +111 -0
- data/spec/message_spec.rb +19 -0
- data/spec/phobos.bad_db.yml +73 -0
- data/spec/phobos.yml +77 -0
- data/spec/producer_spec.rb +498 -0
- data/spec/rake_spec.rb +19 -0
- data/spec/schema_backends/avro_base_shared.rb +199 -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 +33 -0
- data/spec/schemas/com/my-namespace/Generated.avsc +71 -0
- data/spec/schemas/com/my-namespace/MyNestedSchema.avsc +62 -0
- data/spec/schemas/com/my-namespace/MySchema-key.avsc +13 -0
- data/spec/schemas/com/my-namespace/MySchema.avsc +18 -0
- data/spec/schemas/com/my-namespace/MySchemaCompound-key.avsc +18 -0
- data/spec/schemas/com/my-namespace/MySchemaWithBooleans.avsc +18 -0
- data/spec/schemas/com/my-namespace/MySchemaWithDateTimes.avsc +33 -0
- data/spec/schemas/com/my-namespace/MySchemaWithId.avsc +28 -0
- data/spec/schemas/com/my-namespace/MySchemaWithUniqueId.avsc +32 -0
- data/spec/schemas/com/my-namespace/Wibble.avsc +43 -0
- data/spec/schemas/com/my-namespace/Widget.avsc +27 -0
- data/spec/schemas/com/my-namespace/WidgetTheSecond.avsc +27 -0
- data/spec/schemas/com/my-namespace/request/CreateTopic.avsc +11 -0
- data/spec/schemas/com/my-namespace/request/Index.avsc +11 -0
- data/spec/schemas/com/my-namespace/request/UpdateRequest.avsc +11 -0
- data/spec/schemas/com/my-namespace/response/CreateTopic.avsc +11 -0
- data/spec/schemas/com/my-namespace/response/Index.avsc +11 -0
- data/spec/schemas/com/my-namespace/response/UpdateResponse.avsc +11 -0
- data/spec/spec_helper.rb +267 -0
- data/spec/utils/db_poller_spec.rb +320 -0
- data/spec/utils/db_producer_spec.rb +514 -0
- data/spec/utils/deadlock_retry_spec.rb +74 -0
- data/spec/utils/inline_consumer_spec.rb +31 -0
- data/spec/utils/lag_reporter_spec.rb +76 -0
- data/spec/utils/platform_schema_validation_spec.rb +0 -0
- data/spec/utils/schema_controller_mixin_spec.rb +84 -0
- data/support/deimos-solo.png +0 -0
- data/support/deimos-with-name-next.png +0 -0
- data/support/deimos-with-name.png +0 -0
- data/support/flipp-logo.png +0 -0
- metadata +551 -0
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
# :nodoc:
|
|
37
|
+
def sql_type(field)
|
|
38
|
+
type = field.type.type
|
|
39
|
+
return type if %w(array map record).include?(type)
|
|
40
|
+
|
|
41
|
+
if type == :union
|
|
42
|
+
non_null = field.type.schemas.reject { |f| f.type == :null }
|
|
43
|
+
if non_null.size > 1
|
|
44
|
+
warn("WARNING: #{field.name} has more than one non-null type. Picking the first for the SQL type.")
|
|
45
|
+
end
|
|
46
|
+
return non_null.first.type
|
|
47
|
+
end
|
|
48
|
+
return type.to_sym if %w(float boolean).include?(type)
|
|
49
|
+
return :integer if type == 'int'
|
|
50
|
+
return :bigint if type == 'long'
|
|
51
|
+
|
|
52
|
+
if type == 'double'
|
|
53
|
+
warn('Avro `double` type turns into SQL `float` type. Please ensure you have the correct `limit` set.')
|
|
54
|
+
return :float
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
:string
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @override
|
|
61
|
+
def coerce_field(field, value)
|
|
62
|
+
AvroSchemaCoercer.new(avro_schema).coerce_type(field.type, value)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @override
|
|
66
|
+
def schema_fields
|
|
67
|
+
avro_schema.fields.map do |field|
|
|
68
|
+
enum_values = field.type.type == 'enum' ? field.type.symbols : []
|
|
69
|
+
SchemaField.new(field.name, field.type, enum_values)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @override
|
|
74
|
+
def validate(payload, schema:)
|
|
75
|
+
Avro::SchemaValidator.validate!(avro_schema(schema), payload,
|
|
76
|
+
recursive: true,
|
|
77
|
+
fail_on_extra_fields: true)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @override
|
|
81
|
+
def self.mock_backend
|
|
82
|
+
:avro_validation
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @override
|
|
86
|
+
def self.content_type
|
|
87
|
+
'avro/binary'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# @param schema [String]
|
|
93
|
+
# @return [Avro::Schema]
|
|
94
|
+
def avro_schema(schema=nil)
|
|
95
|
+
schema ||= @schema
|
|
96
|
+
@schema_store.find(schema, @namespace)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Generate a key schema from the given value schema and key ID. This
|
|
100
|
+
# is used when encoding or decoding keys from an existing value schema.
|
|
101
|
+
# @param key_id [Symbol]
|
|
102
|
+
# @return [Hash]
|
|
103
|
+
def _generate_key_schema(key_id)
|
|
104
|
+
key_field = avro_schema.fields.find { |f| f.name == key_id.to_s }
|
|
105
|
+
name = _key_schema_name(@schema)
|
|
106
|
+
key_schema = {
|
|
107
|
+
'type' => 'record',
|
|
108
|
+
'name' => name,
|
|
109
|
+
'namespace' => @namespace,
|
|
110
|
+
'doc' => "Key for #{@namespace}.#{@schema} - autogenerated by Deimos",
|
|
111
|
+
'fields' => [
|
|
112
|
+
{
|
|
113
|
+
'name' => key_id,
|
|
114
|
+
'type' => key_field.type.type_sym.to_s
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
@schema_store.add_schema(key_schema)
|
|
119
|
+
key_schema
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @param value_schema [Hash]
|
|
123
|
+
# @return [String]
|
|
124
|
+
def _field_name_from_schema(value_schema)
|
|
125
|
+
raise "Schema #{@schema} not found!" if value_schema.nil?
|
|
126
|
+
if value_schema['fields'].nil? || value_schema['fields'].empty?
|
|
127
|
+
raise "Schema #{@schema} has no fields!"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
value_schema['fields'][0]['name']
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @param schema [String]
|
|
134
|
+
# @return [String]
|
|
135
|
+
def _key_schema_name(schema)
|
|
136
|
+
"#{schema}_key"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
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
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/time'
|
|
4
|
+
|
|
5
|
+
module Deimos
|
|
6
|
+
# Class to coerce values in a payload to match a schema.
|
|
7
|
+
class AvroSchemaCoercer
|
|
8
|
+
# @param schema [Avro::Schema]
|
|
9
|
+
def initialize(schema)
|
|
10
|
+
@schema = schema
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Coerce sub-records in a payload to match the schema.
|
|
14
|
+
# @param type [Avro::Schema::UnionSchema]
|
|
15
|
+
# @param val [Object]
|
|
16
|
+
# @return [Object]
|
|
17
|
+
def coerce_union(type, val)
|
|
18
|
+
union_types = type.schemas.map { |s| s.type.to_sym }
|
|
19
|
+
return nil if val.nil? && union_types.include?(:null)
|
|
20
|
+
|
|
21
|
+
schema_type = type.schemas.find { |s| s.type.to_sym != :null }
|
|
22
|
+
coerce_type(schema_type, val)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Coerce sub-records in a payload to match the schema.
|
|
26
|
+
# @param type [Avro::Schema::RecordSchema]
|
|
27
|
+
# @param val [Object]
|
|
28
|
+
# @return [Object]
|
|
29
|
+
def coerce_record(type, val)
|
|
30
|
+
record = val.map do |name, value|
|
|
31
|
+
field = type.fields.find { |f| f.name == name }
|
|
32
|
+
coerce_type(field.type, value)
|
|
33
|
+
end
|
|
34
|
+
val.keys.zip(record).to_h
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Coerce values in a payload to match the schema.
|
|
38
|
+
# @param type [Avro::Schema]
|
|
39
|
+
# @param val [Object]
|
|
40
|
+
# @return [Object]
|
|
41
|
+
def coerce_type(type, val)
|
|
42
|
+
int_classes = [Time, ActiveSupport::TimeWithZone]
|
|
43
|
+
field_type = type.type.to_sym
|
|
44
|
+
|
|
45
|
+
case field_type
|
|
46
|
+
when :int, :long
|
|
47
|
+
if %w(timestamp-millis timestamp-micros).include?(type.logical_type)
|
|
48
|
+
val
|
|
49
|
+
elsif val.is_a?(Integer) ||
|
|
50
|
+
_is_integer_string?(val) ||
|
|
51
|
+
int_classes.any? { |klass| val.is_a?(klass) }
|
|
52
|
+
val.to_i
|
|
53
|
+
else
|
|
54
|
+
val # this will fail
|
|
55
|
+
end
|
|
56
|
+
when :float, :double
|
|
57
|
+
if val.is_a?(Numeric) || _is_float_string?(val)
|
|
58
|
+
val.to_f
|
|
59
|
+
else
|
|
60
|
+
val # this will fail
|
|
61
|
+
end
|
|
62
|
+
when :string
|
|
63
|
+
if val.respond_to?(:to_str)
|
|
64
|
+
val.to_s
|
|
65
|
+
elsif _is_to_s_defined?(val)
|
|
66
|
+
val.to_s
|
|
67
|
+
else
|
|
68
|
+
val # this will fail
|
|
69
|
+
end
|
|
70
|
+
when :boolean
|
|
71
|
+
if val.nil? || val == false
|
|
72
|
+
false
|
|
73
|
+
else
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
when :union
|
|
77
|
+
coerce_union(type, val)
|
|
78
|
+
when :record
|
|
79
|
+
coerce_record(type, val)
|
|
80
|
+
else
|
|
81
|
+
val
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# @param val [String]
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def _is_integer_string?(val)
|
|
90
|
+
return false unless val.is_a?(String)
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
true if Integer(val)
|
|
94
|
+
rescue StandardError
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @param val [String]
|
|
100
|
+
# @return [Boolean]
|
|
101
|
+
def _is_float_string?(val)
|
|
102
|
+
return false unless val.is_a?(String)
|
|
103
|
+
|
|
104
|
+
begin
|
|
105
|
+
true if Float(val)
|
|
106
|
+
rescue StandardError
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param val [Object]
|
|
112
|
+
# @return [Boolean]
|
|
113
|
+
def _is_to_s_defined?(val)
|
|
114
|
+
return false if val.nil?
|
|
115
|
+
|
|
116
|
+
Object.instance_method(:to_s).bind(val).call != val.to_s
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
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 || schema)
|
|
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,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deimos
|
|
4
|
+
# Represents a field in the schema.
|
|
5
|
+
class SchemaField
|
|
6
|
+
attr_accessor :name, :type, :enum_values
|
|
7
|
+
|
|
8
|
+
# @param name [String]
|
|
9
|
+
# @param type [Object]
|
|
10
|
+
# @param enum_values [Array<String>]
|
|
11
|
+
def initialize(name, type, enum_values=[])
|
|
12
|
+
@name = name
|
|
13
|
+
@type = type
|
|
14
|
+
@enum_values = enum_values
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module SchemaBackends
|
|
19
|
+
# Base class for encoding / decoding.
|
|
20
|
+
class Base
|
|
21
|
+
attr_accessor :schema, :namespace, :key_schema
|
|
22
|
+
|
|
23
|
+
# @param schema [String|Symbol]
|
|
24
|
+
# @param namespace [String]
|
|
25
|
+
def initialize(schema:, namespace: nil)
|
|
26
|
+
@schema = schema
|
|
27
|
+
@namespace = namespace
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Encode a payload with a schema. Public method.
|
|
31
|
+
# @param payload [Hash]
|
|
32
|
+
# @param schema [Symbol|String]
|
|
33
|
+
# @param topic [String]
|
|
34
|
+
# @return [String]
|
|
35
|
+
def encode(payload, schema: nil, topic: nil)
|
|
36
|
+
validate(payload, schema: schema || @schema)
|
|
37
|
+
encode_payload(payload, schema: schema || @schema, topic: topic)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Decode a payload with a schema. Public method.
|
|
41
|
+
# @param payload [String]
|
|
42
|
+
# @param schema [Symbol|String]
|
|
43
|
+
# @return [Hash,nil]
|
|
44
|
+
def decode(payload, schema: nil)
|
|
45
|
+
return nil if payload.nil?
|
|
46
|
+
decode_payload(payload, schema: schema || @schema)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Given a hash, coerce its types to our schema. To be defined by subclass.
|
|
50
|
+
# @param payload [Hash]
|
|
51
|
+
# @return [Hash]
|
|
52
|
+
def coerce(payload)
|
|
53
|
+
result = {}
|
|
54
|
+
self.schema_fields.each do |field|
|
|
55
|
+
name = field.name
|
|
56
|
+
next unless payload.key?(name)
|
|
57
|
+
|
|
58
|
+
val = payload[name]
|
|
59
|
+
result[name] = coerce_field(field, val)
|
|
60
|
+
end
|
|
61
|
+
result.with_indifferent_access
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Indicate a class which should act as a mocked version of this backend.
|
|
65
|
+
# This class should perform all validations but not actually do any
|
|
66
|
+
# encoding.
|
|
67
|
+
# Note that the "mock" version (e.g. avro_validation) should return
|
|
68
|
+
# its own symbol when this is called, since it may be called multiple
|
|
69
|
+
# times depending on the order of RSpec helpers.
|
|
70
|
+
# @return [Symbol]
|
|
71
|
+
def self.mock_backend
|
|
72
|
+
:mock
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The content type to use when encoding / decoding requests over HTTP via ActionController.
|
|
76
|
+
# @return [String]
|
|
77
|
+
def self.content_type
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Encode a payload. To be defined by subclass.
|
|
82
|
+
# @param payload [Hash]
|
|
83
|
+
# @param schema [Symbol|String]
|
|
84
|
+
# @param topic [String]
|
|
85
|
+
# @return [String]
|
|
86
|
+
def encode_payload(_payload, schema:, topic: nil)
|
|
87
|
+
raise NotImplementedError
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Decode a payload. To be defined by subclass.
|
|
91
|
+
# @param payload [String]
|
|
92
|
+
# @param schema [String|Symbol]
|
|
93
|
+
# @return [Hash]
|
|
94
|
+
def decode_payload(_payload, schema:)
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Validate that a payload matches the schema. To be defined by subclass.
|
|
99
|
+
# @param payload [Hash]
|
|
100
|
+
# @param schema [String|Symbol]
|
|
101
|
+
def validate(_payload, schema:)
|
|
102
|
+
raise NotImplementedError
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# List of field names belonging to the schema. To be defined by subclass.
|
|
106
|
+
# @return [Array<SchemaField>]
|
|
107
|
+
def schema_fields
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Given a value and a field definition (as defined by whatever the
|
|
112
|
+
# underlying schema library is), coerce the given value to
|
|
113
|
+
# the given field type.
|
|
114
|
+
# @param field [SchemaField]
|
|
115
|
+
# @param value [Object]
|
|
116
|
+
# @return [Object]
|
|
117
|
+
def coerce_field(_field, _value)
|
|
118
|
+
raise NotImplementedError
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Given a field definition, return the SQL type that might be used in
|
|
122
|
+
# ActiveRecord table creation - e.g. for Avro, a `long` type would
|
|
123
|
+
# return `:bigint`. There are also special values that need to be returned:
|
|
124
|
+
# `:array`, `:map` and `:record`, for types representing those structures.
|
|
125
|
+
# `:enum` is also recognized.
|
|
126
|
+
# @param field [SchemaField]
|
|
127
|
+
# @return [Symbol]
|
|
128
|
+
def sql_type(field)
|
|
129
|
+
raise NotImplementedError
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Encode a message key. To be defined by subclass.
|
|
133
|
+
# @param key [String|Hash] the value to use as the key.
|
|
134
|
+
# @param key_id [Symbol|String] the field name of the key.
|
|
135
|
+
# @param topic [String]
|
|
136
|
+
# @return [String]
|
|
137
|
+
def encode_key(_key, _key_id, topic: nil)
|
|
138
|
+
raise NotImplementedError
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Decode a message key. To be defined by subclass.
|
|
142
|
+
# @param payload [Hash] the message itself.
|
|
143
|
+
# @param key_id [Symbol|String] the field in the message to decode.
|
|
144
|
+
# @return [String]
|
|
145
|
+
def decode_key(_payload, _key_id)
|
|
146
|
+
raise NotImplementedError
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|