realm-core 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+ require 'realm/query_handler'
5
+ require 'realm/command_handler'
6
+ require 'realm/domain_resolver'
7
+ require 'realm/error'
8
+ require 'realm/mixins/dependency_injection'
9
+ require 'realm/persistence/repository_query_handler_adapter'
10
+
11
+ module Realm
12
+ class Dispatcher
13
+ include Mixins::DependencyInjection
14
+ inject DomainResolver
15
+
16
+ def initialize(runtime)
17
+ @runtime = runtime
18
+ @threads = []
19
+ end
20
+
21
+ def query(identifier, params = {})
22
+ callable, action = get_callable(QueryHandler, identifier)
23
+ callable, action = get_repo_adapter(identifier) unless callable
24
+ raise QueryHandlerMissing, identifier unless callable
25
+
26
+ dispatch(callable, action, params)
27
+ end
28
+
29
+ def run(identifier, params = {})
30
+ callable, action = get_callable(CommandHandler, identifier)
31
+ raise CommandHandlerMissing, identifier unless callable
32
+
33
+ dispatch(callable, action, params)
34
+ end
35
+
36
+ def run_as_job(identifier, params = {})
37
+ callable, action = get_callable(CommandHandler, identifier)
38
+ raise CommandHandlerMissing, identifier unless callable
39
+
40
+ @threads.delete_if(&:stop?)
41
+ @threads << Thread.new do # TODO: back by SQS
42
+ result = dispatch(callable, action, params)
43
+ yield result if block_given?
44
+ end
45
+ end
46
+
47
+ # Blocks until all jobs are finished. Useful mainly in tests.
48
+ def wait_for_jobs
49
+ @threads.each(&:join)
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :runtime
55
+
56
+ def dispatch(callable, action, params)
57
+ arguments = { action: action, params: params, runtime: runtime }.compact
58
+ callable.(**arguments)
59
+ end
60
+
61
+ def get_callable(type, identifier)
62
+ return [identifier, nil] if identifier.respond_to?(:call)
63
+
64
+ domain_resolver.get_handler_with_action(type, identifier)
65
+ end
66
+
67
+ def get_repo_adapter(identifier)
68
+ parts = identifier.to_s.split('.')
69
+ return [nil, nil] unless parts.size == 2 && runtime&.context&.key?("#{parts[0]}_repo")
70
+
71
+ [Persistence::RepositoryQueryHandlerAdapter.new(runtime.context["#{parts[0]}_repo"]), parts[1].to_sym]
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+ require 'realm/command_handler'
5
+ require 'realm/query_handler'
6
+ require 'realm/event_handler'
7
+ require 'realm/event'
8
+
9
+ module Realm
10
+ class DomainResolver
11
+ DOMAIN_CLASS_TYPES = [CommandHandler, QueryHandler, EventHandler].freeze
12
+
13
+ def initialize(domain_module = nil)
14
+ # nil domain resolver is useful in tests
15
+ @domain_module = domain_module
16
+ @index = DOMAIN_CLASS_TYPES.map { |t| [t, {}] }.to_h
17
+ scan(domain_module) if domain_module
18
+ end
19
+
20
+ def get_handler_with_action(type, identifier)
21
+ handlers = @index[type]
22
+ return [handlers[identifier], :handle] if handlers.key?(identifier)
23
+
24
+ # The last part of the identifier can action method name inside the handler
25
+ parts = identifier.split('.')
26
+ handler_part = parts[..-2].join('.')
27
+ action = parts[-1]
28
+ return [handlers[handler_part], action.to_sym] if handlers.key?(handler_part)
29
+
30
+ [nil, nil]
31
+ end
32
+
33
+ def all_event_handlers
34
+ @index[EventHandler].values
35
+ end
36
+
37
+ private
38
+
39
+ def scan(root_module)
40
+ root_module_str = root_module.to_s
41
+ root_module.constants.each do |const_sym|
42
+ const = root_module.const_get(const_sym)
43
+ next unless const.is_a?(Module) && !(const < Event) && const.to_s.start_with?(root_module_str)
44
+
45
+ type = DOMAIN_CLASS_TYPES.find { |t| const < t }
46
+ next scan(const) unless type
47
+
48
+ register(type, const)
49
+ end
50
+ end
51
+
52
+ def register(type, const)
53
+ # Remove domain module prefix and handler type suffixes
54
+ operation_type = type.to_s.demodulize.sub('Handler', '')
55
+ identifier = const.to_s.gsub(/(^#{@domain_module})|((#{operation_type})?Handlers?)/, '')
56
+ @index[type][identifier.underscore.gsub(%r{(^/+)|(/+$)}, '').gsub(%r{/+}, '.')] = const
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ class Error < StandardError
5
+ def self.[](default_msg)
6
+ Class.new(Realm::Error) do
7
+ define_method(:initialize) do |msg = default_msg|
8
+ super(msg)
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ class QueryHandlerMissing < Error
15
+ def initialize(query_name, msg: "Cannot find handler for query '#{query_name}'")
16
+ super(msg)
17
+ end
18
+ end
19
+
20
+ class CommandHandlerMissing < Error
21
+ def initialize(command_name, msg: "Cannot find handler for command '#{command_name}'")
22
+ super(msg)
23
+ end
24
+ end
25
+
26
+ class CannotHandleAction < Error
27
+ def initialize(handler, action, msg: "#{handler} cannot handle action '#{action}'")
28
+ super(msg)
29
+ end
30
+ end
31
+
32
+ class DependencyMissing < Error
33
+ def initialize(dependency_name, msg: "Dependency '#{dependency_name}' missing in container")
34
+ super(msg)
35
+ end
36
+ end
37
+
38
+ class EventClassMissing < Error
39
+ def initialize(identifier, events_module, msg: "Cannot find event class for #{identifier} in #{events_module}")
40
+ super(msg)
41
+ end
42
+ end
43
+
44
+ class InvalidParams < Error
45
+ def initialize(validation_result, msg: "Validation failed: #{validation_result.errors.to_h}")
46
+ @validation_result = validation_result
47
+ super(msg)
48
+ end
49
+
50
+ def params
51
+ @validation_result.to_h
52
+ end
53
+
54
+ def messages
55
+ @validation_result.errors.to_h
56
+ end
57
+
58
+ def full_messages
59
+ @validation_result.errors(full: true).to_h
60
+ end
61
+ end
62
+
63
+ class UniqueConstraintError < Error; end
64
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'active_support/core_ext/hash'
5
+ require 'active_support/core_ext/string'
6
+ require 'dry-struct'
7
+ require 'realm/types'
8
+
9
+ module Realm
10
+ class Event < Dry::Struct
11
+ T = Realm::Types
12
+
13
+ transform_keys(&:to_sym)
14
+
15
+ attribute :head do
16
+ attribute :id, T::Strict::String
17
+ attribute :triggered_at, T::JSON::Time
18
+ attribute? :version, T::Coercible::String
19
+ attribute? :origin, T::Strict::String
20
+ attribute :correlation_id, T::Strict::String
21
+ attribute? :cause_event_id, T::Strict::String
22
+ attribute? :cause, T::Strict::String
23
+ end
24
+
25
+ class << self
26
+ def new(attributes = {})
27
+ head = {
28
+ id: SecureRandom.uuid,
29
+ correlation_id: SecureRandom.uuid,
30
+ triggered_at: Time.now,
31
+ version: 1, # until we need breaking change (anything except adding attribute) all events are version 1
32
+ }.merge(attributes.fetch(:head, {}))
33
+ body = attributes[:body] || attributes.except(:head)
34
+ super({ head: head }.merge(body.empty? ? {} : { body: body }))
35
+ end
36
+
37
+ def type
38
+ @type ||= name.demodulize.sub('Event', '').underscore
39
+ end
40
+
41
+ protected
42
+
43
+ def body_struct(&block)
44
+ attribute(:body, &block)
45
+ end
46
+ end
47
+
48
+ def type
49
+ self.class.type
50
+ end
51
+
52
+ def to_json(*args)
53
+ JSON.generate(to_h, *args)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'realm/error'
4
+ require 'realm/event'
5
+
6
+ module Realm
7
+ class EventFactory
8
+ def initialize(events_module)
9
+ @events_module = events_module
10
+ end
11
+
12
+ def create_event(event_type, correlate: nil, cause: nil, **attributes)
13
+ head = enhance_head(attributes.fetch(:head, {}), correlate: correlate, cause: cause)
14
+ body = attributes.fetch(:body, attributes.except(:head))
15
+
16
+ event_class_for(event_type).new(head: head, body: body)
17
+ end
18
+
19
+ def event_class_for(event_type)
20
+ return event_type if event_type.respond_to?(:new)
21
+
22
+ class_name = "#{@events_module}::#{event_type.to_s.camelize}"
23
+ klass = class_name.safe_constantize || "#{class_name}Event".safe_constantize
24
+ return klass if klass
25
+
26
+ raise EventClassMissing.new(event_type, @events_module)
27
+ end
28
+
29
+ private
30
+
31
+ def enhance_head(head, correlate:, cause:)
32
+ head[:correlation_id] = correlate.head.correlation_id if correlate
33
+ if cause.is_a?(Event)
34
+ head[:cause_event_id] = cause.head.id
35
+ head[:correlation_id] ||= cause.head.correlation_id
36
+ elsif cause
37
+ head[:cause] = cause
38
+ end
39
+ head
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+ require 'realm/error'
5
+ require 'realm/mixins/context_injection'
6
+ require 'realm/mixins/reactive'
7
+ require 'realm/mixins/repository_helper'
8
+ require 'realm/mixins/aggregate_member'
9
+
10
+ module Realm
11
+ class EventHandler
12
+ extend Mixins::ContextInjection::ClassMethods
13
+ include Mixins::AggregateMember
14
+ include Mixins::Reactive
15
+ include Mixins::RepositoryHelper
16
+
17
+ class RuntimeBound
18
+ delegate :identifier, :event_types, to: :@handler_class
19
+
20
+ def initialize(handler_class, runtime)
21
+ @handler_class = handler_class
22
+ @runtime = runtime
23
+ end
24
+
25
+ def call(event)
26
+ @handler_class.(event, runtime: @runtime.session(cause: event))
27
+ end
28
+ end
29
+
30
+ class << self
31
+ attr_reader :trigger_mapping, :event_namespace
32
+
33
+ def bind_runtime(runtime)
34
+ RuntimeBound.new(self, runtime)
35
+ end
36
+
37
+ def call(event, runtime:)
38
+ new(runtime: runtime).(event)
39
+ end
40
+
41
+ def identifier(value = :undefined)
42
+ if value == :undefined
43
+ defined?(@identifier) ? @identifier : name.gsub(/(Domain|EventHandlers?)/, '').underscore.gsub(%r{/+}, '-')
44
+ else
45
+ @identifier = value
46
+ end
47
+ end
48
+
49
+ def event_types
50
+ defined?(@trigger_mapping) ? @trigger_mapping.keys.uniq : []
51
+ end
52
+
53
+ protected
54
+
55
+ def namespace(value)
56
+ @event_namespace = value
57
+ end
58
+
59
+ def on(*triggers, run: nil, **options, &block)
60
+ @method_triggers = triggers
61
+ @method_trigger_options = options # TODO: store and pass to gateway
62
+ return unless run || block
63
+
64
+ block = ->(event) { self.run(run, event.body) } if run
65
+ define_method("handle_#{triggers.join('_or_')}", &block)
66
+ end
67
+
68
+ def method_added(method_name)
69
+ super
70
+ return unless defined?(@method_triggers)
71
+
72
+ @trigger_mapping ||= {}
73
+ @method_triggers.each do |trigger|
74
+ (@trigger_mapping[trigger.to_sym] ||= []) << method_name
75
+ end
76
+ remove_instance_variable(:@method_triggers)
77
+ end
78
+ end
79
+
80
+ def initialize(runtime: nil)
81
+ @runtime = runtime
82
+ end
83
+
84
+ def call(event)
85
+ event_to_methods(event).each do |method_name|
86
+ send(method_name, event)
87
+ rescue Realm::UniqueConstraintError => e
88
+ context[:logger]&.warn(e.full_message)
89
+ end
90
+ end
91
+
92
+ protected
93
+
94
+ delegate :context, to: :@runtime
95
+
96
+ private
97
+
98
+ def event_to_methods(event)
99
+ self.class.trigger_mapping.fetch_values(event.type.to_sym, :any) { [] }.flatten
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+ require 'active_support/core_ext/hash'
5
+ require 'realm/error'
6
+ require 'realm/domain_resolver'
7
+ require 'realm/event_handler'
8
+ require 'realm/event_factory'
9
+ require 'realm/mixins/dependency_injection'
10
+ require_relative 'event_router/internal_loop_gateway'
11
+
12
+ module Realm
13
+ class EventRouter
14
+ include Mixins::DependencyInjection
15
+ inject DomainResolver
16
+ inject 'Realm::Runtime', lazy: true
17
+
18
+ def initialize(gateways_spec, prefix: nil)
19
+ @prefix = prefix
20
+ @auto_registered = false
21
+ @default_namespace = nil
22
+ init_gateways(gateways_spec)
23
+ end
24
+
25
+ def register(handler_class)
26
+ gateway_for(handler_class.try(:event_namespace)).register(handler_class)
27
+ end
28
+
29
+ def add_listener(event_type, listener, namespace: nil)
30
+ gateway_for(namespace).add_listener(event_type, listener)
31
+ end
32
+
33
+ def trigger(identifier, attributes = {})
34
+ namespace, event_type = identifier.to_s.include?('.') ? identifier.split('.') : [nil, identifier]
35
+ gateway_for(namespace).trigger(event_type, attributes)
36
+ end
37
+
38
+ def workers(*namespaces, **options)
39
+ auto_register_handlers
40
+ @gateways.filter_map do |(namespace, gateway)|
41
+ gateway.worker(**options) if namespaces.empty? || namespaces.include?(namespace)
42
+ end
43
+ end
44
+
45
+ def active_queues
46
+ auto_register_handlers
47
+ @gateways.values.reduce([]) do |queues, gateway|
48
+ queues + gateway.queues
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def init_gateways(gateways_spec)
55
+ auto_register_on_init = false
56
+ @gateways = gateways_spec.each_with_object({}) do |(namespace, config), gateways|
57
+ gateway_class = gateway_class(config.fetch(:type))
58
+ auto_register_on_init ||= gateway_class.auto_register_on_init
59
+ gateways[namespace] = instantiate_gateway(namespace, gateway_class, config)
60
+ @default_namespace = namespace if config[:default]
61
+ end
62
+ auto_register_handlers if auto_register_on_init
63
+ end
64
+
65
+ def gateway_class(type)
66
+ return InternalLoopGateway if type.to_s == 'internal_loop'
67
+
68
+ runtime.container.resolve("event_router.gateway_classes.#{type}")
69
+ end
70
+
71
+ def instantiate_gateway(namespace, klass, config)
72
+ klass.new(
73
+ namespace: namespace,
74
+ queue_prefix: @prefix,
75
+ event_factory: EventFactory.new(config.fetch(:events_module)),
76
+ runtime: runtime,
77
+ **config.except(:type, :default, :events_module),
78
+ )
79
+ end
80
+
81
+ def auto_register_handlers
82
+ return if @auto_registered || !domain_resolver
83
+
84
+ @auto_registered = true
85
+ domain_resolver.all_event_handlers.each { |klass| register(klass) }
86
+ end
87
+
88
+ def gateway_for(namespace)
89
+ @gateways.fetch(namespace.try(:to_sym) || default_namespace) do
90
+ raise "No event gateway for #{namespace || 'default'} namespace" # TODO: extract error class
91
+ end
92
+ end
93
+
94
+ def default_namespace
95
+ return @default_namespace if @default_namespace
96
+
97
+ @gateways.keys[0] if @gateways.keys.size == 1
98
+ end
99
+ end
100
+ end