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
data/bin/cyclone_lariat CHANGED
@@ -24,13 +24,25 @@ module CycloneLariat
24
24
 
25
25
  class Install < Dry::CLI::Command
26
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'
27
31
 
28
- def call(*)
29
- create_config
32
+ def call(adapter: 'sequel', **)
33
+ create_config(adapter)
30
34
  create_rake_task
31
35
  end
32
36
 
33
- def create_config
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
34
46
  FileUtils.mkdir_p RAKE_TASKS_DIR unless Dir.exist? RAKE_TASKS_DIR
35
47
  config_path = "#{RAKE_TASKS_DIR}/cyclone_lariat.rake"
36
48
  config_file = File.open(config_path, 'w')
@@ -38,28 +50,49 @@ module CycloneLariat
38
50
  puts "Created rake task: #{config_path}"
39
51
  end
40
52
 
41
- def create_rake_task
42
- FileUtils.mkdir_p INITIALIZERS_DIR unless Dir.exist? INITIALIZERS_DIR
43
- config_path = "#{INITIALIZERS_DIR}/cyclone_lariat.rb"
44
- config_file = File.open(config_path, 'w')
45
- config_file.puts config_contents
46
- puts "Created config: #{config_path}"
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}"
47
58
  end
48
59
 
49
- def config_contents
60
+ def config_sequel_contents
50
61
  <<~CONFIG
51
62
  # frozen_string_literal: true
52
63
 
53
- CycloneLariat.tap do |cl|
54
- cl.default_version = 1 # api version
55
- cl.aws_key = ENV['AWS_KEY'] # aws key
56
- cl.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
57
- cl.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
58
- cl.aws_default_region = ENV['AWS_REGION'] # aws default region
59
- cl.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
60
- cl.default_instance = ENV['INSTANCE'] # stage, production, test
61
- cl.events_dataset = DB[:events] # sequel dataset for store income messages, for receiver
62
- cl.versions_dataset = DB[:lariat_versions] # sequel dataset for migrations, for publisher
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
63
96
  end
64
97
  CONFIG
65
98
  end
@@ -69,46 +102,44 @@ module CycloneLariat
69
102
  # frozen_string_literal: true
70
103
 
71
104
  require 'cyclone_lariat'
72
-
105
+
73
106
  namespace :cyclone_lariat do
74
107
  desc 'Migrate topics for SQS/SNS'
75
- task migrate: :config do
76
- require_relative '../../config/initializers/cyclone_lariat'
108
+ task migrate: :cyclone_lariat_config do
77
109
  CycloneLariat::Migration.migrate
78
110
  end
79
-
111
+
80
112
  desc 'Rollback topics for SQS/SNS'
81
- task :rollback, [:version] => :config do |_, args|
82
- require_relative '../../config/initializers/cyclone_lariat'
113
+ task :rollback, [:version] => :cyclone_lariat_config do |_, args|
83
114
  target_version = args[:version] ? args[:version].to_i : nil
84
115
  CycloneLariat::Migration.rollback(target_version)
85
116
  end
86
-
117
+
87
118
  namespace :list do
88
119
  desc 'List all topics'
89
- task :topics do
90
- require_relative '../../config/initializers/cyclone_lariat'
120
+ task topics: :cyclone_lariat_config do
91
121
  CycloneLariat::Migration.list_topics
92
122
  end
93
-
123
+
94
124
  desc 'List all queues'
95
- task :queues do
96
- require_relative '../../config/initializers/cyclone_lariat'
125
+ task queues: :cyclone_lariat_config do
97
126
  CycloneLariat::Migration.list_queues
98
127
  end
99
-
128
+
100
129
  desc 'List all subscriptions'
101
- task :subscriptions do
102
- require_relative '../../config/initializers/cyclone_lariat'
130
+ task subscriptions: :cyclone_lariat_config do
103
131
  CycloneLariat::Migration.list_subscriptions
104
132
  end
105
133
  end
106
-
134
+
107
135
  desc 'Build graphviz graph for whole system'
108
- task :graph do
109
- require_relative '../../config/initializers/cyclone_lariat'
136
+ task graph: :cyclone_lariat_config do
110
137
  CycloneLariat::Migration.build_graph
111
138
  end
139
+
140
+ task :cyclone_lariat_config do
141
+ require_relative '../../config/initializers/cyclone_lariat'
142
+ end
112
143
  end
113
144
  TASKS
114
145
  end
@@ -125,10 +156,11 @@ module CycloneLariat
125
156
 
126
157
  FileUtils.mkdir_p CycloneLariat::Migration::DIR unless Dir.exist? CycloneLariat::Migration::DIR
127
158
 
128
- file_name = generate_filename title
129
- class_name = generate_class_name title
130
- file = File.open(file_name, 'w')
131
- file.puts file_contents(class_name)
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))
132
164
  puts "Migration successful created:\n\t#{file_name}"
133
165
  end
134
166
 
@@ -153,7 +185,7 @@ module CycloneLariat
153
185
  class #{klass_name} < CycloneLariat::Migration
154
186
  def up
155
187
  end
156
-
188
+
157
189
  def down
158
190
  end
159
191
  end
@@ -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,21 +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
34
  spec.require_paths = ['lib']
35
- spec.executables = ['cyclone_lariat']
35
+ spec.bindir = 'bin'
36
+ spec.executables = ['cyclone_lariat']
36
37
 
37
38
  spec.add_dependency 'aws-sdk-sns'
38
39
  spec.add_dependency 'aws-sdk-sqs'
39
40
  spec.add_dependency 'dry-cli', '~> 0.6'
41
+ spec.add_dependency 'dry-validation', '~> 1.5'
40
42
  spec.add_dependency 'luna_park', '~> 0.11'
41
43
  spec.add_dependency 'terminal-table', '~> 3.0'
42
44
 
43
45
  spec.add_development_dependency 'bundler', '~> 1.17'
44
46
  spec.add_development_dependency 'byebug', '~> 11.1'
47
+ spec.add_development_dependency 'database_cleaner-active_record'
45
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'
46
53
  spec.add_development_dependency 'pg', '~> 1.2'
47
54
  spec.add_development_dependency 'pry', '~> 0.13'
48
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
@@ -40,5 +40,21 @@ module CycloneLariat
40
40
  class SubscriptionDoesNotExists < LunaPark::Errors::System
41
41
  message { |d| "Subscription for topic `#{d[:topic].name}`, on endpoint `#{d[:endpoint].name}` does not exists" }
42
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
43
59
  end
44
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
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'cyclone_lariat/messages/v1/command'
5
+ require 'cyclone_lariat/messages/v2/command'
6
+
7
+ module CycloneLariat
8
+ module Generators
9
+ module Command
10
+ def command(type, version: config.version, **options)
11
+ case version.to_i
12
+ when 1 then command_v1(type, **options)
13
+ when 2 then command_v2(type, **options)
14
+ else raise ArgumentError, "Unknown version #{version}"
15
+ end
16
+ end
17
+
18
+ def command_v1(type, data: {}, request_id: nil, group_id: nil, deduplication_id: nil, uuid: SecureRandom.uuid)
19
+ params = {
20
+ uuid: uuid,
21
+ type: type,
22
+ sent_at: Time.now.iso8601(3),
23
+ version: 1,
24
+ publisher: config.publisher,
25
+ data: data,
26
+ request_id: request_id,
27
+ group_id: group_id,
28
+ deduplication_id: deduplication_id
29
+ }
30
+
31
+ Messages::V1::Command.wrap(params.compact)
32
+ end
33
+
34
+ def command_v2(type, subject:, object:, data: {}, request_id: nil, group_id: nil, deduplication_id: nil, uuid: SecureRandom.uuid)
35
+ params = {
36
+ uuid: uuid,
37
+ type: type,
38
+ subject: subject,
39
+ object: object,
40
+ sent_at: Time.now.iso8601(3),
41
+ version: 2,
42
+ publisher: config.publisher,
43
+ data: data,
44
+ request_id: request_id,
45
+ group_id: group_id,
46
+ deduplication_id: deduplication_id
47
+ }
48
+
49
+ Messages::V2::Command.wrap(params.compact)
50
+ end
51
+ end
52
+ end
53
+ end