realm-core 0.7.3 → 0.7.4

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