deimos-ruby 1.8.0.pre.beta1 → 1.8.1.pre.beta4
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 +8 -4
- data/CHANGELOG.md +42 -0
- data/Gemfile.lock +101 -73
- data/README.md +78 -1
- data/deimos-ruby.gemspec +2 -2
- data/lib/deimos.rb +4 -3
- data/lib/deimos/consume/batch_consumption.rb +2 -0
- data/lib/deimos/consume/message_consumption.rb +1 -0
- data/lib/deimos/instrumentation.rb +10 -5
- data/lib/deimos/kafka_topic_info.rb +21 -2
- data/lib/deimos/schema_backends/avro_base.rb +33 -1
- data/lib/deimos/schema_backends/avro_schema_coercer.rb +30 -9
- data/lib/deimos/schema_backends/base.rb +21 -2
- data/lib/deimos/utils/db_producer.rb +57 -19
- data/lib/deimos/utils/schema_controller_mixin.rb +111 -0
- data/lib/deimos/version.rb +1 -1
- 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 +1 -0
- data/lib/generators/deimos/db_backend/templates/rails3_migration +1 -0
- data/spec/batch_consumer_spec.rb +1 -0
- data/spec/generators/active_record_generator_spec.rb +56 -0
- data/spec/kafka_listener_spec.rb +54 -0
- data/spec/kafka_topic_info_spec.rb +39 -16
- data/spec/producer_spec.rb +36 -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/request/Index.avsc +11 -0
- data/spec/schemas/com/my-namespace/request/UpdateRequest.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 +7 -0
- data/spec/utils/db_producer_spec.rb +84 -10
- data/spec/utils/schema_controller_mixin_spec.rb +68 -0
- metadata +40 -24
data/deimos-ruby.gemspec
CHANGED
@@ -23,7 +23,6 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_runtime_dependency('ruby-kafka', '~> 0.7')
|
24
24
|
spec.add_runtime_dependency('sigurd', '0.0.1')
|
25
25
|
|
26
|
-
spec.add_development_dependency('activerecord', '~> 5.2')
|
27
26
|
spec.add_development_dependency('activerecord-import')
|
28
27
|
spec.add_development_dependency('avro', '~> 1.9')
|
29
28
|
spec.add_development_dependency('database_cleaner', '~> 1.7')
|
@@ -34,10 +33,11 @@ Gem::Specification.new do |spec|
|
|
34
33
|
spec.add_development_dependency('guard-rubocop', '~> 1')
|
35
34
|
spec.add_development_dependency('mysql2', '~> 0.5')
|
36
35
|
spec.add_development_dependency('pg', '~> 1.1')
|
37
|
-
spec.add_development_dependency('rails', '~>
|
36
|
+
spec.add_development_dependency('rails', '~> 6')
|
38
37
|
spec.add_development_dependency('rake', '~> 13')
|
39
38
|
spec.add_development_dependency('rspec', '~> 3')
|
40
39
|
spec.add_development_dependency('rspec_junit_formatter', '~>0.3')
|
40
|
+
spec.add_development_dependency('rspec-rails', '~> 4')
|
41
41
|
spec.add_development_dependency('rubocop', '~> 0.72')
|
42
42
|
spec.add_development_dependency('rubocop-rspec', '~> 1.27')
|
43
43
|
spec.add_development_dependency('sqlite3', '~> 1.3')
|
data/lib/deimos.rb
CHANGED
@@ -23,14 +23,15 @@ require 'deimos/monkey_patches/phobos_producer'
|
|
23
23
|
require 'deimos/monkey_patches/phobos_cli'
|
24
24
|
|
25
25
|
require 'deimos/railtie' if defined?(Rails)
|
26
|
+
require 'deimos/utils/schema_controller_mixin' if defined?(ActionController)
|
26
27
|
|
27
28
|
if defined?(ActiveRecord)
|
28
29
|
require 'deimos/kafka_source'
|
29
30
|
require 'deimos/kafka_topic_info'
|
30
31
|
require 'deimos/backends/db'
|
31
|
-
require 'sigurd/signal_handler
|
32
|
-
require 'sigurd/executor
|
33
|
-
require 'deimos/utils/db_producer
|
32
|
+
require 'sigurd/signal_handler'
|
33
|
+
require 'sigurd/executor'
|
34
|
+
require 'deimos/utils/db_producer'
|
34
35
|
require 'deimos/utils/db_poller'
|
35
36
|
end
|
36
37
|
|
@@ -6,6 +6,7 @@ module Deimos
|
|
6
6
|
# delivery. Payloads are decoded then consumers are invoked with arrays
|
7
7
|
# of messages to be handled at once
|
8
8
|
module BatchConsumption
|
9
|
+
extend ActiveSupport::Concern
|
9
10
|
include Phobos::BatchHandler
|
10
11
|
|
11
12
|
# :nodoc:
|
@@ -17,6 +18,7 @@ module Deimos
|
|
17
18
|
decode_key(message.key)
|
18
19
|
end
|
19
20
|
end
|
21
|
+
metadata[:first_offset] = batch.first&.offset
|
20
22
|
|
21
23
|
payloads = batch.map do |message|
|
22
24
|
message.payload ? self.class.decoder.decode(message.payload) : nil
|
@@ -46,13 +46,18 @@ module Deimos
|
|
46
46
|
|
47
47
|
messages = exception.failed_messages
|
48
48
|
messages.group_by(&:topic).each do |topic, batch|
|
49
|
-
|
49
|
+
producer = Deimos::Producer.descendants.find { |c| c.topic == topic }
|
50
|
+
next if batch.empty? || !producer
|
50
51
|
|
51
|
-
|
52
|
-
|
52
|
+
decoder = Deimos.schema_backend(schema: producer.config[:schema],
|
53
|
+
namespace: producer.config[:namespace])
|
54
|
+
payloads = batch.map { |m| decoder.decode(m.value) }
|
53
55
|
|
54
|
-
Deimos.config.metrics&.
|
55
|
-
|
56
|
+
Deimos.config.metrics&.increment(
|
57
|
+
'publish_error',
|
58
|
+
tags: %W(topic:#{topic}),
|
59
|
+
by: payloads.size
|
60
|
+
)
|
56
61
|
Deimos.instrument(
|
57
62
|
'produce_error',
|
58
63
|
producer: producer,
|
@@ -13,7 +13,7 @@ module Deimos
|
|
13
13
|
def lock(topic, lock_id)
|
14
14
|
# Try to create it - it's fine if it already exists
|
15
15
|
begin
|
16
|
-
self.create(topic: topic)
|
16
|
+
self.create(topic: topic, last_processed_at: Time.zone.now)
|
17
17
|
rescue ActiveRecord::RecordNotUnique
|
18
18
|
# continue on
|
19
19
|
end
|
@@ -52,7 +52,26 @@ module Deimos
|
|
52
52
|
# @param lock_id [String]
|
53
53
|
def clear_lock(topic, lock_id)
|
54
54
|
self.where(topic: topic, locked_by: lock_id).
|
55
|
-
update_all(locked_by: nil,
|
55
|
+
update_all(locked_by: nil,
|
56
|
+
locked_at: nil,
|
57
|
+
error: false,
|
58
|
+
retries: 0,
|
59
|
+
last_processed_at: Time.zone.now)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Update all topics that aren't currently locked and have no messages
|
63
|
+
# waiting. It's OK if some messages get inserted in the middle of this
|
64
|
+
# because the point is that at least within a few milliseconds of each
|
65
|
+
# other, it wasn't locked and had no messages, meaning the topic
|
66
|
+
# was in a good state.
|
67
|
+
# @param except_topics [Array<String>] the list of topics we've just
|
68
|
+
# realized had messages in them, meaning all other topics were empty.
|
69
|
+
def ping_empty_topics(except_topics)
|
70
|
+
records = KafkaTopicInfo.where(locked_by: nil).
|
71
|
+
where('topic not in(?)', except_topics)
|
72
|
+
records.each do |info|
|
73
|
+
info.update_attribute(:last_processed_at, Time.zone.now)
|
74
|
+
end
|
56
75
|
end
|
57
76
|
|
58
77
|
# The producer calls this if it gets an error sending messages. This
|
@@ -33,6 +33,30 @@ module Deimos
|
|
33
33
|
decode(payload, schema: @key_schema['name'])[field_name]
|
34
34
|
end
|
35
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
|
+
|
36
60
|
# @override
|
37
61
|
def coerce_field(field, value)
|
38
62
|
AvroSchemaCoercer.new(avro_schema).coerce_type(field.type, value)
|
@@ -40,7 +64,10 @@ module Deimos
|
|
40
64
|
|
41
65
|
# @override
|
42
66
|
def schema_fields
|
43
|
-
avro_schema.fields.map
|
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
|
44
71
|
end
|
45
72
|
|
46
73
|
# @override
|
@@ -55,6 +82,11 @@ module Deimos
|
|
55
82
|
:avro_validation
|
56
83
|
end
|
57
84
|
|
85
|
+
# @override
|
86
|
+
def self.content_type
|
87
|
+
'avro/binary'
|
88
|
+
end
|
89
|
+
|
58
90
|
private
|
59
91
|
|
60
92
|
# @param schema [String]
|
@@ -10,18 +10,37 @@ module Deimos
|
|
10
10
|
@schema = schema
|
11
11
|
end
|
12
12
|
|
13
|
-
#
|
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]
|
14
39
|
# @param val [Object]
|
15
40
|
# @return [Object]
|
16
41
|
def coerce_type(type, val)
|
17
42
|
int_classes = [Time, ActiveSupport::TimeWithZone]
|
18
43
|
field_type = type.type.to_sym
|
19
|
-
if field_type == :union
|
20
|
-
union_types = type.schemas.map { |s| s.type.to_sym }
|
21
|
-
return nil if val.nil? && union_types.include?(:null)
|
22
|
-
|
23
|
-
field_type = union_types.find { |t| t != :null }
|
24
|
-
end
|
25
44
|
|
26
45
|
case field_type
|
27
46
|
when :int, :long
|
@@ -32,14 +51,12 @@ module Deimos
|
|
32
51
|
else
|
33
52
|
val # this will fail
|
34
53
|
end
|
35
|
-
|
36
54
|
when :float, :double
|
37
55
|
if val.is_a?(Numeric) || _is_float_string?(val)
|
38
56
|
val.to_f
|
39
57
|
else
|
40
58
|
val # this will fail
|
41
59
|
end
|
42
|
-
|
43
60
|
when :string
|
44
61
|
if val.respond_to?(:to_str)
|
45
62
|
val.to_s
|
@@ -54,6 +71,10 @@ module Deimos
|
|
54
71
|
else
|
55
72
|
true
|
56
73
|
end
|
74
|
+
when :union
|
75
|
+
coerce_union(type, val)
|
76
|
+
when :record
|
77
|
+
coerce_record(type, val)
|
57
78
|
else
|
58
79
|
val
|
59
80
|
end
|
@@ -3,13 +3,15 @@
|
|
3
3
|
module Deimos
|
4
4
|
# Represents a field in the schema.
|
5
5
|
class SchemaField
|
6
|
-
attr_accessor :name, :type
|
6
|
+
attr_accessor :name, :type, :enum_values
|
7
7
|
|
8
8
|
# @param name [String]
|
9
9
|
# @param type [Object]
|
10
|
-
|
10
|
+
# @param enum_values [Array<String>]
|
11
|
+
def initialize(name, type, enum_values=[])
|
11
12
|
@name = name
|
12
13
|
@type = type
|
14
|
+
@enum_values = enum_values
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
@@ -69,6 +71,12 @@ module Deimos
|
|
69
71
|
:mock
|
70
72
|
end
|
71
73
|
|
74
|
+
# The content type to use when encoding / decoding requests over HTTP via ActionController.
|
75
|
+
# @return [String]
|
76
|
+
def self.content_type
|
77
|
+
raise NotImplementedError
|
78
|
+
end
|
79
|
+
|
72
80
|
# Encode a payload. To be defined by subclass.
|
73
81
|
# @param payload [Hash]
|
74
82
|
# @param schema [Symbol|String]
|
@@ -109,6 +117,17 @@ module Deimos
|
|
109
117
|
raise NotImplementedError
|
110
118
|
end
|
111
119
|
|
120
|
+
# Given a field definition, return the SQL type that might be used in
|
121
|
+
# ActiveRecord table creation - e.g. for Avro, a `long` type would
|
122
|
+
# return `:bigint`. There are also special values that need to be returned:
|
123
|
+
# `:array`, `:map` and `:record`, for types representing those structures.
|
124
|
+
# `:enum` is also recognized.
|
125
|
+
# @param field [SchemaField]
|
126
|
+
# @return [Symbol]
|
127
|
+
def sql_type(field)
|
128
|
+
raise NotImplementedError
|
129
|
+
end
|
130
|
+
|
112
131
|
# Encode a message key. To be defined by subclass.
|
113
132
|
# @param key [String|Hash] the value to use as the key.
|
114
133
|
# @param key_id [Symbol|String] the field name of the key.
|
@@ -9,6 +9,8 @@ module Deimos
|
|
9
9
|
attr_accessor :id, :current_topic
|
10
10
|
|
11
11
|
BATCH_SIZE = 1000
|
12
|
+
DELETE_BATCH_SIZE = 10
|
13
|
+
MAX_DELETE_ATTEMPTS = 3
|
12
14
|
|
13
15
|
# @param logger [Logger]
|
14
16
|
def initialize(logger=Logger.new(STDOUT))
|
@@ -48,6 +50,7 @@ module Deimos
|
|
48
50
|
topics = retrieve_topics
|
49
51
|
@logger.info("Found topics: #{topics}")
|
50
52
|
topics.each(&method(:process_topic))
|
53
|
+
KafkaTopicInfo.ping_empty_topics(topics)
|
51
54
|
sleep(0.5)
|
52
55
|
end
|
53
56
|
|
@@ -87,13 +90,13 @@ module Deimos
|
|
87
90
|
begin
|
88
91
|
produce_messages(compacted_messages.map(&:phobos_message))
|
89
92
|
rescue Kafka::BufferOverflow, Kafka::MessageSizeTooLarge, Kafka::RecordListTooLarge
|
93
|
+
delete_messages(messages)
|
90
94
|
@logger.error('Message batch too large, deleting...')
|
91
95
|
@logger.error(Deimos::KafkaMessage.decoded(messages))
|
92
|
-
Deimos::KafkaMessage.where(id: messages.map(&:id)).delete_all
|
93
96
|
raise
|
94
97
|
end
|
95
98
|
end
|
96
|
-
|
99
|
+
delete_messages(messages)
|
97
100
|
Deimos.config.metrics&.increment(
|
98
101
|
'db_producer.process',
|
99
102
|
tags: %W(topic:#{@current_topic}),
|
@@ -106,6 +109,27 @@ module Deimos
|
|
106
109
|
true
|
107
110
|
end
|
108
111
|
|
112
|
+
# @param messages [Array<Deimos::KafkaMessage>]
|
113
|
+
def delete_messages(messages)
|
114
|
+
attempts = 1
|
115
|
+
begin
|
116
|
+
messages.in_groups_of(DELETE_BATCH_SIZE, false).each do |batch|
|
117
|
+
Deimos::KafkaMessage.where(topic: batch.first.topic,
|
118
|
+
id: batch.map(&:id)).
|
119
|
+
delete_all
|
120
|
+
end
|
121
|
+
rescue StandardError => e
|
122
|
+
if (e.message =~ /Lock wait/i || e.message =~ /Lost connection/i) &&
|
123
|
+
attempts <= MAX_DELETE_ATTEMPTS
|
124
|
+
attempts += 1
|
125
|
+
ActiveRecord::Base.connection.verify!
|
126
|
+
sleep(1)
|
127
|
+
retry
|
128
|
+
end
|
129
|
+
raise
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
109
133
|
# @return [Array<Deimos::KafkaMessage>]
|
110
134
|
def retrieve_messages
|
111
135
|
KafkaMessage.where(topic: @current_topic).order(:id).limit(BATCH_SIZE)
|
@@ -126,19 +150,33 @@ module Deimos
|
|
126
150
|
metrics = Deimos.config.metrics
|
127
151
|
return unless metrics
|
128
152
|
|
153
|
+
topics = KafkaTopicInfo.select(%w(topic last_processed_at))
|
129
154
|
messages = Deimos::KafkaMessage.
|
130
155
|
select('count(*) as num_messages, min(created_at) as earliest, topic').
|
131
|
-
group(:topic)
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
156
|
+
group(:topic).
|
157
|
+
index_by(&:topic)
|
158
|
+
topics.each do |record|
|
159
|
+
message_record = messages[record.topic]
|
160
|
+
# We want to record the last time we saw any activity, meaning either
|
161
|
+
# the oldest message, or the last time we processed, whichever comes
|
162
|
+
# last.
|
163
|
+
if message_record
|
164
|
+
record_earliest = record.earliest
|
165
|
+
# SQLite gives a string here
|
166
|
+
if record_earliest.is_a?(String)
|
167
|
+
record_earliest = Time.zone.parse(record_earliest)
|
168
|
+
end
|
169
|
+
|
170
|
+
earliest = [record.last_processed_at, record_earliest].max
|
171
|
+
time_diff = Time.zone.now - earliest
|
172
|
+
metrics.gauge('pending_db_messages_max_wait', time_diff,
|
173
|
+
tags: ["topic:#{record.topic}"])
|
174
|
+
else
|
175
|
+
# no messages waiting
|
176
|
+
metrics.gauge('pending_db_messages_max_wait', 0,
|
177
|
+
tags: ["topic:#{record.topic}"])
|
178
|
+
end
|
179
|
+
metrics.gauge('pending_db_messages_count', message_record&.num_messages || 0,
|
142
180
|
tags: ["topic:#{record.topic}"])
|
143
181
|
end
|
144
182
|
end
|
@@ -174,11 +212,11 @@ module Deimos
|
|
174
212
|
end
|
175
213
|
|
176
214
|
@logger.error("Got error #{e.class.name} when publishing #{batch.size} in groups of #{batch_size}, retrying...")
|
177
|
-
if batch_size < 10
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
215
|
+
batch_size = if batch_size < 10
|
216
|
+
1
|
217
|
+
else
|
218
|
+
(batch_size / 10).to_i
|
219
|
+
end
|
182
220
|
shutdown_producer
|
183
221
|
retry
|
184
222
|
end
|
@@ -187,7 +225,7 @@ module Deimos
|
|
187
225
|
# @param batch [Array<Deimos::KafkaMessage>]
|
188
226
|
# @return [Array<Deimos::KafkaMessage>]
|
189
227
|
def compact_messages(batch)
|
190
|
-
return batch
|
228
|
+
return batch if batch.first&.key.blank?
|
191
229
|
|
192
230
|
topic = batch.first.topic
|
193
231
|
return batch if config.compact_topics != :all &&
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deimos
|
4
|
+
module Utils
|
5
|
+
# Mixin to automatically decode schema-encoded payloads when given the correct content type,
|
6
|
+
# and provide the `render_schema` method to encode the payload for responses.
|
7
|
+
module SchemaControllerMixin
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
Mime::Type.register('avro/binary', :avro)
|
12
|
+
|
13
|
+
attr_accessor :payload
|
14
|
+
|
15
|
+
if respond_to?(:before_filter)
|
16
|
+
before_filter(:decode_schema, if: :schema_format?)
|
17
|
+
else
|
18
|
+
before_action(:decode_schema, if: :schema_format?)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# :nodoc:
|
23
|
+
module ClassMethods
|
24
|
+
# @return [Hash<String, Hash<Symbol, String>>]
|
25
|
+
def schema_mapping
|
26
|
+
@schema_mapping ||= {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Indicate which schemas should be assigned to actions.
|
30
|
+
# @param actions [Symbol]
|
31
|
+
# @param request [String]
|
32
|
+
# @param response [String]
|
33
|
+
def schemas(*actions, request: nil, response: nil)
|
34
|
+
actions.each do |action|
|
35
|
+
request ||= action.to_s.titleize
|
36
|
+
response ||= action.to_s.titleize
|
37
|
+
schema_mapping[action.to_s] = { request: request, response: response }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Hash<Symbol, String>]
|
42
|
+
def namespaces
|
43
|
+
@namespaces ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set the namespace for both requests and responses.
|
47
|
+
# @param name [String]
|
48
|
+
def namespace(name)
|
49
|
+
request_namespace(name)
|
50
|
+
response_namespace(name)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the namespace for requests.
|
54
|
+
# @param name [String]
|
55
|
+
def request_namespace(name)
|
56
|
+
namespaces[:request] = name
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the namespace for repsonses.
|
60
|
+
# @param name [String]
|
61
|
+
def response_namespace(name)
|
62
|
+
namespaces[:response] = name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Boolean]
|
67
|
+
def schema_format?
|
68
|
+
request.content_type == Deimos.schema_backend_class.content_type
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get the namespace from either an existing instance variable, or tease it out of the schema.
|
72
|
+
# @param type [Symbol] :request or :response
|
73
|
+
# @return [Array<String, String>] the namespace and schema.
|
74
|
+
def parse_namespace(type)
|
75
|
+
namespace = self.class.namespaces[type]
|
76
|
+
schema = self.class.schema_mapping[params['action']][type]
|
77
|
+
if schema.nil?
|
78
|
+
raise "No #{type} schema defined for #{params[:controller]}##{params[:action]}!"
|
79
|
+
end
|
80
|
+
|
81
|
+
if namespace.nil?
|
82
|
+
last_period = schema.rindex('.')
|
83
|
+
namespace, schema = schema.split(last_period)
|
84
|
+
end
|
85
|
+
if namespace.nil? || schema.nil?
|
86
|
+
raise "No request namespace defined for #{params[:controller]}##{params[:action]}!"
|
87
|
+
end
|
88
|
+
|
89
|
+
[namespace, schema]
|
90
|
+
end
|
91
|
+
|
92
|
+
# Decode the payload with the parameters.
|
93
|
+
def decode_schema
|
94
|
+
namespace, schema = parse_namespace(:request)
|
95
|
+
decoder = Deimos.schema_backend(schema: schema, namespace: namespace)
|
96
|
+
@payload = decoder.decode(request.body.read).with_indifferent_access
|
97
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Render a hash into a payload as specified by the configured schema and namespace.
|
101
|
+
# @param payload [Hash]
|
102
|
+
def render_schema(payload, schema: nil, namespace: nil)
|
103
|
+
namespace, schema = parse_namespace(:response) if !schema && !namespace
|
104
|
+
encoder = Deimos.schema_backend(schema: schema, namespace: namespace)
|
105
|
+
encoded = encoder.encode(payload)
|
106
|
+
response.headers['Content-Type'] = encoder.class.content_type
|
107
|
+
send_data(encoded)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|