deimos-ruby 2.0.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9c0999bb0a5a4b9b415220ce78e60a27ee535e0a556ae20f63d8b989c2e3f6d
4
- data.tar.gz: a6ee9c3686162e45cbb33982b0ca85c79c38ff095dd2f8c670a83c0aa551cf6b
3
+ metadata.gz: 03e440f70d2ffedc89bae21e3eaef6e317c89d52a53d48d5319fcbea028d775d
4
+ data.tar.gz: 2a8737062e2404f9bbbb67d35124083c9da740a2f42c670bd0d8a776522377a8
5
5
  SHA512:
6
- metadata.gz: dd8b7220297cecbed63e753bb081f9656dbe7547207de5a5af14e7d8276de8ef9ec063490e52ca12bbba6b6c7f23ecc43703f68e44e4e1beaf843acc813cbbdc
7
- data.tar.gz: 8cd50f82af228b9518b579347d67d3c4e4faeae199f4ebd9cd7601aa4ad36b5b1f59c403ae2c5af735eae94f33c44ff69146508215cf03313568581c85013945
6
+ metadata.gz: d8beb919e34f80e9fca1b6ca5e0a378b2f8494f06e7f27b29f8bcad4028ae22f9c1747050bd64e4523a6d32e631e1c696ae917e4583859be0313817522788298
7
+ data.tar.gz: bd6754c7848d7c874e2bcf6f7e27cc65bbe5a1fce8f95fbca27538b56a52f14c8c2ac113c65185ad1cd349a2e25dbd498ea81c5af49f5bfe20cf6be136df7057
@@ -36,7 +36,7 @@ jobs:
36
36
  strategy:
37
37
  fail-fast: false
38
38
  matrix:
39
- ruby: [ '2.7', '3.0', '3.1', '3.2', '3.3' ]
39
+ ruby: [ '3.0', '3.1', '3.2', '3.3', '3.4' ]
40
40
 
41
41
  steps:
42
42
  - uses: actions/checkout@v3
data/CHANGELOG.md CHANGED
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## UNRELEASED
9
+ - Fix: Added support to handle producing union type of multiple records & data types.
10
+ - Fix: Outbox producer was trying to double-encode messages.
9
11
 
10
12
  ## 2.0.0 - 2025-03-03
11
13
  - ***Full rewrite of Deimos to work with Karafka.*** Please see [Upgrading](./docs/UPGRADING.md) for full information.
data/Gemfile CHANGED
@@ -4,3 +4,9 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in boilerplate.gemspec
6
6
  gemspec
7
+
8
+ # for older activesupport
9
+ gem 'mutex_m'
10
+ gem 'bigdecimal'
11
+ gem 'benchmark'
12
+ gem 'drb'
data/README.md CHANGED
@@ -10,8 +10,10 @@ A Ruby framework for marrying Kafka, a schema definition like Avro, and/or Activ
10
10
  a useful toolbox of goodies for Ruby-based Kafka development.
11
11
  Built on [Karafka](https://karafka.io/).
12
12
 
13
- [!IMPORTANT]
14
- Deimos 2.x is a major rewrite from 1.x. Please see the [Upgrading Guide](./docs/UPGRADING.md) for information on the changes and how to upgrade.
13
+ > [!IMPORTANT]
14
+ > Deimos 2.x is a major rewrite from 1.x. Please see the [Upgrading Guide](./docs/UPGRADING.md) for information on the changes and how to upgrade.
15
+
16
+ Version 1.0 documentation can be found [here](https://github.com/flipp-oss/deimos/tree/v1).
15
17
 
16
18
  <!--ts-->
17
19
  * [Additional Documentation](#additional-documentation)
@@ -11,6 +11,7 @@ module Deimos
11
11
  ) do
12
12
  config = Deimos.karafka_config_for(topic: message[:topic])
13
13
  return message if config.nil? || config.schema.nil?
14
+ return message if message.delete(:already_encoded)
14
15
  return if message[:payload] && !message[:payload].is_a?(Hash) && !message[:payload].is_a?(SchemaClass::Record)
15
16
 
16
17
  m = Deimos::Message.new(message[:payload].to_h,
@@ -54,7 +54,8 @@ module Deimos
54
54
  payload: self.message,
55
55
  partition_key: self.partition_key,
56
56
  key: self.key,
57
- topic: self.topic
57
+ topic: self.topic,
58
+ already_encoded: true
58
59
  }
59
60
  end
60
61
 
@@ -18,10 +18,48 @@ module Deimos
18
18
  union_types = type.schemas.map { |s| s.type.to_sym }
19
19
  return nil if val.nil? && union_types.include?(:null)
20
20
 
21
- schema_type = type.schemas.find { |s| s.type.to_sym != :null }
21
+ schema_type = find_schema_type(type, val)
22
22
  coerce_type(schema_type, val)
23
23
  end
24
24
 
25
+ # Find the right schema for val from a UnionSchema.
26
+ # @param type [Avro::Schema::UnionSchema]
27
+ # @param val [Object]
28
+ # @return [Avro::Schema::PrimitiveSchema]
29
+ def find_schema_type(type, val)
30
+ int_classes = [Time, ActiveSupport::TimeWithZone]
31
+
32
+ schema_type = type.schemas.find do |schema|
33
+ field_type = schema.type.to_sym
34
+
35
+ case field_type
36
+ when :int, :long
37
+ val.is_a?(Integer) ||
38
+ _is_integer_string?(val) ||
39
+ int_classes.any? { |klass| val.is_a?(klass) }
40
+ when :float, :double
41
+ val.is_a?(Float) || _is_float_string?(val)
42
+ when :array
43
+ val.is_a?(Array)
44
+ when :record
45
+ if val.is_a?(Hash)
46
+ schema_fields_set = Set.new(schema.fields.map(&:name))
47
+ Set.new(val.keys).subset?(schema_fields_set)
48
+ else
49
+ # If the value is not a hash, we can't coerce it to a record.
50
+ # Keep looking for another schema
51
+ false
52
+ end
53
+ else
54
+ schema.type.to_sym != :null
55
+ end
56
+ end
57
+
58
+ raise "No Schema type found for VALUE: #{val}\n TYPE: #{type}" if schema_type.nil?
59
+
60
+ schema_type
61
+ end
62
+
25
63
  # Coerce sub-records in a payload to match the schema.
26
64
  # @param type [Avro::Schema::RecordSchema]
27
65
  # @param val [Object]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Deimos
4
- VERSION = '2.0.0'
4
+ VERSION = '2.0.1'
5
5
  end
@@ -3,6 +3,7 @@
3
3
  describe Deimos::ActiveRecordProducer do
4
4
 
5
5
  include_context 'with widgets'
6
+ include_context 'with widget_with_union_types'
6
7
 
7
8
  prepend_before(:each) do
8
9
  producer_class = Class.new(Deimos::ActiveRecordProducer)
@@ -40,6 +41,12 @@ describe Deimos::ActiveRecordProducer do
40
41
  end
41
42
 
42
43
  stub_const('MyProducerWithPostProcess', producer_class)
44
+
45
+ producer_class = Class.new(Deimos::ActiveRecordProducer) do
46
+ record_class WidgetWithUnionType
47
+ end
48
+ stub_const('MyProducerWithUnionType', producer_class)
49
+
43
50
  Karafka::App.routes.redraw do
44
51
  topic 'my-topic' do
45
52
  schema 'MySchema'
@@ -71,6 +78,13 @@ describe Deimos::ActiveRecordProducer do
71
78
  key_config none: true
72
79
  producer_class MyProducerWithPostProcess
73
80
  end
81
+ topic 'my-topic-with-union-type' do
82
+ schema 'MySchemaWithUnionType'
83
+ namespace 'com.my-namespace'
84
+ key_config none: true
85
+ producer_class MyProducerWithUnionType
86
+ end
87
+
74
88
  end
75
89
 
76
90
  end
@@ -90,6 +104,98 @@ describe Deimos::ActiveRecordProducer do
90
104
  expect('my-topic').to have_sent(test_id: 'abc', some_int: 3)
91
105
  end
92
106
 
107
+ it 'should coerce values for a UnionSchema' do
108
+ MyProducerWithUnionType.send_event(WidgetWithUnionType.new(
109
+ test_id: "abc",
110
+ test_long: 399999,
111
+ test_union_type: %w(hello world)
112
+ ))
113
+
114
+ expect('my-topic-with-union-type').to have_sent(
115
+ test_id: "abc",
116
+ test_long: 399999,
117
+ test_union_type: %w(hello world)
118
+ )
119
+
120
+ MyProducerWithUnionType.send_event(WidgetWithUnionType.new(
121
+ test_id: "abc",
122
+ test_long: 399999,
123
+ test_union_type: {
124
+ record1_map:{ a:9999, b:234 },
125
+ record1_id: 567
126
+ }
127
+ ))
128
+
129
+ expect('my-topic-with-union-type').to have_sent(
130
+ test_id: "abc",
131
+ test_long: 399999,
132
+ test_union_type:{
133
+ record1_map:{ a:9999, b:234 },
134
+ record1_id: 567
135
+ }
136
+ )
137
+
138
+ MyProducerWithUnionType.send_event(WidgetWithUnionType.new(
139
+ test_id: "abc",
140
+ test_long: 399999,
141
+ test_union_type: 1010101
142
+ ))
143
+
144
+ expect('my-topic-with-union-type').to have_sent(
145
+ test_id: "abc",
146
+ test_long: 399999,
147
+ test_union_type:1010101
148
+ )
149
+
150
+ MyProducerWithUnionType.send_event(WidgetWithUnionType.new(
151
+ test_id: "abc",
152
+ test_long: 399999,
153
+ test_union_type: {
154
+ record2_id: "hello world"
155
+ }
156
+ ))
157
+
158
+ expect('my-topic-with-union-type').to have_sent(
159
+ test_id: "abc",
160
+ test_long: 399999,
161
+ test_union_type: {
162
+ record2_id: "hello world"
163
+ }
164
+ )
165
+
166
+ MyProducerWithUnionType.send_event(WidgetWithUnionType.new(
167
+ test_id: "abc",
168
+ test_long: 399999,
169
+ test_union_type: {
170
+ record3_id:10.1010
171
+ }
172
+ ))
173
+
174
+ expect('my-topic-with-union-type').to have_sent(
175
+ test_id: "abc",
176
+ test_long: 399999,
177
+ test_union_type: {
178
+ record3_id:10.1010
179
+ }
180
+ )
181
+
182
+ MyProducerWithUnionType.send_event(WidgetWithUnionType.new(
183
+ test_id: "abc",
184
+ test_long: 399999,
185
+ test_union_type: {
186
+ record4_id:101010
187
+ }
188
+ ))
189
+
190
+ expect('my-topic-with-union-type').to have_sent(
191
+ test_id: "abc",
192
+ test_long: 399999,
193
+ test_union_type: {
194
+ record4_id:101010
195
+ }
196
+ )
197
+ end
198
+
93
199
  it 'should coerce values' do
94
200
  MyProducer.send_event(Widget.new(test_id: 'abc', some_int: '3'))
95
201
  MyProducer.send_event(Widget.new(test_id: 'abc', some_int: 4.5))
@@ -109,11 +215,11 @@ describe Deimos::ActiveRecordProducer do
109
215
  widget = Widget.create!(test_id: 'abc2', some_int: 3)
110
216
  MyProducerWithID.send_event({id: widget.id, test_id: 'abc2', some_int: 3})
111
217
  expect('my-topic-with-id').to have_sent(
112
- test_id: 'abc2',
113
- some_int: 3,
114
- message_id: 'generated_id',
115
- timestamp: anything
116
- )
218
+ test_id: 'abc2',
219
+ some_int: 3,
220
+ message_id: 'generated_id',
221
+ timestamp: anything
222
+ )
117
223
  end
118
224
 
119
225
  it 'should post process the batch of records in #send_events' do
@@ -4,8 +4,17 @@ require 'generators/deimos/schema_class_generator'
4
4
  require 'fileutils'
5
5
 
6
6
  class MultiFileSerializer
7
+ def process_string(s)
8
+ # Ruby 3.4 changes how hashes are printed
9
+ if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('3.4.0')
10
+ s.gsub(/{"(.*)" => /, '{"\1"=>')
11
+ else
12
+ s
13
+ end
14
+
15
+ end
7
16
  def dump(value)
8
- value.keys.sort.map { |k| "#{k}:\n#{value[k]}\n" }.join("\n")
17
+ value.keys.sort.map { |k| "#{k}:\n#{process_string(value[k])}\n" }.join("\n")
9
18
  end
10
19
  end
11
20
 
@@ -0,0 +1,91 @@
1
+ {
2
+ "namespace": "com.my-namespace",
3
+ "name": "MySchemaWithUnionType",
4
+ "type": "record",
5
+ "doc": "Test schema",
6
+ "fields": [
7
+ {
8
+ "name": "test_id",
9
+ "type": "string",
10
+ "default": ""
11
+ },
12
+ {
13
+ "name": "test_long",
14
+ "type": [
15
+ "null",
16
+ "long"
17
+ ],
18
+ "default": null
19
+ },
20
+ {
21
+ "name": "test_union_type",
22
+ "type": [
23
+ "null",
24
+ {
25
+ "type": "record",
26
+ "name": "Record1",
27
+ "namespace": "com.flipp.content",
28
+ "fields": [
29
+ {
30
+ "name": "record1_map",
31
+ "type": {
32
+ "type": "map",
33
+ "values": "long"
34
+ },
35
+ "default": {}
36
+ },
37
+ {
38
+ "name": "record1_id",
39
+ "type": "int",
40
+ "default": 0
41
+ }
42
+ ]
43
+ },
44
+ {
45
+ "type": "record",
46
+ "name": "Record2",
47
+ "namespace": "com.flipp.content",
48
+ "fields": [
49
+ {
50
+ "name": "record2_id",
51
+ "type": "string",
52
+ "default": ""
53
+ }
54
+ ]
55
+ },
56
+ {
57
+ "type": "record",
58
+ "name": "Record3",
59
+ "namespace": "com.flipp.content",
60
+ "fields": [
61
+ {
62
+ "name": "record3_id",
63
+ "type": "float",
64
+ "default": 0.0
65
+ }
66
+ ]
67
+ },
68
+ {
69
+ "type": "record",
70
+ "name": "Record4",
71
+ "namespace": "com.flipp.content",
72
+ "fields": [
73
+ {
74
+ "name": "record4_id",
75
+ "type": "int",
76
+ "default": 0
77
+ }
78
+ ]
79
+ },
80
+ "int",
81
+ {
82
+ "name": "test_array_of_strings",
83
+ "type": "array",
84
+ "default": [],
85
+ "items":"string"
86
+ }
87
+ ],
88
+ "default": null
89
+ }
90
+ ]
91
+ }
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is autogenerated by Deimos, Do NOT modify
4
+ module Schemas
5
+ ### Primary Schema Class ###
6
+ # Autogenerated Schema for Record at com.my-namespace.MySchemaWithUnionType
7
+ class MySchemaWithUnionType < Deimos::SchemaClass::Record
8
+
9
+ ### Secondary Schema Classes ###
10
+ # Autogenerated Schema for Record at com.flipp.content.Record1
11
+ class Record1 < Deimos::SchemaClass::Record
12
+
13
+ ### Attribute Accessors ###
14
+ # @return [Hash<String, Integer>]
15
+ attr_accessor :record1_map
16
+ # @return [Integer]
17
+ attr_accessor :record1_id
18
+
19
+ # @override
20
+ def initialize(record1_map: {},
21
+ record1_id: 0)
22
+ super
23
+ self.record1_map = record1_map
24
+ self.record1_id = record1_id
25
+ end
26
+
27
+ # @override
28
+ def schema
29
+ 'Record1'
30
+ end
31
+
32
+ # @override
33
+ def namespace
34
+ 'com.flipp.content'
35
+ end
36
+
37
+ # @override
38
+ def as_json(_opts={})
39
+ {
40
+ 'record1_map' => @record1_map,
41
+ 'record1_id' => @record1_id
42
+ }
43
+ end
44
+ end
45
+
46
+ # Autogenerated Schema for Record at com.flipp.content.Record2
47
+ class Record2 < Deimos::SchemaClass::Record
48
+
49
+ ### Attribute Accessors ###
50
+ # @return [String]
51
+ attr_accessor :record2_id
52
+
53
+ # @override
54
+ def initialize(record2_id: "")
55
+ super
56
+ self.record2_id = record2_id
57
+ end
58
+
59
+ # @override
60
+ def schema
61
+ 'Record2'
62
+ end
63
+
64
+ # @override
65
+ def namespace
66
+ 'com.flipp.content'
67
+ end
68
+
69
+ # @override
70
+ def as_json(_opts={})
71
+ {
72
+ 'record2_id' => @record2_id
73
+ }
74
+ end
75
+ end
76
+
77
+ # Autogenerated Schema for Record at com.flipp.content.Record3
78
+ class Record3 < Deimos::SchemaClass::Record
79
+
80
+ ### Attribute Accessors ###
81
+ # @return [Float]
82
+ attr_accessor :record3_id
83
+
84
+ # @override
85
+ def initialize(record3_id: 0.0)
86
+ super
87
+ self.record3_id = record3_id
88
+ end
89
+
90
+ # @override
91
+ def schema
92
+ 'Record3'
93
+ end
94
+
95
+ # @override
96
+ def namespace
97
+ 'com.flipp.content'
98
+ end
99
+
100
+ # @override
101
+ def as_json(_opts={})
102
+ {
103
+ 'record3_id' => @record3_id
104
+ }
105
+ end
106
+ end
107
+
108
+ # Autogenerated Schema for Record at com.flipp.content.Record4
109
+ class Record4 < Deimos::SchemaClass::Record
110
+
111
+ ### Attribute Accessors ###
112
+ # @return [Integer]
113
+ attr_accessor :record4_id
114
+
115
+ # @override
116
+ def initialize(record4_id: 0)
117
+ super
118
+ self.record4_id = record4_id
119
+ end
120
+
121
+ # @override
122
+ def schema
123
+ 'Record4'
124
+ end
125
+
126
+ # @override
127
+ def namespace
128
+ 'com.flipp.content'
129
+ end
130
+
131
+ # @override
132
+ def as_json(_opts={})
133
+ {
134
+ 'record4_id' => @record4_id
135
+ }
136
+ end
137
+ end
138
+
139
+
140
+ ### Attribute Readers ###
141
+ # @return [nil, Record1, Record2, Record3, Record4, Integer, Array<String>]
142
+ attr_reader :test_union_type
143
+
144
+ ### Attribute Accessors ###
145
+ # @return [String]
146
+ attr_accessor :test_id
147
+ # @return [nil, Integer]
148
+ attr_accessor :test_long
149
+
150
+ ### Attribute Writers ###
151
+ # @return [nil, Record1, Record2, Record3, Record4, Integer, Array<String>]
152
+ def test_union_type=(value)
153
+ @test_union_type = Record1.initialize_from_value(value)
154
+ end
155
+
156
+ # @override
157
+ def initialize(test_id: "",
158
+ test_long: nil,
159
+ test_union_type: nil)
160
+ super
161
+ self.test_id = test_id
162
+ self.test_long = test_long
163
+ self.test_union_type = test_union_type
164
+ end
165
+
166
+ # @override
167
+ def schema
168
+ 'MySchemaWithUnionType'
169
+ end
170
+
171
+ # @override
172
+ def namespace
173
+ 'com.my-namespace'
174
+ end
175
+
176
+ # @override
177
+ def as_json(_opts={})
178
+ {
179
+ 'test_id' => @test_id,
180
+ 'test_long' => @test_long,
181
+ 'test_union_type' => @test_union_type&.as_json
182
+ }
183
+ end
184
+ end
185
+ end