pubsub_on_rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e5aeb57501923d0faf35b735734647ab6880660059475126dc4aae2bce6f6d59
4
+ data.tar.gz: 7da45ebcbc7c928f93c4960c02bed54585dfe3bfa9c91906bba7070e8f8b45a5
5
+ SHA512:
6
+ metadata.gz: 730b60a32e1f9873e783f9f029f60f1cf9ec105d55e001afba968dd80eee794f74f2f685fe944419780dc381d64a43157f6f51c660022d70766a1d9b7532fe94
7
+ data.tar.gz: 69c30741019142f5124e9e98f58723efad81fb74ec918a1ffd7a27822dedd66c74f5db888ac4c97f5cf0296ad84e19cc1ceae46ad1858730740048d666bad954
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,17 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pubsub_on_rails (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+
10
+ PLATFORMS
11
+ ruby
12
+
13
+ DEPENDENCIES
14
+ pubsub_on_rails!
15
+
16
+ BUNDLED WITH
17
+ 1.16.1
@@ -0,0 +1,21 @@
1
+ # MIT LICENSE
2
+
3
+ Copyright (c) 2019 Stevo <b.kosmowski@selleo.com>
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.
@@ -0,0 +1,2 @@
1
+ module PubSub
2
+ end
@@ -0,0 +1,23 @@
1
+ module PubSub
2
+ module Domain
3
+ def method_missing(event_or_method_name, *event_data)
4
+ if handler_name(event_or_method_name)
5
+ const_get(handler_name(event_or_method_name)).new(*event_data).call!
6
+ else
7
+ super
8
+ end
9
+ end
10
+
11
+ def handler_name(event_name)
12
+ return nil unless event_name.to_s.start_with?(/[a-z_]+__/)
13
+ "#{event_name.to_s.camelize}Handler"
14
+ end
15
+
16
+ def respond_to_missing?(event_or_method_name, include_private = false)
17
+ return super unless handler_name(event_or_method_name)
18
+ const_get(handler_name(event_or_method_name))
19
+ rescue NameError
20
+ super
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ module PubSub
2
+ class DomainEvent < Dry::Struct
3
+ include Wisper::Publisher
4
+
5
+ def broadcast!
6
+ broadcast(event_name, attributes)
7
+ end
8
+
9
+ private
10
+
11
+ def event_name
12
+ self.class.name.underscore.sub('/', '__').chomp('_event')
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module PubSub
2
+ class DomainEventHandler
3
+ def initialize(*args)
4
+ @event_data_hash = args.extract_options!
5
+ end
6
+
7
+ def call
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def call!
12
+ call if process_event?
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :event_data_hash
18
+
19
+ def process_event?
20
+ true
21
+ end
22
+
23
+ def event_data
24
+ @event_data ||= OpenStruct.new(event_data_hash)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ require 'pub_sub/event_class_factory'
2
+ require 'pub_sub/event_emission'
3
+
4
+ module PubSub
5
+ module Emit
6
+ def emit(event_name, explicit_payload = {})
7
+ event_class = EventClassFactory.build(
8
+ event_name,
9
+ domain_name: self.class.name.deconstantize.demodulize,
10
+ abstract_event_class: explicit_payload.delete(:abstract_event_class)
11
+ )
12
+
13
+ EventEmission.new(event_class, explicit_payload, self).call
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,59 @@
1
+ module PubSub
2
+ class EventClassFactory
3
+ EventClassDoesNotExist = Class.new(StandardError)
4
+
5
+ def self.build(event_name, domain_name: nil, abstract_event_class: nil)
6
+ new(
7
+ event_name,
8
+ domain_name: domain_name,
9
+ abstract_event_class: abstract_event_class
10
+ ).build_event_class
11
+ end
12
+
13
+ def initialize(event_name, domain_name:, abstract_event_class:)
14
+ @event_name = event_name.to_s
15
+ @abstract_event_class = abstract_event_class
16
+ @domain_name = domain_name
17
+ end
18
+
19
+ def build_event_class
20
+ event_class = event_class_name.safe_constantize
21
+
22
+ return event_class unless event_class.nil?
23
+
24
+ if abstract_event_class.nil?
25
+ raise(EventClassDoesNotExist, event_class_name)
26
+ else
27
+ register_new_event_class
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :event_name, :abstract_event_class, :domain_name
34
+
35
+ def register_new_event_class
36
+ event_class_namespace.const_set(event_class_name.demodulize, Class.new(abstract_event_class))
37
+ end
38
+
39
+ def event_class_namespace
40
+ event_class_name.deconstantize.constantize
41
+ end
42
+
43
+ def event_name_includes_domain?
44
+ event_name.include?('__')
45
+ end
46
+
47
+ def event_name_with_domain
48
+ if event_name_includes_domain?
49
+ event_name.to_s.sub('__', '/')
50
+ else
51
+ "#{domain_name}/#{event_name}"
52
+ end
53
+ end
54
+
55
+ def event_class_name
56
+ @event_class_name ||= "#{event_name_with_domain}_event".classify
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,44 @@
1
+ require 'pub_sub/payload_attribute'
2
+
3
+ module PubSub
4
+ class EventEmission
5
+ EventPayloadArgumentMissing = Class.new(StandardError)
6
+
7
+ def initialize(event_class, explicit_payload, context)
8
+ @event_class = event_class
9
+ @explicit_payload = explicit_payload
10
+ @context = context
11
+ end
12
+
13
+ def call
14
+ event_class.new(full_payload).broadcast!
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :event_class, :explicit_payload, :context
20
+
21
+ def event_payload_attribute_names
22
+ event_class.attribute_names
23
+ end
24
+
25
+ # rubocop:disable Metrics/MethodLength
26
+ def full_payload
27
+ event_payload_attribute_names.each_with_object({}) do |attribute_name, result|
28
+ result[attribute_name] = PayloadAttribute.new(attribute_name, explicit_payload, context).get
29
+ rescue PayloadAttribute::CannotEvaluate => cannot_evaluate_error
30
+ if event_class.schema[attribute_name.to_sym].default?
31
+ next
32
+ else
33
+ raise(
34
+ EventPayloadArgumentMissing,
35
+ "Event [#{event_class.name}] expects [#{attribute_name}] payload attribute to be" \
36
+ " either exposed as [#{cannot_evaluate_error.message}] method in emitting object" \
37
+ ' or provided as argument'
38
+ )
39
+ end
40
+ end
41
+ end
42
+ # rubocop:enable Metrics/MethodLength
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ class Linter
2
+ MissingSubscriptions = Class.new(StandardError)
3
+
4
+ def initialize(config)
5
+ @config = config
6
+ end
7
+
8
+ def lint!
9
+ raise MissingSubscriptions, error_message if missing_subscriptions.present?
10
+ end
11
+
12
+ private
13
+
14
+ def error_message
15
+ "The following subscriptions are missing: \n#{missing_subscriptions.join("\n")}"
16
+ end
17
+
18
+ def missing_subscriptions
19
+ (handlers_list - subscriptions_list)
20
+ end
21
+
22
+ attr_reader :config
23
+
24
+ def subscriptions_list
25
+ config.flat_map do |domain_name, subscriptions|
26
+ subscriptions.keys.map do |event_name|
27
+ "#{domain_name}/#{event_name.sub('__', '_')}"
28
+ end
29
+ end
30
+ end
31
+
32
+ def handlers_list
33
+ Dir[Rails.root.join('app/event_handlers/*/*.rb')].map do |file_path|
34
+ file_path.
35
+ sub("#{Rails.root}/app/event_handlers/", '').
36
+ sub('_handler.rb', '')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ module PubSub
2
+ class PayloadAttribute
3
+ CannotEvaluate = Class.new(StandardError)
4
+
5
+ def initialize(attribute_name, explicit_payload, context)
6
+ @attribute_name = attribute_name
7
+ @explicit_payload = explicit_payload
8
+ @context = context
9
+ end
10
+
11
+ def get
12
+ return explicit_payload.fetch(attribute_name) if explicit_payload.key?(attribute_name)
13
+
14
+ identifier? ? context.send(getter_name)&.id : context.send(getter_name)
15
+ rescue NoMethodError
16
+ raise CannotEvaluate, getter_name
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :attribute_name, :explicit_payload, :context
22
+
23
+ def identifier?
24
+ !context.respond_to?(attribute_name, true) && attribute_name.to_s.end_with?('_id')
25
+ end
26
+
27
+ def getter_name
28
+ identifier? ? attribute_name.to_s.chomp('_id') : attribute_name
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ require 'pub_sub/linter'
2
+
3
+ module PubSub
4
+ mattr_accessor :subscriptions
5
+
6
+ class Subscriptions
7
+ include Singleton
8
+
9
+ cattr_accessor :subscriptions_path
10
+ self.subscriptions_path = 'config/subscriptions.yml'
11
+
12
+ def self.load!
13
+ PubSub.subscriptions = Subscriptions.instance
14
+ PubSub.subscriptions.register(:all)
15
+ end
16
+
17
+ def self.lint!
18
+ instance.lint!
19
+ end
20
+
21
+ def lint!
22
+ Linter.new(config).lint!
23
+ end
24
+
25
+ def initialize
26
+ @config = YAML.load_file(self.class.subscriptions_path)
27
+ end
28
+
29
+ def register(scope = :all)
30
+ (scope == :all ? config : config.slice(scope.to_s)).each do |domain_name, subscriptions|
31
+ subscriptions.each do |event_name, subscription_type|
32
+ options = {}
33
+ options[:on] = event_name unless event_name == 'all_events'
34
+ options[:async] = true if subscription_type == 'async'
35
+ options[:broadcaster] = :run_once if subscription_type == 'run_once'
36
+
37
+ Wisper.subscribe("::#{domain_name.camelize}".constantize, options)
38
+ end
39
+ end
40
+ end
41
+
42
+ def clear!
43
+ Wisper.clear
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :config
49
+ end
50
+ end
@@ -0,0 +1,3 @@
1
+ require 'wisper/rspec/matchers'
2
+ require 'pub_sub/testing/event_data_helper'
3
+ require 'pub_sub/testing/subscription_helpers'
@@ -0,0 +1,12 @@
1
+ module PubSub
2
+ module Testing
3
+ module EventDataHelper
4
+ def event_data_for(event_name, **payload)
5
+ EventClassFactory.
6
+ build(event_name, abstract_event_class: payload.delete(:abstract_event_class)).
7
+ new(payload).
8
+ attributes
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module PubSub
2
+ module Testing
3
+ module SubscriptionsHelper
4
+ def with_subscription_to(*domains)
5
+ domains.each do |domain|
6
+ PubSub.subscriptions.register(domain)
7
+ end
8
+ yield
9
+ clear_wisper_subscriptions!
10
+ end
11
+
12
+ def subscribe_logger!
13
+ PubSub.subscriptions.register(:logging)
14
+ end
15
+ module_function :subscribe_logger!
16
+
17
+ def clear_wisper_subscriptions!
18
+ PubSub.subscriptions.clear!
19
+ subscribe_logger!
20
+ end
21
+ module_function :clear_wisper_subscriptions!
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module PubSub
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,9 @@
1
+ require 'wisper'
2
+ require 'wisper/sidekiq'
3
+
4
+ require 'pub_sub'
5
+ require 'pub_sub/emit'
6
+ require 'pub_sub/subscriptions'
7
+ require 'pub_sub/domain'
8
+ require 'pub_sub/domain_event'
9
+ require 'pub_sub/domain_event_handler'
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pubsub_on_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stevo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-struct
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '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'
27
+ - !ruby/object:Gem::Dependency
28
+ name: wisper
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: wisper-sidekiq
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: wisper-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Opinionated publish-subscribe pattern for ruby and rails
70
+ email:
71
+ - b.kosmowski@selleo.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - Gemfile
77
+ - Gemfile.lock
78
+ - LICENSE.md
79
+ - lib/pub_sub.rb
80
+ - lib/pub_sub/domain.rb
81
+ - lib/pub_sub/domain_event.rb
82
+ - lib/pub_sub/domain_event_handler.rb
83
+ - lib/pub_sub/emit.rb
84
+ - lib/pub_sub/event_class_factory.rb
85
+ - lib/pub_sub/event_emission.rb
86
+ - lib/pub_sub/linter.rb
87
+ - lib/pub_sub/payload_attribute.rb
88
+ - lib/pub_sub/subscriptions.rb
89
+ - lib/pub_sub/testing.rb
90
+ - lib/pub_sub/testing/event_data_helper.rb
91
+ - lib/pub_sub/testing/subscription_helpers.rb
92
+ - lib/pub_sub/version.rb
93
+ - lib/pubsub_on_rails.rb
94
+ homepage: https://github.com/Selleo/pubsub_on_rails
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.0.3
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Opinionated publish-subscribe pattern for ruby and rails
117
+ test_files: []