realm-core 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/realm-core.rb +6 -0
- data/lib/realm.rb +22 -0
- data/lib/realm/action_handler.rb +84 -0
- data/lib/realm/action_handler/result.rb +32 -0
- data/lib/realm/builder.rb +93 -0
- data/lib/realm/command_handler.rb +23 -0
- data/lib/realm/config.rb +57 -0
- data/lib/realm/container.rb +68 -0
- data/lib/realm/context.rb +37 -0
- data/lib/realm/dependency.rb +24 -0
- data/lib/realm/dispatcher.rb +74 -0
- data/lib/realm/domain_resolver.rb +59 -0
- data/lib/realm/error.rb +64 -0
- data/lib/realm/event.rb +56 -0
- data/lib/realm/event_factory.rb +42 -0
- data/lib/realm/event_handler.rb +102 -0
- data/lib/realm/event_router.rb +100 -0
- data/lib/realm/event_router/gateway.rb +49 -0
- data/lib/realm/event_router/internal_loop_gateway.rb +50 -0
- data/lib/realm/health_status.rb +46 -0
- data/lib/realm/mixins/aggregate_member.rb +25 -0
- data/lib/realm/mixins/context_injection.rb +49 -0
- data/lib/realm/mixins/controller.rb +53 -0
- data/lib/realm/mixins/decorator.rb +33 -0
- data/lib/realm/mixins/dependency_injection.rb +52 -0
- data/lib/realm/mixins/reactive.rb +32 -0
- data/lib/realm/mixins/repository_helper.rb +43 -0
- data/lib/realm/multi_worker.rb +30 -0
- data/lib/realm/persistence.rb +54 -0
- data/lib/realm/persistence/repository_query_handler_adapter.rb +24 -0
- data/lib/realm/plugin.rb +20 -0
- data/lib/realm/query_handler.rb +8 -0
- data/lib/realm/runtime.rb +61 -0
- data/lib/realm/runtime/session.rb +33 -0
- data/lib/realm/types.rb +9 -0
- metadata +217 -0
@@ -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
|
data/lib/realm/error.rb
ADDED
@@ -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
|
data/lib/realm/event.rb
ADDED
@@ -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
|