realm-core 0.7.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 +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
|