realm-core 0.7.3 → 0.7.4

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.
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