icalia-sdk-event-notification 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dd944c7a944c0dcdaa380b7bc2105cb46d31d04047309e997b46eff3b773e3f1
4
+ data.tar.gz: 9a57249cdd95b40398fd624d9295ad8a5cacbe55d0629709dc05d93c31124b7e
5
+ SHA512:
6
+ metadata.gz: 0db9bd4994ad6dafbfb7133dfb800884e95614e2f28aa55d4e766984034aabdb168349a6071ff8bae0f7cf4d407cf32cf16dbc78ee574d444cbda6c81fab90ca
7
+ data.tar.gz: fd3672441acd33d6aa469801f5bfc2af7355db9b360efc30018edbbf5d4c693ea39ee197b4fe5f2949f60bfa24d48f37af54326c27af54ff700712b5fbc0ffda
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.3
7
+ before_install: gem install bundler -v 1.17.2
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at TODO: Write your email address. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in icalia-sdk-events.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Icalia::Event via Notifications
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be
4
+ able to package up your Ruby library into a gem. Put your Ruby code in the file
5
+ `lib/icalia-sdk-event-notification`. To experiment with that code, run
6
+ `bin/console` for an interactive prompt.
7
+
8
+ TODO: Delete this and the text above, and describe your gem
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'icalia-sdk-event-notification'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install icalia-sdk-events
25
+
26
+ ## Usage
27
+
28
+ TODO: Write usage instructions here
29
+
30
+ ## Development
31
+
32
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
33
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
34
+ prompt that will allow you to experiment.
35
+
36
+ To install this gem onto your local machine, run `bundle exec rake install`. To
37
+ release a new version, update the version number in `version.rb`, and then run
38
+ `bundle exec rake release`, which will create a git tag for the version, push
39
+ git commits and tags, and push the `.gem` file to
40
+ [rubygems.org](https://rubygems.org).
41
+
42
+ ## Contributing
43
+
44
+ Bug reports and pull requests are welcome on GitHub at
45
+ https://github.com/IcaliaLabs/icalia-sdk-ruby. This project is intended to be a
46
+ safe, welcoming space for collaboration, and contributors are expected to adhere
47
+ to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
48
+
49
+ ## License
50
+
51
+ The gem is available as open source under the terms of the
52
+ [MIT License](https://opensource.org/licenses/MIT).
53
+
54
+ ## Code of Conduct
55
+
56
+ Everyone interacting in the Icalia::Sdk::Events project’s codebases, issue
57
+ trackers, chat rooms and mailing lists is expected to follow the
58
+ [code of conduct](https://github.com/IcaliaLabs/icalia-sdk-ruby/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'icalia-sdk-events'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,54 @@
1
+
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'icalia-sdk-event-notification/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'icalia-sdk-event-notification'
8
+ spec.version = Icalia::Event::NOTIFICATION_VERSION
9
+ spec.authors = ['Roberto Quintanilla']
10
+ spec.email = %w[vov@icalialabs.com]
11
+
12
+ spec.summary = 'Icalia SDK for Ruby - Events via Message Broker'
13
+ spec.description = 'Official AWS Ruby gem for Icalia Events via Message Broker' \
14
+ 'This gem is part of the Icalia SDK for Ruby.'
15
+ spec.homepage = 'https://github.com/IcaliaLabs/icalia-sdk-ruby'
16
+ spec.license = 'MIT'
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the
19
+ # 'allowed_push_host'
20
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
21
+ if spec.respond_to?(:metadata)
22
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
23
+
24
+ spec.metadata['homepage_uri'] = spec.homepage
25
+ spec.metadata['source_code_uri'] = 'https://github.com/IcaliaLabs/icalia-sdk-ruby'
26
+ spec.metadata['changelog_uri'] = 'https://github.com/IcaliaLabs/icalia-sdk-ruby'
27
+ else
28
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
29
+ 'public gem pushes.'
30
+ end
31
+
32
+ # Specify which files should be added to the gem when it is released.
33
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
34
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
35
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
36
+ end
37
+ spec.bindir = 'exe'
38
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
39
+ spec.require_paths = %w[lib]
40
+
41
+ spec.add_dependency 'icalia-sdk-event-core',
42
+ Icalia::Event::NOTIFICATION_VERSION
43
+
44
+ # We require mattr_reader to be able to accept a `default` value, which is
45
+ # only supported on Rails ActiveSupport >= 5.2.0:
46
+ spec.add_dependency 'activesupport', '~> 5.2', '>= 5.2.0'
47
+
48
+ spec.add_dependency 'aws-sdk-sqs', '~> 1.20'
49
+ spec.add_dependency 'aws-sdk-sns', '~> 1.19'
50
+
51
+ spec.add_development_dependency 'bundler', '~> 1.17'
52
+ spec.add_development_dependency 'rake', '~> 10.0'
53
+ spec.add_development_dependency 'rspec', '~> 3.0'
54
+ end
data/lib/.DS_Store ADDED
Binary file
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Icalia::Event::EventTriggeringRecord
4
+ #
5
+ # Defines methods & behavior of ActiveRecord models that trigger events to the
6
+ # Icalia Event Bus whenever something changes on it.
7
+ #
8
+ # The specifics on how the event is generated is left to the specifics of the
9
+ # app.
10
+ module Icalia::Event
11
+ module EventTriggeringRecord
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ mattr_reader :delayed_icalia_event_publishing, default: true
16
+
17
+ after_commit :publish_create_to_icalia_events, on: :create
18
+ after_commit :publish_update_to_icalia_events, on: :update
19
+ after_commit :publish_destroy_to_icalia_events, on: :destroy
20
+
21
+ delegate :icalia_event_class_name,
22
+ :icalia_event_publisher_class,
23
+ :icalia_events_publisher_method,
24
+ :delayed_icalia_event_publishing?,
25
+ to: :class
26
+ end
27
+
28
+ module ClassMethods
29
+ def icalia_events_publisher_method
30
+ return :perform_now unless delayed_icalia_event_publishing?
31
+ :perform_later
32
+ end
33
+
34
+ def delayed_icalia_event_publishing?
35
+ delayed_icalia_event_publishing == true
36
+ end
37
+
38
+ def publish_to_icalia_events_immediately!
39
+ class_variable_set :@@delayed_icalia_event_publishing, false
40
+ end
41
+
42
+ def publish_to_icalia_events_later!
43
+ class_variable_set :@@delayed_icalia_event_publishing, true
44
+ end
45
+
46
+ def icalia_event_publishing_enabled?
47
+ :publish_to_icalia_events.in? self
48
+ ._commit_callbacks
49
+ .select { |callback| callback.kind.eql?(:after) }
50
+ .map(&:filter)
51
+ end
52
+
53
+ def icalia_event_publishing_disabled?
54
+ !icalia_event_publishing_enabled?
55
+ end
56
+
57
+ def disable_icalia_event_publishing
58
+ skip_callback(:commit, :after, :publish_to_icalia_events)
59
+ end
60
+
61
+ def enable_icalia_event_publishing
62
+ set_callback(:commit, :after, :publish_to_icalia_events)
63
+ end
64
+
65
+ def without_icalia_event_publishing(&_block)
66
+ disable_icalia_event_publishing if icalia_event_publishing_enabled?
67
+ result = yield self
68
+ enable_icalia_event_publishing if icalia_event_publishing_disabled?
69
+ result
70
+ end
71
+
72
+ def icalia_event_class_name
73
+ # The default behavior is to return the class name + 'Event':
74
+ "#{name}Event"
75
+ end
76
+
77
+ def icalia_event_publisher_class_name
78
+ "IcaliaEventPublishing::Publish#{icalia_event_class_name}Job"
79
+ end
80
+
81
+ def icalia_event_publisher_class
82
+ icalia_event_publisher_class_name.safe_constantize
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def publish_to_icalia_events(action)
89
+ publisher_class = icalia_event_publisher_class
90
+ return unless publisher_class.present?
91
+ publisher_class.send icalia_events_publisher_method, action.to_s, self
92
+ end
93
+
94
+ def publish_create_to_icalia_events
95
+ publish_to_icalia_events 'created'
96
+ end
97
+
98
+ def publish_update_to_icalia_events
99
+ publish_to_icalia_events 'updated'
100
+ end
101
+
102
+ def publish_destroy_to_icalia_events
103
+ publish_to_icalia_events 'destroyed'
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalia::Event
4
+ #= Icalia::Event::Notification
5
+ class Notification
6
+ attr_reader :data,
7
+ :topic,
8
+ :event_data,
9
+ :included_data,
10
+ :metadata
11
+
12
+ def initialize(raw_notification)
13
+ @data = parse_raw_notification raw_notification
14
+ end
15
+
16
+ def topic_arn; data.fetch :topic_arn; end
17
+ def topic; @topic ||= topic_arn.split(':')[5..-1].join(':'); end
18
+
19
+ def metadata; @metadata ||= data.dig :message, :meta; end
20
+ def emitter; metadata.dig :emitter; end
21
+
22
+ def message; data.fetch :message; end
23
+
24
+ def event_data; @event_data ||= deserialize_data(message.fetch(:data)); end
25
+ def action; event_data.fetch(:action, 'message'); end
26
+
27
+ def included_data
28
+ @included_data ||= message
29
+ .fetch(:included)
30
+ .each_with_object({}) do |data, included|
31
+ included[data.slice(:id, :type)] = deserialize_data(data)
32
+ end
33
+ end
34
+
35
+ def fetch_data(property_name)
36
+ fetched_property = fetch_attribute property_name
37
+ return fetched_property if fetched_property.present?
38
+
39
+ fetch_relationship property_name
40
+ end
41
+
42
+ delegate :parse_raw_notification, :deserialize_data, to: :class
43
+
44
+ class << self
45
+ delegate :decode, to: ActiveSupport::JSON, prefix: :json
46
+
47
+ def parse_raw_notification(raw_notification)
48
+ json_decode(raw_notification)
49
+ .transform_keys(&:underscore)
50
+ .tap { |parsed| parsed['message'] = json_decode parsed['message'] }
51
+ .with_indifferent_access
52
+ end
53
+
54
+ def deserialize_data(data)
55
+ klass = data.fetch(:type).underscore.classify
56
+ deserializer = "Icalia::Event::Deserializable#{klass}".constantize
57
+ deserializer.call(data).with_indifferent_access
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def fetch_attribute(attribute_name)
64
+ message.dig(:data, :attributes, attribute_name) ||
65
+ message.dig(:data, :attributes, attribute_name.to_s.dasherize)
66
+ end
67
+
68
+ def fetch_relationship(relationship_name)
69
+ rel = message.dig(:data, :relationships, relationship_name) ||
70
+ message.dig(:data, :relationships, relationship_name.to_s.dasherize)
71
+ return unless rel.present?
72
+ included_data[rel.fetch(:data)]
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalia::Event
4
+ class Publisher
5
+
6
+ class UnknownTopicError < StandardError
7
+ end
8
+
9
+ class << self
10
+ def instance
11
+ @instance ||= new
12
+ end
13
+
14
+ delegate :publish, to: :instance
15
+ end
16
+
17
+ attr_reader :sns_client
18
+
19
+ def initialize
20
+ @sns_client = Aws::SNS::Resource.new Icalia::Event.sns_client_options
21
+ end
22
+
23
+ def publish(topic_name, data, json_data: true)
24
+ raise UnknownTopicError, "Topic '#{topic_name}' does not exist" \
25
+ unless topic_exists?(topic_name)
26
+
27
+ topic = get_topic topic_name
28
+ topic.publish message: (json_data ? json_encode(data) : data)
29
+ end
30
+
31
+ delegate :encode, to: ActiveSupport::JSON, prefix: :json
32
+
33
+ protected
34
+
35
+ def get_topic(topic_name)
36
+ sns_client.topics.detect do |topic|
37
+ topic.arn.split(':').last == topic_name
38
+ end
39
+ end
40
+
41
+ def topic_exists?(topic_name)
42
+ get_topic(topic_name).present?
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalia::Event::Shoryuken::CLI
4
+ class Base < Thor
5
+ no_commands do
6
+ def print_table(entries)
7
+ column_sizes = print_columns_size(entries)
8
+
9
+ entries.map do |entry|
10
+ puts entry.map.with_index { |e, i| print_format_column(e, column_sizes[i]) }.join
11
+ end
12
+ end
13
+
14
+ def print_columns_size(entries)
15
+ column_sizes = Hash.new(0)
16
+
17
+ entries.each do |entry|
18
+ entry.each_with_index do |e, i|
19
+ e = e.to_s
20
+ column_sizes[i] = e.size if column_sizes[i] < e.size
21
+ end
22
+ end
23
+
24
+ column_sizes
25
+ end
26
+
27
+ def print_format_column(column, size)
28
+ size_with_padding = size + 4
29
+ column = column.to_s.ljust(size_with_padding)
30
+ column
31
+ end
32
+
33
+ def fail_task(msg, quit = true)
34
+ say "[FAIL] #{msg}", :red
35
+ exit(1) if quit
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'aws-sdk-core'
5
+
6
+ module Icalia::Event::Shoryuken::CLI
7
+ class Runner < Base
8
+ default_task :start
9
+
10
+ register Icalia::Event::Shoryuken::CLI::SQS,
11
+ 'sqs',
12
+ 'sqs COMMAND',
13
+ 'SQS commands'
14
+
15
+ desc 'start', 'Starts shoryuken'
16
+ method_option :concurrency, aliases: '-c', type: :numeric, desc: 'Processor threads to use'
17
+ method_option :daemon, aliases: '-d', type: :boolean, desc: 'Daemonize process'
18
+ method_option :queues, aliases: '-q', type: :array, desc: 'Queues to process with optional weights'
19
+ method_option :require, aliases: '-r', type: :string, desc: 'Dir or path of the workers'
20
+ method_option :timeout, aliases: '-t', type: :numeric, desc: 'Hard shutdown timeout'
21
+ method_option :config, aliases: '-C', type: :string, desc: 'Path to config file'
22
+ method_option :config_file, type: :string, desc: 'Path to config file (backwards compatibility)'
23
+ method_option :rails, aliases: '-R', type: :boolean, desc: 'Load Rails'
24
+ method_option :logfile, aliases: '-L', type: :string, desc: 'Path to logfile'
25
+ method_option :pidfile, aliases: '-P', type: :string, desc: 'Path to pidfile'
26
+ method_option :verbose, aliases: '-v', type: :boolean, desc: 'Print more verbose output'
27
+ method_option :delay, aliases: '-D', type: :numeric, desc: 'Number of seconds to pause fetching from an empty queue'
28
+
29
+ def start
30
+ opts = options.to_h.symbolize_keys
31
+
32
+ say '[DEPRECATED] Please use --config instead of --config-file', :yellow if opts[:config_file]
33
+
34
+ opts[:config_file] = opts.delete(:config) if opts[:config]
35
+
36
+ # Keep compatibility with old CLI queue format
37
+ opts[:queues] = opts[:queues].reject(&:empty?).map { |q| q.split(',') } if opts[:queues]
38
+
39
+ fail_task "You should set a logfile if you're going to daemonize" if opts[:daemon] && opts[:logfile].nil?
40
+
41
+ Shoryuken::Runner.instance.run(opts.freeze)
42
+ end
43
+
44
+ desc 'version', 'Prints version'
45
+ def version
46
+ say "Shoryuken #{Shoryuken::VERSION}"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ # rubocop:disable Metrics/BlockLength
6
+ module Icalia::Event::Shoryuken::CLI
7
+ class SQS < Base
8
+ namespace :sqs
9
+
10
+ no_commands do
11
+ def normalize_dump_message(message)
12
+ # symbolize_keys is needed for keeping it compatible with `requeue`
13
+ attributes = message[:attributes].symbolize_keys
14
+ {
15
+ id: message[:message_id],
16
+ message_body: message[:body],
17
+ message_attributes: message[:message_attributes],
18
+ message_deduplication_id: attributes[:MessageDeduplicationId],
19
+ message_group_id: attributes[:MessageGroupId]
20
+ }
21
+ end
22
+
23
+ def sqs
24
+ @_sqs ||= Aws::SQS::Client.new Icalia::Event.sqs_client_options
25
+ end
26
+
27
+ def find_queue_url(queue_name)
28
+ sqs.get_queue_url(queue_name: queue_name).queue_url
29
+ rescue Aws::SQS::Errors::NonExistentQueue
30
+ fail_task "The specified queue #{queue_name} does not exist"
31
+ end
32
+
33
+ def batch_delete(url, messages)
34
+ messages.to_a.flatten.each_slice(10) do |batch|
35
+ sqs.delete_message_batch(
36
+ queue_url: url,
37
+ entries: batch.map { |message| { id: message.message_id, receipt_handle: message.receipt_handle } }
38
+ ).failed.any? do |failure|
39
+ say(
40
+ "Could not delete #{failure.id}, code: #{failure.code}, message: #{failure.message}, sender_fault: #{failure.sender_fault}",
41
+ :yellow
42
+ )
43
+ end
44
+ end
45
+ end
46
+
47
+ def batch_send(url, messages, messages_per_batch = 10)
48
+ messages.to_a.flatten.map(&method(:normalize_dump_message)).each_slice(messages_per_batch) do |batch|
49
+ sqs.send_message_batch(queue_url: url, entries: batch).failed.any? do |failure|
50
+ say "Could not requeue #{failure.id}, code: #{failure.code}", :yellow
51
+ end
52
+ end
53
+ end
54
+
55
+ def find_all(url, limit)
56
+ count = 0
57
+ batch_size = limit > 10 ? 10 : limit
58
+
59
+ loop do
60
+ n = limit - count
61
+ batch_size = n if n < batch_size
62
+
63
+ messages = sqs.receive_message(
64
+ queue_url: url,
65
+ max_number_of_messages: batch_size,
66
+ attribute_names: ['All'],
67
+ message_attribute_names: ['All']
68
+ ).messages
69
+
70
+ messages.each { |m| yield m }
71
+
72
+ count += messages.size
73
+
74
+ break if count >= limit
75
+ break if messages.empty?
76
+ end
77
+
78
+ count
79
+ end
80
+
81
+ def list_and_print_queues(urls)
82
+ attrs = %w[QueueArn ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible LastModifiedTimestamp]
83
+
84
+ entries = urls.map { |u| sqs.get_queue_attributes(queue_url: u, attribute_names: attrs).attributes }.map do |q|
85
+ [
86
+ q['QueueArn'].split(':').last,
87
+ q['ApproximateNumberOfMessages'],
88
+ q['ApproximateNumberOfMessagesNotVisible'],
89
+ Time.at(q['LastModifiedTimestamp'].to_i)
90
+ ]
91
+ end
92
+
93
+ entries.unshift(['Queue', 'Messages Available', 'Messages Inflight', 'Last Modified'])
94
+
95
+ print_table(entries)
96
+ end
97
+
98
+ def dump_file(path, queue_name)
99
+ File.join(path, "#{queue_name}-#{Date.today}.jsonl")
100
+ end
101
+ end
102
+
103
+ desc 'ls [QUEUE-NAME-PREFIX]', 'Lists queues'
104
+ method_option :watch, aliases: '-w', type: :boolean, desc: 'watch queues'
105
+ method_option :interval, aliases: '-n', type: :numeric, default: 2, desc: 'watch interval in seconds'
106
+ def ls(queue_name_prefix = '')
107
+ trap('SIGINT', 'EXIT') # expect ctrl-c from loop
108
+
109
+ urls = sqs.list_queues(queue_name_prefix: queue_name_prefix).queue_urls
110
+
111
+ loop do
112
+ list_and_print_queues(urls)
113
+
114
+ break unless options[:watch]
115
+
116
+ sleep options[:interval]
117
+ puts
118
+ end
119
+ end
120
+
121
+ desc 'dump QUEUE-NAME', 'Dumps messages from a queue into a JSON lines file'
122
+ method_option :number, aliases: '-n', type: :numeric, default: Float::INFINITY, desc: 'number of messages to dump'
123
+ method_option :path, aliases: '-p', type: :string, default: './', desc: 'path to save the dump file'
124
+ method_option :delete, aliases: '-d', type: :boolean, default: true, desc: 'delete from the queue'
125
+ def dump(queue_name)
126
+ path = dump_file(options[:path], queue_name)
127
+
128
+ fail_task "File #{path} already exists" if File.exist?(path)
129
+
130
+ url = find_queue_url(queue_name)
131
+
132
+ messages = []
133
+
134
+ file = nil
135
+
136
+ count = find_all(url, options[:number]) do |m|
137
+ file ||= File.open(path, 'w')
138
+
139
+ file.puts(JSON.dump(m.to_h))
140
+
141
+ messages << m if options[:delete]
142
+ end
143
+
144
+ batch_delete(url, messages) if options[:delete]
145
+
146
+ if count.zero?
147
+ say "Queue #{queue_name} is empty", :yellow
148
+ else
149
+ say "Dump saved in #{path} with #{count} messages", :green
150
+ end
151
+ ensure
152
+ file.close if file
153
+ end
154
+
155
+ desc 'requeue QUEUE-NAME PATH', 'Requeues messages from a dump file'
156
+ method_option :batch_size, aliases: '-n', type: :numeric, default: 10, desc: 'number of messages per batch to send'
157
+ def requeue(queue_name, path)
158
+ fail_task "Path #{path} not found" unless File.exist?(path)
159
+
160
+ messages = File.readlines(path).map { |line| JSON.parse(line, symbolize_names: true) }
161
+
162
+ batch_send(find_queue_url(queue_name), messages, options[:batch_size])
163
+
164
+ say "Requeued #{messages.size} messages from #{path} to #{queue_name}", :green
165
+ end
166
+
167
+ desc 'mv QUEUE-NAME-SOURCE QUEUE-NAME-TARGET', 'Moves messages from one queue (source) to another (target)'
168
+ method_option :number, aliases: '-n', type: :numeric, default: Float::INFINITY, desc: 'number of messages to move'
169
+ method_option :delete, aliases: '-d', type: :boolean, default: true, desc: 'delete from the queue'
170
+ def mv(queue_name_source, queue_name_target)
171
+ url_source = find_queue_url(queue_name_source)
172
+ messages = []
173
+
174
+ count = find_all(url_source, options[:number]) do |m|
175
+ messages << m
176
+ end
177
+
178
+ batch_send(find_queue_url(queue_name_target), messages.map(&:to_h))
179
+ batch_delete(url_source, messages) if options[:delete]
180
+
181
+ if count.zero?
182
+ say "Queue #{queue_name_source} is empty", :yellow
183
+ else
184
+ say "Moved #{count} messages from #{queue_name_source} to #{queue_name_target}", :green
185
+ end
186
+ end
187
+
188
+ desc 'purge QUEUE-NAME', 'Deletes the messages in a queue'
189
+ def purge(queue_name)
190
+ sqs.purge_queue(queue_url: find_queue_url(queue_name))
191
+
192
+ say "Purge request sent for #{queue_name}. The message deletion process takes up to 60 seconds", :yellow
193
+ end
194
+
195
+ desc 'create QUEUE-NAME', 'Create a queue'
196
+ method_option :attributes, aliases: '-a', type: :hash, default: {}, desc: 'queue attributes'
197
+ def create(queue_name)
198
+ attributes = options[:attributes]
199
+ attributes['FifoQueue'] ||= 'true' if queue_name.end_with?('.fifo')
200
+
201
+ queue_url = sqs.create_queue(queue_name: queue_name, attributes: attributes).queue_url
202
+
203
+ say "Queue #{queue_name} was successfully created. Queue URL #{queue_url}", :green
204
+ end
205
+
206
+ desc 'delete QUEUE-NAME', 'delete a queue'
207
+ def delete(queue_name)
208
+ sqs.delete_queue(queue_url: find_queue_url(queue_name))
209
+
210
+ say "Queue #{queue_name} was successfully delete", :green
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalia::Event::Shoryuken
4
+ module CLI
5
+ autoload :Base, 'icalia-sdk-event-notification/shoryuken/cli/base'
6
+ autoload :Runner, 'icalia-sdk-event-notification/shoryuken/cli/runner'
7
+ autoload :SQS, 'icalia-sdk-event-notification/shoryuken/cli/sqs'
8
+ end
9
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalia::Event::Shoryuken
4
+ class Worker
5
+ include Shoryuken::Worker
6
+ include Icalia::Event::TopicMessageProcessing
7
+
8
+ shoryuken_options queue: 'icalia_events', auto_delete: true
9
+
10
+ delegate :logger, to: Shoryuken
11
+
12
+ def perform(_sqs_msg, raw_notification)
13
+ notification = Icalia::Event::Notification.new raw_notification
14
+
15
+ return unless process_notification?(notification)
16
+
17
+ logger.info "Processing notification from topic"\
18
+ " '#{notification.topic}'" #: #{notification.inspect}"
19
+
20
+ # If there was no action (some events may not have an action!), we'll
21
+ # run the `process_message` method instead:
22
+ action_name = notification.action || 'default'
23
+ send "process_#{action_name.underscore}".to_sym, notification
24
+ end
25
+
26
+ delegate :process_notification,
27
+ :process_notification?,
28
+ :define_noop_action_processor,
29
+ to: :class
30
+
31
+ class ActionProcessorPatternTest
32
+ attr_reader :action_name
33
+
34
+ def initialize(method_sym)
35
+ if method_sym.to_s =~ /\Aprocess_(\w+)\z/
36
+ @action_name = $1.to_sym
37
+ end
38
+ end
39
+
40
+ def match?; @action_name != nil; end
41
+ end
42
+
43
+ class << self
44
+ def method_missing(method_sym, *arguments, &block)
45
+ processor_test = ActionProcessorPatternTest.new(method_sym)
46
+ if processor_test.match?
47
+ define_noop_action_processor(method_sym, processor_test.action_name)
48
+ send(method_sym, *arguments)
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ def define_noop_action_processor(action_processor_method, action_name)
55
+ define_singleton_method action_processor_method do |*args|
56
+ logger.info "#{name} does not know how to process #{action_name}" \
57
+ " - Ignoring message"
58
+ end
59
+ delegate action_processor_method, to: :class
60
+ end
61
+
62
+ def process_data?(data); true; end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalia::Event::Shoryuken
4
+ class WorkerRegistry < Shoryuken::DefaultWorkerRegistry
5
+ def fetch_worker(queue, message)
6
+ worker = super
7
+ return worker if worker.present?
8
+
9
+ require_jobs if empty_mappings?
10
+
11
+ topic_name = extract_topic_name(message)
12
+
13
+ worker_class = Icalia::Event::TopicMessageProcessing
14
+ .worker_mappings[topic_name]
15
+
16
+ return worker_class.new if worker_class.present?
17
+ end
18
+
19
+ protected
20
+
21
+ delegate :extract_topic_name, :require_jobs, to: :class
22
+ delegate :empty_mappings?, to: Icalia::Event::TopicMessageProcessing
23
+
24
+ def self.require_jobs
25
+ path = Rails.root.join('app', 'jobs', '**', '*_job.rb')
26
+ Dir[path].each {|file| require file }
27
+ end
28
+
29
+ def self.extract_topic_name(message)
30
+ extract_metadata(message)['topic_arn'].split(':')[5..-1].join(':')
31
+ end
32
+
33
+ def self.extract_metadata(message)
34
+ extract_body(message)
35
+ .except('Message')
36
+ .transform_keys(&:underscore)
37
+ .with_indifferent_access
38
+ end
39
+
40
+ def self.extract_body(message)
41
+ ActiveSupport::JSON.decode message.body
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalia::Event
4
+ module Shoryuken
5
+ autoload :Worker, 'icalia-sdk-event-notification/shoryuken/worker'
6
+ autoload :WorkerRegistry, 'icalia-sdk-event-notification/shoryuken/worker_registry'
7
+ autoload :CLI, 'icalia-sdk-event-notification/shoryuken/cli'
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalia::Event
4
+ module TopicMessageProcessing
5
+ extend ActiveSupport::Concern
6
+
7
+ mattr_reader :worker_mappings, default: {}
8
+
9
+ def self.empty_mappings?
10
+ Icalia::Event::TopicMessageProcessing.worker_mappings.empty?
11
+ end
12
+
13
+ module ClassMethods
14
+ def process_from_topics(*topic_names)
15
+ topic_names.each do |topic_name|
16
+ Icalia::Event::TopicMessageProcessing
17
+ .worker_mappings[topic_name] = self
18
+ end
19
+ end
20
+
21
+ alias process_from_topic process_from_topics
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalia
4
+ module Event
5
+ NOTIFICATION_VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ module Icalia
7
+ module Events
8
+ autoload :Version, 'icalia-sdk-event-notification/version'
9
+ autoload :Publisher, 'icalia-sdk-event-notification/publisher'
10
+ autoload :Notification, 'icalia-sdk-event-notification/notification'
11
+
12
+ autoload :Shoryuken, 'icalia-sdk-event-notification/shoryuken'
13
+
14
+ autoload :EventTriggeringRecord,
15
+ 'icalia-sdk-event-notification/event_triggering_record'
16
+
17
+ autoload :TopicMessageProcessing,
18
+ 'icalia-sdk-event-notification/topic_message_processing'
19
+
20
+ def self.generic_aws_client_options
21
+ {
22
+ region: ENV.fetch('AWS_REGION', 'us-west-2'),
23
+ access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'),
24
+ secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY')
25
+ }
26
+ end
27
+
28
+ def self.sqs_client_options
29
+ options = Icalia::Event.generic_aws_client_options
30
+ return options if ENV['AWS_SQS_ENDPOINT'].blank?
31
+
32
+ options.merge endpoint: ENV['AWS_SQS_ENDPOINT'], verify_checksums: false
33
+ end
34
+
35
+ def self.sns_client_options
36
+ options = Icalia::Event.generic_aws_client_options
37
+ return options if ENV['AWS_SNS_ENDPOINT'].blank?
38
+
39
+ options.merge endpoint: ENV['AWS_SQS_ENDPOINT']
40
+ end
41
+
42
+ mattr_reader :deferred_publishing, default: true
43
+ end
44
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: icalia-sdk-event-notification
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Roberto Quintanilla
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-08-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: icalia-sdk-event-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.2.0
34
+ - - "~>"
35
+ - !ruby/object:Gem::Version
36
+ version: '5.2'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 5.2.0
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.2'
47
+ - !ruby/object:Gem::Dependency
48
+ name: aws-sdk-sqs
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.20'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.20'
61
+ - !ruby/object:Gem::Dependency
62
+ name: aws-sdk-sns
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.19'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.19'
75
+ - !ruby/object:Gem::Dependency
76
+ name: bundler
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.17'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.17'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '10.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '10.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.0'
117
+ description: Official AWS Ruby gem for Icalia Events via Message BrokerThis gem is
118
+ part of the Icalia SDK for Ruby.
119
+ email:
120
+ - vov@icalialabs.com
121
+ executables: []
122
+ extensions: []
123
+ extra_rdoc_files: []
124
+ files:
125
+ - ".rspec"
126
+ - ".travis.yml"
127
+ - CODE_OF_CONDUCT.md
128
+ - Gemfile
129
+ - LICENSE.txt
130
+ - README.md
131
+ - Rakefile
132
+ - bin/console
133
+ - bin/setup
134
+ - icalia-sdk-event-notification.gemspec
135
+ - lib/.DS_Store
136
+ - lib/icalia-sdk-event-notification.rb
137
+ - lib/icalia-sdk-event-notification/event_triggering_record.rb
138
+ - lib/icalia-sdk-event-notification/notification.rb
139
+ - lib/icalia-sdk-event-notification/publisher.rb
140
+ - lib/icalia-sdk-event-notification/shoryuken.rb
141
+ - lib/icalia-sdk-event-notification/shoryuken/cli.rb
142
+ - lib/icalia-sdk-event-notification/shoryuken/cli/base.rb
143
+ - lib/icalia-sdk-event-notification/shoryuken/cli/runner.rb
144
+ - lib/icalia-sdk-event-notification/shoryuken/cli/sqs.rb
145
+ - lib/icalia-sdk-event-notification/shoryuken/worker.rb
146
+ - lib/icalia-sdk-event-notification/shoryuken/worker_registry.rb
147
+ - lib/icalia-sdk-event-notification/topic_message_processing.rb
148
+ - lib/icalia-sdk-event-notification/version.rb
149
+ homepage: https://github.com/IcaliaLabs/icalia-sdk-ruby
150
+ licenses:
151
+ - MIT
152
+ metadata:
153
+ allowed_push_host: https://rubygems.org
154
+ homepage_uri: https://github.com/IcaliaLabs/icalia-sdk-ruby
155
+ source_code_uri: https://github.com/IcaliaLabs/icalia-sdk-ruby
156
+ changelog_uri: https://github.com/IcaliaLabs/icalia-sdk-ruby
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubygems_version: 3.0.3
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Icalia SDK for Ruby - Events via Message Broker
176
+ test_files: []