cyclone_lariat 0.4.0 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/gem-push.yml +4 -4
  3. data/.rubocop.yml +9 -5
  4. data/Gemfile.lock +123 -21
  5. data/Guardfile +42 -0
  6. data/README.md +417 -220
  7. data/bin/cyclone_lariat +75 -43
  8. data/cyclone_lariat.gemspec +10 -3
  9. data/lib/cyclone_lariat/clients/abstract.rb +40 -0
  10. data/lib/cyclone_lariat/clients/sns.rb +163 -0
  11. data/lib/cyclone_lariat/clients/sqs.rb +114 -0
  12. data/lib/cyclone_lariat/core.rb +21 -0
  13. data/lib/cyclone_lariat/errors.rb +16 -0
  14. data/lib/cyclone_lariat/fake.rb +19 -0
  15. data/lib/cyclone_lariat/generators/command.rb +53 -0
  16. data/lib/cyclone_lariat/generators/event.rb +52 -0
  17. data/lib/cyclone_lariat/generators/queue.rb +30 -0
  18. data/lib/cyclone_lariat/generators/topic.rb +29 -0
  19. data/lib/cyclone_lariat/messages/v1/abstract.rb +139 -0
  20. data/lib/cyclone_lariat/messages/v1/command.rb +20 -0
  21. data/lib/cyclone_lariat/messages/v1/event.rb +20 -0
  22. data/lib/cyclone_lariat/messages/v1/validator.rb +31 -0
  23. data/lib/cyclone_lariat/messages/v2/abstract.rb +149 -0
  24. data/lib/cyclone_lariat/messages/v2/command.rb +20 -0
  25. data/lib/cyclone_lariat/messages/v2/event.rb +20 -0
  26. data/lib/cyclone_lariat/messages/v2/validator.rb +39 -0
  27. data/lib/cyclone_lariat/middleware.rb +9 -6
  28. data/lib/cyclone_lariat/migration.rb +54 -117
  29. data/lib/cyclone_lariat/options.rb +52 -0
  30. data/lib/cyclone_lariat/presenters/graph.rb +54 -0
  31. data/lib/cyclone_lariat/presenters/queues.rb +41 -0
  32. data/lib/cyclone_lariat/presenters/subscriptions.rb +34 -0
  33. data/lib/cyclone_lariat/presenters/topics.rb +40 -0
  34. data/lib/cyclone_lariat/publisher.rb +25 -0
  35. data/lib/cyclone_lariat/repo/active_record/messages.rb +92 -0
  36. data/lib/cyclone_lariat/repo/active_record/versions.rb +28 -0
  37. data/lib/cyclone_lariat/repo/messages.rb +43 -0
  38. data/lib/cyclone_lariat/repo/messages_mapper.rb +49 -0
  39. data/lib/cyclone_lariat/repo/sequel/messages.rb +73 -0
  40. data/lib/cyclone_lariat/repo/sequel/versions.rb +28 -0
  41. data/lib/cyclone_lariat/repo/versions.rb +42 -0
  42. data/lib/cyclone_lariat/resources/queue.rb +167 -0
  43. data/lib/cyclone_lariat/resources/topic.rb +132 -0
  44. data/lib/cyclone_lariat/services/migrate.rb +51 -0
  45. data/lib/cyclone_lariat/services/rollback.rb +51 -0
  46. data/lib/cyclone_lariat/version.rb +1 -1
  47. data/lib/cyclone_lariat.rb +4 -11
  48. data/lib/tasks/console.rake +1 -1
  49. data/lib/tasks/cyclone_lariat.rake +10 -12
  50. data/lib/tasks/db.rake +0 -15
  51. metadata +127 -27
  52. data/config/db.example.rb +0 -9
  53. data/config/initializers/sequel.rb +0 -7
  54. data/db/migrate/01_add_uuid_extensions.rb +0 -15
  55. data/db/migrate/02_add_events.rb +0 -19
  56. data/db/migrate/03_add_versions.rb +0 -9
  57. data/docs/_imgs/graphviz_01.png +0 -0
  58. data/docs/_imgs/graphviz_02.png +0 -0
  59. data/docs/_imgs/graphviz_03.png +0 -0
  60. data/docs/_imgs/lariat.jpg +0 -0
  61. data/docs/_imgs/logic.png +0 -0
  62. data/docs/_imgs/sqs_sns_diagram.png +0 -0
  63. data/lib/cyclone_lariat/abstract/client.rb +0 -112
  64. data/lib/cyclone_lariat/abstract/message.rb +0 -98
  65. data/lib/cyclone_lariat/command.rb +0 -13
  66. data/lib/cyclone_lariat/configure.rb +0 -15
  67. data/lib/cyclone_lariat/event.rb +0 -13
  68. data/lib/cyclone_lariat/messages_mapper.rb +0 -46
  69. data/lib/cyclone_lariat/messages_repo.rb +0 -60
  70. data/lib/cyclone_lariat/queue.rb +0 -147
  71. data/lib/cyclone_lariat/sns_client.rb +0 -149
  72. data/lib/cyclone_lariat/sqs_client.rb +0 -93
  73. data/lib/cyclone_lariat/topic.rb +0 -113
@@ -2,19 +2,24 @@
2
2
 
3
3
  require 'fileutils'
4
4
  require 'forwardable'
5
- require_relative 'sns_client'
6
- require_relative 'sqs_client'
5
+ require 'cyclone_lariat/clients/sns'
6
+ require 'cyclone_lariat/clients/sqs'
7
+ require 'cyclone_lariat/repo/versions'
8
+ require 'cyclone_lariat/services/migrate'
9
+ require 'cyclone_lariat/services/rollback'
10
+ require 'cyclone_lariat/presenters/topics'
11
+ require 'cyclone_lariat/presenters/queues'
12
+ require 'cyclone_lariat/presenters/subscriptions'
13
+ require 'cyclone_lariat/presenters/graph'
7
14
  require 'luna_park/errors'
8
- require 'terminal-table'
9
- require 'set'
10
15
 
11
16
  module CycloneLariat
12
17
  class Migration
13
18
  extend Forwardable
14
19
  include LunaPark::Extensions::Injector
15
20
 
16
- dependency(:sns) { CycloneLariat::SnsClient.new }
17
- dependency(:sqs) { CycloneLariat::SqsClient.new }
21
+ dependency(:sns) { CycloneLariat::Clients::Sns.new }
22
+ dependency(:sqs) { CycloneLariat::Clients::Sqs.new }
18
23
 
19
24
  DIR = './lariat/migrate'
20
25
 
@@ -56,7 +61,9 @@ module CycloneLariat
56
61
  )
57
62
  end
58
63
 
59
- def subscribe(topic:, endpoint:)
64
+ def subscribe(topic:, endpoint:, policy: nil)
65
+ policy ||= default_policy(endpoint)
66
+ sqs.add_policy(queue: endpoint, policy: policy) if endpoint.queue?
60
67
  sns.subscribe topic: topic, endpoint: endpoint
61
68
  puts " Subscription was created `#{topic.name} -> #{endpoint.name}`"
62
69
  end
@@ -66,6 +73,23 @@ module CycloneLariat
66
73
  puts " Subscription was deleted `#{topic.name} -> #{endpoint.name}`"
67
74
  end
68
75
 
76
+ def default_policy(queue)
77
+ {
78
+ 'Sid' => queue.arn,
79
+ 'Effect' => 'Allow',
80
+ 'Principal' => {
81
+ 'AWS' => '*'
82
+ },
83
+ 'Action' => 'SQS:*',
84
+ 'Resource' => queue.arn,
85
+ 'Condition' => {
86
+ 'ArnEquals' => {
87
+ 'aws:SourceArn' => fanout_arn_pattern
88
+ }
89
+ }
90
+ }
91
+ end
92
+
69
93
  def topics
70
94
  sns.list_all
71
95
  end
@@ -82,132 +106,45 @@ module CycloneLariat
82
106
 
83
107
  def process(resource:, for_topic:, for_queue:)
84
108
  case resource
85
- when Topic then for_topic.call(resource)
86
- when Queue then for_queue.call(resource)
109
+ when Resources::Topic then for_topic.call(resource)
110
+ when Resources::Queue then for_queue.call(resource)
87
111
  else
88
112
  raise ArgumentError, "Unknown resource class #{resource.class}"
89
113
  end
90
114
  end
91
115
 
116
+ def fanout_arn_pattern
117
+ @fanout_arn_pattern ||= [
118
+ 'arn:aws:sns',
119
+ CycloneLariat.config.aws_region,
120
+ CycloneLariat.config.aws_account_id,
121
+ "#{CycloneLariat.config.instance}-*-fanout-*"
122
+ ].join(':')
123
+ end
124
+
92
125
  class << self
93
- def migrate(dataset: CycloneLariat.versions_dataset, dir: DIR)
94
- alert('No one migration exists') if !Dir.exist?(dir) || Dir.empty?(dir)
95
-
96
- Dir.glob("#{dir}/*.rb") do |path|
97
- filename = File.basename(path, '.rb')
98
- version, title = filename.split('_', 2)
99
-
100
- existed_migrations = dataset.all.map { |row| row[:version] }
101
- unless existed_migrations.include? version.to_i
102
- class_name = title.split('_').collect(&:capitalize).join
103
- puts "Up - #{version} #{class_name} #{path}"
104
- require_relative Pathname.new(Dir.pwd) + Pathname.new(path)
105
- Object.const_get(class_name).new.up
106
- dataset.insert(version: version)
107
- end
108
- end
126
+ def migrate(repo: CycloneLariat::Repo::Versions.new, dir: DIR, service: Services::Migrate)
127
+ puts service.new(repo: repo, dir: dir).call
109
128
  end
110
129
 
111
- def rollback(version = nil, dataset: CycloneLariat.versions_dataset, dir: DIR)
112
- existed_migrations = dataset.all.map { |row| row[:version] }.sort
113
- version ||= existed_migrations[-1]
114
- migrations_to_downgrade = existed_migrations.select { |migration| migration >= version }
115
-
116
- paths = []
117
- migrations_to_downgrade.each do |migration|
118
- path = Pathname.new(Dir.pwd) + Pathname.new(dir)
119
- founded = Dir.glob("#{path}/#{migration}_*.rb")
120
- raise "Could not found migration: `#{migration}` in #{path}" if founded.empty?
121
- raise "Found lot of migration: `#{migration}` in #{path}" if founded.size > 1
122
-
123
- paths += founded
124
- end
125
-
126
- paths.each do |path|
127
- filename = File.basename(path, '.rb')
128
- version, title = filename.split('_', 2)
129
- class_name = title.split('_').collect(&:capitalize).join
130
- puts "Down - #{version} #{class_name} #{path}"
131
- require_relative Pathname.new(Dir.pwd) + Pathname.new(path)
132
- Object.const_get(class_name).new.down
133
- dataset.filter(version: version).delete
134
- end
130
+ def rollback(version = nil, repo: CycloneLariat::Repo::Versions.new, dir: DIR, service: Services::Rollback)
131
+ puts service.new(repo: repo, dir: dir).call(version)
135
132
  end
136
133
 
137
- def list_topics
138
- rows = []
139
- new.topics.each do |topic|
140
- rows << [
141
- topic.custom? ? 'custom' : 'standard',
142
- topic.region,
143
- topic.account_id,
144
- topic.name,
145
- topic.instance,
146
- topic.kind,
147
- topic.publisher,
148
- topic.type,
149
- topic.fifo
150
- ]
151
- end
152
-
153
- puts Terminal::Table.new rows: rows, headings: %w[valid region account_id name instance kind publisher type fifo]
134
+ def list_topics(presenter: Presenters::Topics)
135
+ puts presenter.call(new.topics)
154
136
  end
155
137
 
156
- def list_queues
157
- rows = []
158
- new.queues.each do |queue|
159
- rows << [
160
- queue.custom? ? 'custom' : 'standard',
161
- queue.region,
162
- queue.account_id,
163
- queue.name,
164
- queue.instance,
165
- queue.kind,
166
- queue.publisher,
167
- queue.type,
168
- queue.dest,
169
- queue.fifo
170
- ]
171
- end
172
-
173
- puts Terminal::Table.new rows: rows, headings: %w[valid region account_id name instance kind publisher type destination fifo]
138
+ def list_queues(presenter: Presenters::Queues)
139
+ puts presenter.call(new.queues)
174
140
  end
175
141
 
176
- def list_subscriptions
177
- rows = []
178
- new.subscriptions.each do |subscription|
179
- rows << [
180
- subscription[:topic].name,
181
- subscription[:endpoint].name,
182
- subscription[:arn]
183
- ]
184
- end
185
-
186
- puts Terminal::Table.new rows: rows, headings: %w[topic endpoint subscription_arn]
142
+ def list_subscriptions(presenter: Presenters::Subscriptions)
143
+ puts presenter.call(new.subscriptions)
187
144
  end
188
145
 
189
- def build_graph
190
- subscriptions = new.subscriptions
191
- resources_set = Set.new
192
-
193
- subscriptions.each do |subscription|
194
- resources_set << subscription[:topic]
195
- resources_set << subscription[:endpoint]
196
- end
197
-
198
- puts 'digraph G {'
199
- puts ' rankdir=LR;'
200
-
201
- resources_set.each do |resource|
202
- color = resource.custom? ? ', fillcolor=grey' : ', fillcolor=white'
203
- style = resource.topic? ? "[shape=component style=filled#{color}]" : "[shape=record, style=\"rounded,filled\"#{color}]"
204
- puts " \"#{resource.name}\" #{style};"
205
- end
206
-
207
- subscriptions.each do |subscription|
208
- puts " \"#{subscription[:topic].name}\" -> \"#{subscription[:endpoint].name}\";"
209
- end
210
- puts '}'
146
+ def build_graph(presenter: Presenters::Graph)
147
+ puts presenter.call(new.subscriptions)
211
148
  end
212
149
  end
213
150
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luna_park/values/compound'
4
+
5
+ module CycloneLariat
6
+ class Options < LunaPark::Values::Compound
7
+ attr_accessor :aws_key, :aws_secret_key, :publisher,
8
+ :aws_region, :instance, :aws_account_id,
9
+ :messages_dataset, :version, :versions_dataset,
10
+ :driver, :fake_publish
11
+
12
+ # @param [CycloneLariat::Options, Hash] other
13
+ # @return [CycloneLariat::Options]
14
+ def merge!(other)
15
+ other = self.class.wrap(other)
16
+
17
+ self.aws_key ||= other.aws_key
18
+ self.aws_secret_key ||= other.aws_secret_key
19
+ self.publisher ||= other.publisher
20
+ self.aws_region ||= other.aws_region
21
+ self.instance ||= other.instance
22
+ self.aws_account_id ||= other.aws_account_id
23
+ self.messages_dataset ||= other.messages_dataset
24
+ self.version ||= other.version
25
+ self.versions_dataset ||= other.versions_dataset
26
+ self.driver ||= other.driver
27
+ self.fake_publish ||= other.fake_publish
28
+
29
+ self
30
+ end
31
+
32
+ def merge(other)
33
+ dup.merge!(other)
34
+ end
35
+
36
+ def to_h
37
+ {
38
+ aws_key: aws_key,
39
+ aws_secret_key: aws_secret_key,
40
+ publisher: publisher,
41
+ aws_region: aws_region,
42
+ instance: instance,
43
+ aws_account_id: aws_account_id,
44
+ messages_dataset: messages_dataset,
45
+ version: version,
46
+ versions_dataset: versions_dataset,
47
+ driver: driver,
48
+ fake_publish: fake_publish
49
+ }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module CycloneLariat
6
+ module Presenters
7
+ class Graph
8
+ HEADS = %w[topic endpoint subscription_arn].freeze
9
+ def self.call(subscriptions)
10
+ new.call(subscriptions)
11
+ end
12
+
13
+ def call(subscriptions)
14
+ return '' if subscriptions.empty?
15
+
16
+ resources_set = Set.new
17
+
18
+ subscriptions.each do |subscription|
19
+ resources_set << subscription[:topic]
20
+ resources_set << subscription[:endpoint]
21
+ end
22
+
23
+ [].tap do |output|
24
+ output << open_graph
25
+
26
+ resources_set.each { |resource| output << present_resource(resource) }
27
+ subscriptions.each { |subscription| output << present_subscription(subscription) }
28
+
29
+ output << close_graph
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def present_resource(resource)
36
+ color = resource.custom? ? ', fillcolor=grey' : ', fillcolor=white'
37
+ style = resource.topic? ? "[shape=component style=filled#{color}]" : "[shape=record, style=\"rounded,filled\"#{color}]"
38
+ " \"#{resource.name}\" #{style};"
39
+ end
40
+
41
+ def present_subscription(subscription)
42
+ " \"#{subscription[:topic].name}\" -> \"#{subscription[:endpoint].name}\";"
43
+ end
44
+
45
+ def open_graph
46
+ "digraph G {\n rankdir=LR;"
47
+ end
48
+
49
+ def close_graph
50
+ '}'
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module CycloneLariat
6
+ module Presenters
7
+ class Queues
8
+ HEADS = %w[valid region account_id name instance kind publisher type destination fifo].freeze
9
+
10
+ def self.call(queues)
11
+ new.call(queues)
12
+ end
13
+
14
+ def call(queues)
15
+ rows = []
16
+ queues.each do |queue|
17
+ rows << row(queue)
18
+ end
19
+
20
+ Terminal::Table.new rows: rows, headings: HEADS
21
+ end
22
+
23
+ private
24
+
25
+ def row(queue)
26
+ [
27
+ queue.custom? ? 'custom' : 'standard',
28
+ queue.region,
29
+ queue.account_id,
30
+ queue.name,
31
+ queue.instance,
32
+ queue.kind,
33
+ queue.publisher,
34
+ queue.type,
35
+ queue.dest,
36
+ queue.fifo
37
+ ]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module CycloneLariat
6
+ module Presenters
7
+ class Subscriptions
8
+ HEADS = %w[topic endpoint subscription_arn].freeze
9
+
10
+ def self.call(subscriptions)
11
+ new.call(subscriptions)
12
+ end
13
+
14
+ def call(subscriptions)
15
+ rows = []
16
+ subscriptions.each do |subscription|
17
+ rows << row(subscription)
18
+ end
19
+
20
+ Terminal::Table.new rows: rows, headings: HEADS
21
+ end
22
+
23
+ private
24
+
25
+ def row(subscription)
26
+ [
27
+ subscription[:topic].name,
28
+ subscription[:endpoint].name,
29
+ subscription[:arn]
30
+ ]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module CycloneLariat
6
+ module Presenters
7
+ class Topics
8
+ HEADS = %w[valid region account_id name instance kind publisher type fifo].freeze
9
+
10
+ def self.call(topics)
11
+ new.call(topics)
12
+ end
13
+
14
+ def call(topics)
15
+ rows = []
16
+ topics.each do |topic|
17
+ rows << row(topic)
18
+ end
19
+
20
+ Terminal::Table.new rows: rows, headings: HEADS
21
+ end
22
+
23
+ private
24
+
25
+ def row(topic)
26
+ [
27
+ topic.custom? ? 'custom' : 'standard',
28
+ topic.region,
29
+ topic.account_id,
30
+ topic.name,
31
+ topic.instance,
32
+ topic.kind,
33
+ topic.publisher,
34
+ topic.type,
35
+ topic.fifo
36
+ ]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/clients/sns'
4
+ require 'cyclone_lariat/clients/sqs'
5
+
6
+ module CycloneLariat
7
+ class Publisher
8
+ include Generators::Event
9
+ include Generators::Command
10
+
11
+ attr_reader :config
12
+
13
+ def initialize(**options)
14
+ @config = CycloneLariat::Options.wrap(options).merge!(CycloneLariat.config)
15
+ end
16
+
17
+ def sqs
18
+ @sqs ||= Clients::Sqs.new(**config.to_h)
19
+ end
20
+
21
+ def sns
22
+ @sns ||= Clients::Sns.new(**config.to_h)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/messages/v1/event'
4
+ require 'cyclone_lariat/messages/v1/command'
5
+ require 'cyclone_lariat/repo/messages_mapper'
6
+
7
+ module CycloneLariat
8
+ module Repo
9
+ module ActiveRecord
10
+ class Messages
11
+ attr_reader :dataset
12
+
13
+ def initialize(dataset)
14
+ @dataset = dataset
15
+ end
16
+
17
+ def enabled?
18
+ !dataset.nil?
19
+ end
20
+
21
+ def disabled?
22
+ dataset.nil?
23
+ end
24
+
25
+ def create(msg)
26
+ dataset.create(MessagesMapper.to_row(msg)).uuid
27
+ end
28
+
29
+ def exists?(uuid:)
30
+ dataset.exists?(uuid: uuid)
31
+ end
32
+
33
+ def processed!(uuid:, error: nil)
34
+ data = { processed_at: current_timestamp_from_db }
35
+ data.merge!(client_error_message: error.message, client_error_details: JSON.generate(error.details)) if error
36
+
37
+ message = dataset.where(uuid: uuid).first
38
+ return false unless message
39
+
40
+ message.update(data)
41
+ end
42
+
43
+ def find(uuid:)
44
+ row = dataset.where(uuid: uuid).first
45
+ return if row.nil?
46
+
47
+ build_message_from_ar_row(row)
48
+ end
49
+
50
+ def each_unprocessed
51
+ dataset.where(processed_at: nil).each do |row|
52
+ msg = build_message_from_ar_row(row)
53
+ yield(msg)
54
+ end
55
+ end
56
+
57
+ def each_with_client_errors
58
+ dataset
59
+ .where.not(processed_at: nil)
60
+ .where.not(client_error_message: nil)
61
+ .each do |row|
62
+ msg = build_message_from_ar_row(row)
63
+ yield(msg)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def build_message_from_ar_row(row)
70
+ build MessagesMapper.from_row(row.attributes.symbolize_keys)
71
+ end
72
+
73
+ def current_timestamp_from_db
74
+ time_from_db =
75
+ ::ActiveRecord::Base
76
+ .connection.execute('select current_timestamp;')
77
+ .first
78
+ time = time_from_db.is_a?(Hash) ? time_from_db['current_timestamp'] : time_from_db[0]
79
+ time.is_a?(Time) ? time : Time.parse(time)
80
+ end
81
+
82
+ def build(raw)
83
+ case kind = raw.delete(:kind)
84
+ when 'event' then CycloneLariat::Messages::V1::Event.wrap raw
85
+ when 'command' then CycloneLariat::Messages::V1::Command.wrap raw
86
+ else raise ArgumentError, "Unknown kind `#{kind}` of message"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ module Repo
5
+ module ActiveRecord
6
+ class Versions
7
+ attr_reader :dataset
8
+
9
+ def initialize(dataset)
10
+ @dataset = dataset
11
+ end
12
+
13
+ def add(version)
14
+ dataset.create(version: version)
15
+ true
16
+ end
17
+
18
+ def remove(version)
19
+ dataset.where(version: version).delete_all.positive?
20
+ end
21
+
22
+ def all
23
+ dataset.pluck(:version).map { |version| { version: version } }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'luna_park/extensions/injector'
5
+ require 'cyclone_lariat/core'
6
+ require 'cyclone_lariat/repo/sequel/messages'
7
+ require 'cyclone_lariat/repo/active_record/messages'
8
+
9
+ module CycloneLariat
10
+ module Repo
11
+ class Messages
12
+ include LunaPark::Extensions::Injector
13
+
14
+ attr_reader :config
15
+
16
+ dependency(:sequel_messages_class) { Repo::Sequel::Messages }
17
+ dependency(:active_record_messages_class) { Repo::ActiveRecord::Messages }
18
+
19
+ extend Forwardable
20
+
21
+ def_delegators :driver, :create, :exists?, :processed!, :find, :each_unprocessed, :each_with_client_errors,
22
+ :enabled?, :disabled?
23
+
24
+ def initialize(**options)
25
+ @config = CycloneLariat::Options.wrap(options).merge!(CycloneLariat.config)
26
+ end
27
+
28
+ def driver
29
+ @driver ||= select(driver: config.driver)
30
+ end
31
+
32
+ private
33
+
34
+ def select(driver:)
35
+ case driver
36
+ when :sequel then sequel_messages_class.new(config.messages_dataset)
37
+ when :active_record then active_record_messages_class.new(config.messages_dataset)
38
+ else raise ArgumentError, "Undefined driver `#{driver}`"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ module Repo
5
+ class MessagesMapper
6
+ class << self
7
+ def from_row(row)
8
+ return if row.nil?
9
+
10
+ row[:data] = hash_from_json_column(row[:data])
11
+ row[:client_error_details] = hash_from_json_column(row[:client_error_details]) if row[:client_error_details]
12
+ row
13
+ end
14
+
15
+ def to_row(input)
16
+ {
17
+ uuid: input.uuid,
18
+ kind: input.kind,
19
+ type: input.type,
20
+ publisher: input.publisher,
21
+ data: JSON.generate(input.data),
22
+ client_error_message: input.client_error&.message,
23
+ client_error_details: JSON.generate(input.client_error&.details),
24
+ version: input.version,
25
+ sent_at: input.sent_at
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def hash_from_json_column(data)
32
+ return data if data.is_a?(Hash)
33
+ return JSON.parse(data) if data.is_a?(String)
34
+
35
+ if pg_json_extension_enabled?
36
+ return data.to_h if data.is_a?(::Sequel::Postgres::JSONHash)
37
+ return JSON.parse(data.to_s) if data.is_a?(::Sequel::Postgres::JSONString)
38
+ end
39
+
40
+ raise ArgumentError, "Unknown type of `#{data}`"
41
+ end
42
+
43
+ def pg_json_extension_enabled?
44
+ Object.const_defined?('Sequel::Postgres::JSONHash')
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end