cyclone_lariat 0.4.0 → 1.0.0.rc1

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.
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