cyclone_lariat 0.3.10 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/gem-push.yml +4 -4
  3. data/.gitignore +6 -0
  4. data/.rubocop.yml +30 -1
  5. data/CHANGELOG.md +11 -1
  6. data/Gemfile.lock +137 -30
  7. data/Guardfile +42 -0
  8. data/README.md +715 -143
  9. data/Rakefile +2 -5
  10. data/bin/cyclone_lariat +206 -0
  11. data/cyclone_lariat.gemspec +13 -2
  12. data/lib/cyclone_lariat/clients/abstract.rb +40 -0
  13. data/lib/cyclone_lariat/clients/sns.rb +163 -0
  14. data/lib/cyclone_lariat/clients/sqs.rb +114 -0
  15. data/lib/cyclone_lariat/core.rb +21 -0
  16. data/lib/cyclone_lariat/errors.rb +38 -0
  17. data/lib/cyclone_lariat/fake.rb +19 -0
  18. data/lib/cyclone_lariat/generators/command.rb +53 -0
  19. data/lib/cyclone_lariat/generators/event.rb +52 -0
  20. data/lib/cyclone_lariat/generators/queue.rb +30 -0
  21. data/lib/cyclone_lariat/generators/topic.rb +29 -0
  22. data/lib/cyclone_lariat/messages/v1/abstract.rb +139 -0
  23. data/lib/cyclone_lariat/messages/v1/command.rb +20 -0
  24. data/lib/cyclone_lariat/messages/v1/event.rb +20 -0
  25. data/lib/cyclone_lariat/messages/v1/validator.rb +31 -0
  26. data/lib/cyclone_lariat/messages/v2/abstract.rb +149 -0
  27. data/lib/cyclone_lariat/messages/v2/command.rb +20 -0
  28. data/lib/cyclone_lariat/messages/v2/event.rb +20 -0
  29. data/lib/cyclone_lariat/messages/v2/validator.rb +39 -0
  30. data/lib/cyclone_lariat/middleware.rb +9 -5
  31. data/lib/cyclone_lariat/migration.rb +151 -0
  32. data/lib/cyclone_lariat/options.rb +52 -0
  33. data/lib/cyclone_lariat/presenters/graph.rb +54 -0
  34. data/lib/cyclone_lariat/presenters/queues.rb +41 -0
  35. data/lib/cyclone_lariat/presenters/subscriptions.rb +34 -0
  36. data/lib/cyclone_lariat/presenters/topics.rb +40 -0
  37. data/lib/cyclone_lariat/publisher.rb +25 -0
  38. data/lib/cyclone_lariat/repo/active_record/messages.rb +92 -0
  39. data/lib/cyclone_lariat/repo/active_record/versions.rb +28 -0
  40. data/lib/cyclone_lariat/repo/messages.rb +43 -0
  41. data/lib/cyclone_lariat/repo/messages_mapper.rb +49 -0
  42. data/lib/cyclone_lariat/repo/sequel/messages.rb +73 -0
  43. data/lib/cyclone_lariat/repo/sequel/versions.rb +28 -0
  44. data/lib/cyclone_lariat/repo/versions.rb +42 -0
  45. data/lib/cyclone_lariat/resources/queue.rb +167 -0
  46. data/lib/cyclone_lariat/resources/topic.rb +132 -0
  47. data/lib/cyclone_lariat/services/migrate.rb +51 -0
  48. data/lib/cyclone_lariat/services/rollback.rb +51 -0
  49. data/lib/cyclone_lariat/version.rb +1 -1
  50. data/lib/cyclone_lariat.rb +4 -10
  51. data/lib/tasks/console.rake +13 -0
  52. data/lib/tasks/cyclone_lariat.rake +42 -0
  53. data/lib/tasks/db.rake +0 -15
  54. metadata +161 -20
  55. data/config/db.example.rb +0 -9
  56. data/db/migrate/01_add_uuid_extensions.rb +0 -15
  57. data/db/migrate/02_add_events.rb +0 -19
  58. data/docs/_imgs/diagram.png +0 -0
  59. data/docs/_imgs/lariat.jpg +0 -0
  60. data/lib/cyclone_lariat/abstract/client.rb +0 -106
  61. data/lib/cyclone_lariat/abstract/message.rb +0 -83
  62. data/lib/cyclone_lariat/command.rb +0 -13
  63. data/lib/cyclone_lariat/configure.rb +0 -15
  64. data/lib/cyclone_lariat/event.rb +0 -13
  65. data/lib/cyclone_lariat/messages_mapper.rb +0 -46
  66. data/lib/cyclone_lariat/messages_repo.rb +0 -60
  67. data/lib/cyclone_lariat/sns_client.rb +0 -38
  68. data/lib/cyclone_lariat/sqs_client.rb +0 -39
data/Rakefile CHANGED
@@ -7,9 +7,6 @@ require 'rspec/core/rake_task'
7
7
  RSpec::Core::RakeTask.new(:spec)
8
8
 
9
9
  # tasks from lib directory
10
- Dir[File.expand_path('lib/tasks/**/*.rake', __dir__)].each do |entity|
11
- print "#{entity} : "
12
- puts load entity
13
- end
10
+ Rake.add_rakelib 'lib/tasks'
14
11
 
15
- task default: %i[spec] # rubocop]
12
+ task default: %i[spec]
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/cyclone_lariat'
5
+ require 'bundler/setup'
6
+ require 'dry/cli'
7
+ require 'fileutils'
8
+
9
+ module CycloneLariat
10
+ module CLI
11
+ module Commands
12
+ extend Dry::CLI::Registry
13
+
14
+ INITIALIZERS_DIR = './config/initializers'
15
+ RAKE_TASKS_DIR = './lib/tasks'
16
+
17
+ class Version < Dry::CLI::Command
18
+ desc 'Print version'
19
+
20
+ def call(*)
21
+ puts CycloneLariat::VERSION
22
+ end
23
+ end
24
+
25
+ class Install < Dry::CLI::Command
26
+ desc 'Install cyclone lariat to current directory'
27
+ option :adapter,
28
+ default: 'sequel',
29
+ values: %w[sequel active_record],
30
+ desc: 'adapter for store events and versions'
31
+
32
+ def call(adapter: 'sequel', **)
33
+ create_config(adapter)
34
+ create_rake_task
35
+ end
36
+
37
+ def create_config(adapter)
38
+ FileUtils.mkdir_p INITIALIZERS_DIR unless Dir.exist? INITIALIZERS_DIR
39
+ config_path = "#{INITIALIZERS_DIR}/cyclone_lariat.rb"
40
+ config_file = File.open(config_path, 'w')
41
+ config_file.puts config_contents(adapter)
42
+ puts "Created config: #{config_path}"
43
+ end
44
+
45
+ def create_rake_task
46
+ FileUtils.mkdir_p RAKE_TASKS_DIR unless Dir.exist? RAKE_TASKS_DIR
47
+ config_path = "#{RAKE_TASKS_DIR}/cyclone_lariat.rake"
48
+ config_file = File.open(config_path, 'w')
49
+ config_file.puts rake_task_context
50
+ puts "Created rake task: #{config_path}"
51
+ end
52
+
53
+ def config_contents(adapter)
54
+ return config_active_record_contents if adapter == 'active_record'
55
+ return config_sequel_contents if adapter == 'sequel'
56
+
57
+ raise ArgumentError, "Unknown adapter #{adapter}"
58
+ end
59
+
60
+ def config_sequel_contents
61
+ <<~CONFIG
62
+ # frozen_string_literal: true
63
+
64
+ CycloneLariat.configure do |c|
65
+ c.version = 1 # messages version
66
+ c.aws_key = ENV['AWS_KEY'] # aws key
67
+ c.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
68
+ c.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
69
+ c.aws_region = ENV['AWS_REGION'] # aws default region
70
+ c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
71
+ c.instance = ENV['INSTANCE'] # stage, production, test
72
+ c.driver = :sequel # :sequel or :active_record
73
+ c.messages_dataset = DB[:messages] # Sequel dataset / ActiveRecord model for store income messages (on receiver)
74
+ c.versions_dataset = DB[:lariat_versions] # Sequel dataset / ActiveRecord model for publisher migrations
75
+ c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
76
+ end
77
+ CONFIG
78
+ end
79
+
80
+ def config_active_record_contents
81
+ <<~CONFIG
82
+ # frozen_string_literal: true
83
+
84
+ CycloneLariat.configure do |c|
85
+ c.version = 1 # messages version
86
+ c.aws_key = ENV['AWS_KEY'] # aws key
87
+ c.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
88
+ c.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
89
+ c.aws_region = ENV['AWS_REGION'] # aws default region
90
+ c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
91
+ c.instance = ENV['INSTANCE'] # stage, production, test
92
+ c.driver = :active_record # :sequel or :active_record
93
+ c.messages_dataset = CycloneLariatMessage # Sequel dataset / ActiveRecord model for store income messages (on receiver)
94
+ c.versions_dataset = CycloneLariatVersion # Sequel dataset / ActiveRecord model for publisher migrations
95
+ c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
96
+ end
97
+ CONFIG
98
+ end
99
+
100
+ def rake_task_context
101
+ <<~TASKS
102
+ # frozen_string_literal: true
103
+
104
+ require 'cyclone_lariat'
105
+
106
+ namespace :cyclone_lariat do
107
+ desc 'Migrate topics for SQS/SNS'
108
+ task migrate: :cyclone_lariat_config do
109
+ CycloneLariat::Migration.migrate
110
+ end
111
+
112
+ desc 'Rollback topics for SQS/SNS'
113
+ task :rollback, [:version] => :cyclone_lariat_config do |_, args|
114
+ target_version = args[:version] ? args[:version].to_i : nil
115
+ CycloneLariat::Migration.rollback(target_version)
116
+ end
117
+
118
+ namespace :list do
119
+ desc 'List all topics'
120
+ task topics: :cyclone_lariat_config do
121
+ CycloneLariat::Migration.list_topics
122
+ end
123
+
124
+ desc 'List all queues'
125
+ task queues: :cyclone_lariat_config do
126
+ CycloneLariat::Migration.list_queues
127
+ end
128
+
129
+ desc 'List all subscriptions'
130
+ task subscriptions: :cyclone_lariat_config do
131
+ CycloneLariat::Migration.list_subscriptions
132
+ end
133
+ end
134
+
135
+ desc 'Build graphviz graph for whole system'
136
+ task graph: :cyclone_lariat_config do
137
+ CycloneLariat::Migration.build_graph
138
+ end
139
+
140
+ task :cyclone_lariat_config do
141
+ require_relative '../../config/initializers/cyclone_lariat'
142
+ end
143
+ end
144
+ TASKS
145
+ end
146
+ end
147
+
148
+ module Generate
149
+ class Migration < Dry::CLI::Command
150
+ desc 'Generate migration'
151
+
152
+ argument :title, type: :string, required: true, desc: 'Title of migration use only a-z and _'
153
+
154
+ def call(title:, **)
155
+ abort('Use only a-z and _ in your title') unless title_correct? title
156
+
157
+ FileUtils.mkdir_p CycloneLariat::Migration::DIR unless Dir.exist? CycloneLariat::Migration::DIR
158
+
159
+ file_name = generate_filename(title)
160
+ class_name = generate_class_name(title)
161
+
162
+ file = File.open(file_name, 'w')
163
+ file.puts(file_contents(class_name))
164
+ puts "Migration successful created:\n\t#{file_name}"
165
+ end
166
+
167
+ private
168
+
169
+ def title_correct?(title)
170
+ /^(?!.*__.*)[a-z]?[a-z_]+[a-z]+$/.match? title
171
+ end
172
+
173
+ def generate_filename(title)
174
+ "#{CycloneLariat::Migration::DIR}/#{Time.now.to_i}_#{title}.rb"
175
+ end
176
+
177
+ def generate_class_name(title)
178
+ title.split('_').collect(&:capitalize).join
179
+ end
180
+
181
+ def file_contents(klass_name)
182
+ <<~MIGRATION
183
+ # frozen_string_literal: true
184
+
185
+ class #{klass_name} < CycloneLariat::Migration
186
+ def up
187
+ end
188
+
189
+ def down
190
+ end
191
+ end
192
+ MIGRATION
193
+ end
194
+ end
195
+ end
196
+
197
+ register 'version', Version, aliases: %w[v -v --version]
198
+ register 'install', Install
199
+ register 'generate', aliases: %w[g] do |prefix|
200
+ prefix.register 'migration', Generate::Migration
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ Dry::CLI.new(CycloneLariat::CLI::Commands).call
@@ -7,7 +7,7 @@ require 'cyclone_lariat/version'
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = 'cyclone_lariat'
9
9
  spec.version = CycloneLariat::VERSION
10
- spec.authors = ['Alexander Kudrin', 'Philip Sorokin']
10
+ spec.authors = ['Alexander Kudrin', 'Philip Sorokin', 'Kirill Drozdov', 'Vitaly Perminov']
11
11
  spec.email = ['kudrin.alexander@gmail.com']
12
12
 
13
13
  spec.summary = 'Shoryuken middleware for LunaPark based application.'
@@ -28,17 +28,28 @@ Gem::Specification.new do |spec|
28
28
  # Specify which files should be added to the gem when it is released.
29
29
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
30
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
31
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|docs|examples|config)/}) }
32
32
  end
33
+
33
34
  spec.require_paths = ['lib']
35
+ spec.bindir = 'bin'
36
+ spec.executables = ['cyclone_lariat']
34
37
 
35
38
  spec.add_dependency 'aws-sdk-sns'
36
39
  spec.add_dependency 'aws-sdk-sqs'
40
+ spec.add_dependency 'dry-cli', '~> 0.6'
41
+ spec.add_dependency 'dry-validation', '~> 1.5'
37
42
  spec.add_dependency 'luna_park', '~> 0.11'
43
+ spec.add_dependency 'terminal-table', '~> 3.0'
38
44
 
39
45
  spec.add_development_dependency 'bundler', '~> 1.17'
40
46
  spec.add_development_dependency 'byebug', '~> 11.1'
47
+ spec.add_development_dependency 'database_cleaner-active_record'
41
48
  spec.add_development_dependency 'database_cleaner-sequel', '~> 2.0'
49
+ spec.add_development_dependency 'guard'
50
+ spec.add_development_dependency 'guard-bundler'
51
+ spec.add_development_dependency 'guard-rspec'
52
+ spec.add_development_dependency 'guard-rubocop'
42
53
  spec.add_development_dependency 'pg', '~> 1.2'
43
54
  spec.add_development_dependency 'pry', '~> 0.13'
44
55
  spec.add_development_dependency 'pry-byebug', '~> 3.9'
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luna_park/extensions/injector'
4
+ require 'cyclone_lariat/generators/event'
5
+ require 'cyclone_lariat/generators/command'
6
+ require 'cyclone_lariat/errors'
7
+ require 'cyclone_lariat/core'
8
+
9
+ module CycloneLariat
10
+ module Clients
11
+ class Abstract
12
+ include LunaPark::Extensions::Injector
13
+ include Generators::Event
14
+ include Generators::Command
15
+
16
+ dependency(:aws_client_class) { raise ArgumentError, 'Client class should be defined' }
17
+ dependency(:aws_credentials_class) { Aws::Credentials }
18
+
19
+ def initialize(**options)
20
+ @config = CycloneLariat::Options.wrap(options).merge!(CycloneLariat.config)
21
+ end
22
+
23
+ attr_reader :config
24
+
25
+ def publish
26
+ raise LunaPark::Errors::AbstractMethod, 'Publish method should be defined'
27
+ end
28
+
29
+ private
30
+
31
+ def aws_client
32
+ @aws_client ||= aws_client_class.new(credentials: aws_credentials, region: config.aws_region)
33
+ end
34
+
35
+ def aws_credentials
36
+ @aws_credentials ||= aws_credentials_class.new(config.aws_key, config.aws_secret_key)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sns'
4
+ require 'cyclone_lariat/fake'
5
+ require 'cyclone_lariat/clients/abstract'
6
+ require 'cyclone_lariat/resources/topic'
7
+ require 'cyclone_lariat/resources/queue'
8
+
9
+ module CycloneLariat
10
+ module Clients
11
+ class Sns < Abstract
12
+ include LunaPark::Extensions::Injector
13
+ include Generators::Topic
14
+
15
+ dependency(:aws_client_class) { Aws::SNS::Client }
16
+
17
+ def publish(msg, fifo:, topic: nil, skip_validation: false)
18
+ return Fake.sns_publish_response(msg) if config.fake_publish
19
+
20
+ topic = topic ? custom_topic(topic) : topic(msg.type, kind: msg.kind, fifo: fifo)
21
+
22
+ raise Errors::GroupIdUndefined.new(resource: topic) if fifo && msg.group_id.nil?
23
+ raise Errors::GroupDefined.new(resource: topic) if !fifo && msg.group_id
24
+ raise Errors::DeduplicationIdDefined.new(resource: topic) if !fifo && msg.deduplication_id
25
+
26
+ msg.validation.check! unless skip_validation
27
+
28
+ params = {
29
+ topic_arn: topic.arn,
30
+ message: msg.to_json,
31
+ message_group_id: msg.group_id,
32
+ message_deduplication_id: msg.deduplication_id
33
+ }.compact
34
+
35
+ aws_client.publish(**params)
36
+ end
37
+
38
+ def exists?(topic)
39
+ raise ArgumentError, 'Should be Topic' unless topic.is_a? Resources::Topic
40
+
41
+ aws_client.get_topic_attributes({ topic_arn: topic.arn }) && true
42
+ rescue Aws::SNS::Errors::NotFound
43
+ false
44
+ end
45
+
46
+ def publish_event(type, fifo:, topic: nil, **options)
47
+ options[:version] ||= config.version
48
+ options[:data] ||= {}
49
+ options[:uuid] ||= SecureRandom.uuid
50
+
51
+ publish event(type, **options), fifo: fifo, topic: topic
52
+ end
53
+
54
+ def publish_command(type, fifo:, topic: nil, **options)
55
+ options[:version] ||= config.version
56
+ options[:data] ||= {}
57
+ options[:uuid] ||= SecureRandom.uuid
58
+
59
+ publish command(type, **options), fifo: fifo, topic: topic
60
+ end
61
+
62
+ def create(topic)
63
+ raise ArgumentError, 'Should be Resources::Topic' unless topic.is_a? Resources::Topic
64
+ raise Errors::TopicAlreadyExists.new(expected_topic: topic.name) if exists?(topic)
65
+
66
+ aws_client.create_topic(name: topic.name, attributes: topic.attributes, tags: topic.tags)
67
+ topic
68
+ end
69
+
70
+ def delete(topic)
71
+ raise ArgumentError, 'Should be Resources::Topic' unless topic.is_a? Resources::Topic
72
+ raise Errors::TopicDoesNotExists.new(expected_topic: topic.name) unless exists?(topic)
73
+
74
+ aws_client.delete_topic topic_arn: topic.arn
75
+ topic
76
+ end
77
+
78
+ def subscribe(topic:, endpoint:)
79
+ subscription_arn = find_subscription_arn(topic: topic, endpoint: endpoint)
80
+ raise Errors::SubscriptionAlreadyExists.new(topic: topic, endpoint: endpoint) if subscription_arn
81
+
82
+ aws_client.subscribe(
83
+ {
84
+ topic_arn: topic.arn,
85
+ protocol: endpoint.protocol,
86
+ endpoint: endpoint.arn
87
+ }
88
+ )
89
+ end
90
+
91
+ def unsubscribe(topic:, endpoint:)
92
+ subscription_arn = find_subscription_arn(topic: topic, endpoint: endpoint)
93
+ raise Errors::SubscriptionDoesNotExists.new(topic: topic, endpoint: endpoint) unless subscription_arn
94
+
95
+ aws_client.unsubscribe(subscription_arn: subscription_arn)
96
+ end
97
+
98
+ def list_all
99
+ topics = []
100
+ resp = aws_client.list_topics
101
+
102
+ loop do
103
+ resp[:topics].map do |t|
104
+ topics << Resources::Topic.from_arn(t[:topic_arn])
105
+ end
106
+
107
+ break if resp[:next_token].nil?
108
+
109
+ resp = aws_client.list_topics(next_token: resp[:next_token])
110
+ end
111
+ topics
112
+ end
113
+
114
+ def list_subscriptions
115
+ subscriptions = []
116
+ resp = aws_client.list_subscriptions
117
+
118
+ loop do
119
+ resp[:subscriptions].each do |s|
120
+ endpoint = s.endpoint.split(':')[2] == 'sqs' ? Resources::Queue.from_arn(s.endpoint) : Resources::Topic.from_arn(s.endpoint)
121
+ subscriptions << { topic: Resources::Topic.from_arn(s.topic_arn), endpoint: endpoint, arn: s.subscription_arn }
122
+ end
123
+
124
+ break if resp[:next_token].nil?
125
+
126
+ resp = aws_client.list_subscriptions(next_token: resp[:next_token])
127
+ end
128
+ subscriptions
129
+ end
130
+
131
+ def topic_subscriptions(topic)
132
+ raise ArgumentError, 'Should be Topic' unless topic.is_a? Resources::Topic
133
+
134
+ subscriptions = []
135
+
136
+ resp = aws_client.list_subscriptions_by_topic(topic_arn: topic.arn)
137
+
138
+ loop do
139
+ next_token = resp[:next_token]
140
+ subscriptions += resp[:subscriptions]
141
+
142
+ break if next_token.nil?
143
+
144
+ resp = aws_client.list_subscriptions_by_topic(topic_arn: topic.arn, next_token: next_token)
145
+ end
146
+ subscriptions
147
+ end
148
+
149
+ def find_subscription_arn(topic:, endpoint:)
150
+ raise ArgumentError, 'Should be Topic' unless topic.is_a? Resources::Topic
151
+ unless [Resources::Topic, Resources::Queue].include? endpoint.class
152
+ raise ArgumentError, 'Endpoint should be Topic or Queue'
153
+ end
154
+
155
+ found_subscription = topic_subscriptions(topic).select do |subscription|
156
+ subscription.endpoint == endpoint.arn
157
+ end.first
158
+
159
+ found_subscription ? found_subscription.subscription_arn : nil
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sqs'
4
+ require 'cyclone_lariat/fake'
5
+ require 'cyclone_lariat/clients/abstract'
6
+ require 'cyclone_lariat/resources/queue'
7
+ require 'cyclone_lariat/generators/queue'
8
+
9
+ module CycloneLariat
10
+ module Clients
11
+ class Sqs < Abstract
12
+ include LunaPark::Extensions::Injector
13
+ include Generators::Queue
14
+
15
+ dependency(:aws_client_class) { Aws::SQS::Client }
16
+
17
+ def exists?(queue)
18
+ raise ArgumentError, 'Should be queue' unless queue.is_a? Resources::Queue
19
+
20
+ aws_client.get_queue_url(queue_name: queue.to_s) && true
21
+ rescue Aws::SQS::Errors::NonExistentQueue
22
+ false
23
+ end
24
+
25
+ def add_policy(queue:, policy:)
26
+ current_policy_json = aws_client.get_queue_attributes({
27
+ queue_url: queue.url,
28
+ attribute_names: ['Policy']
29
+ }).attributes['Policy']
30
+
31
+ current_policy = JSON.parse(current_policy_json) if current_policy_json
32
+
33
+ return if current_policy && current_policy['Statement'].find { |s| s['Sid'] == policy['Sid'] }
34
+
35
+ new_policy = current_policy || { 'Statement' => [] }
36
+ new_policy['Statement'] << policy
37
+
38
+ aws_client.set_queue_attributes({ queue_url: queue.url, attributes: { 'Policy' => new_policy.to_json } })
39
+ end
40
+
41
+ def publish(msg, fifo:, dest: nil, queue: nil, skip_validation: false)
42
+ return Fake.sqs_send_message_result(msg) if config.fake_publish
43
+
44
+ queue = queue ? custom_queue(queue) : queue(msg.type, kind: msg.kind, fifo: fifo, dest: dest)
45
+
46
+ raise Errors::GroupIdUndefined.new(resource: queue) if fifo && msg.group_id.nil?
47
+ raise Errors::GroupDefined.new(resource: queue) if !fifo && msg.group_id
48
+ raise Errors::DeduplicationIdDefined.new(resource: queue) if !fifo && msg.deduplication_id
49
+
50
+ msg.validation.check! unless skip_validation
51
+
52
+ params = {
53
+ queue_url: queue.url,
54
+ message_body: msg.to_json,
55
+ message_group_id: msg.group_id,
56
+ message_deduplication_id: msg.deduplication_id
57
+ }.compact
58
+
59
+ aws_client.send_message(**params)
60
+ end
61
+
62
+ def publish_event(type, fifo:, dest: nil, queue: nil, **options)
63
+ options[:version] ||= self.config.version
64
+ options[:data] ||= {}
65
+ options[:uuid] ||= SecureRandom.uuid
66
+
67
+ publish event(type, data: data, **options), fifo: fifo, dest: dest, queue: queue
68
+ end
69
+
70
+ def publish_command(type, fifo:, dest: nil, queue: nil, **options)
71
+ options[:version] ||= self.config.version
72
+ options[:data] ||= {}
73
+ options[:uuid] ||= SecureRandom.uuid
74
+
75
+ publish event(type, data: data, **options), fifo: fifo, dest: dest, queue: queue
76
+ end
77
+
78
+ def create(queue)
79
+ raise ArgumentError, 'Should be queue' unless queue.is_a? Resources::Queue
80
+ raise Errors::QueueAlreadyExists.new(expected_queue: queue.name) if exists?(queue)
81
+
82
+ aws_client.create_queue(queue_name: queue.name, attributes: queue.attributes, tags: queue.tags)
83
+ queue
84
+ end
85
+
86
+ def delete(queue)
87
+ raise ArgumentError, 'Should be queue' unless queue.is_a? Resources::Queue
88
+ raise Errors::QueueDoesNotExists.new(expected_queue: queue.name) unless exists?(queue)
89
+
90
+ aws_client.delete_queue queue_url: queue.url
91
+ queue
92
+ end
93
+
94
+ def list_all
95
+ queues = []
96
+ resp = aws_client.list_queues
97
+
98
+ loop do
99
+ next_token = resp[:next_token]
100
+
101
+ resp[:queue_urls].map do |url|
102
+ queues << Resources::Queue.from_url(url)
103
+ end
104
+
105
+ break if next_token.nil?
106
+
107
+ resp = aws_client.list_queues(next_token: next_token)
108
+ end
109
+
110
+ queues
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/generators/queue'
4
+ require 'cyclone_lariat/generators/topic'
5
+ require 'cyclone_lariat/options'
6
+
7
+ module CycloneLariat
8
+ module CycloneLariatMethods
9
+ def config
10
+ @config ||= Options.new
11
+ end
12
+
13
+ def configure
14
+ yield(config)
15
+ end
16
+ end
17
+
18
+ extend Generators::Topic
19
+ extend Generators::Queue
20
+ extend CycloneLariatMethods
21
+ end
@@ -18,5 +18,43 @@ module CycloneLariat
18
18
  other.details == details
19
19
  end
20
20
  end
21
+
22
+ class TopicAlreadyExists < LunaPark::Errors::System
23
+ message { |d| "Topic already exists: `#{d[:expected_topic]}`" }
24
+ end
25
+
26
+ class TopicDoesNotExists < LunaPark::Errors::System
27
+ message { |d| "Topic does not exists: `#{d[:expected_topic]}`" }
28
+ end
29
+
30
+ class QueueAlreadyExists < LunaPark::Errors::System
31
+ message { |d| "Queue already exists: `#{d[:expected_queue]}`" }
32
+ end
33
+
34
+ class QueueDoesNotExists < LunaPark::Errors::System
35
+ message { |d| "Queue does not exists: `#{d[:expected_queue]}`" }
36
+ end
37
+ class SubscriptionAlreadyExists < LunaPark::Errors::System
38
+ message { |d| "Subscription for topic `#{d[:topic].name}`, on endpoint `#{d[:endpoint].name}` already exists" }
39
+ end
40
+ class SubscriptionDoesNotExists < LunaPark::Errors::System
41
+ message { |d| "Subscription for topic `#{d[:topic].name}`, on endpoint `#{d[:endpoint].name}` does not exists" }
42
+ end
43
+
44
+ class InvalidMessage < LunaPark::Errors::Business
45
+ message 'Message is not valid'
46
+ end
47
+
48
+ class GroupIdUndefined < LunaPark::Errors::System
49
+ message { |d| "Group id must be defined for FIFO resources: `#{d[:resource].name}`" }
50
+ end
51
+
52
+ class GroupDefined < LunaPark::Errors::System
53
+ message { |d| "Group id must be nil for non-FIFO resources: `#{d[:resource].name}`" }
54
+ end
55
+
56
+ class DeduplicationIdDefined < LunaPark::Errors::System
57
+ message { |d| "Deduplication id must be nil for non-FIFO resources: `#{d[:resource].name}`" }
58
+ end
21
59
  end
22
60
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ class Fake
5
+ def self.sns_publish_response(message)
6
+ Aws::SNS::Types::PublishResponse.new.tap do |resp|
7
+ resp.message_id = SecureRandom.uuid
8
+ resp.sequence_number = rand(10).to_s if message.fifo?
9
+ end
10
+ end
11
+
12
+ def self.sqs_send_message_result(message)
13
+ Aws::SQS::Types::SendMessageResult.new.tap do |res|
14
+ res.message_id = SecureRandom.uuid
15
+ res.sequence_number = rand(10).to_s if message.fifo?
16
+ end
17
+ end
18
+ end
19
+ end