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,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ class EventRouter
5
+ class Gateway
6
+ def self.auto_register_on_init
7
+ false
8
+ end
9
+
10
+ def initialize(event_factory:, namespace: :default, runtime: nil, **)
11
+ @namespace = namespace
12
+ @event_factory = event_factory
13
+ @runtime = runtime
14
+ end
15
+
16
+ def register(handler_class)
17
+ # TODO: validate event_types for existence of matching class
18
+ handler_class.event_types.each do |event_type|
19
+ add_listener(event_type, handler_class.bind_runtime(@runtime))
20
+ end
21
+ end
22
+
23
+ def add_listener(event_type, listener)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def trigger(event_type, attributes = {})
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def worker(*)
32
+ nil
33
+ end
34
+
35
+ def cleanup
36
+ # do nothing
37
+ end
38
+
39
+ def queues
40
+ []
41
+ end
42
+
43
+ protected
44
+
45
+ def create_event(event_type, attributes = {})
46
+ @event_factory.create_event(event_type, **attributes)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ class EventRouter
5
+ class InternalLoopGateway < Gateway
6
+ def self.auto_register_on_init
7
+ true
8
+ end
9
+
10
+ def initialize(isolated: false, **)
11
+ super
12
+ @listener_map = {}
13
+ @isolated = isolated
14
+ gateways << self
15
+ end
16
+
17
+ def add_listener(event_type, listener)
18
+ (@listener_map[event_type.to_sym] ||= []) << listener
19
+ end
20
+
21
+ def trigger(event_type, attributes = {})
22
+ create_event(event_type, attributes).tap do |event|
23
+ gateways.each { |gateway| gateway.handle(event_type, event) }
24
+ end
25
+ end
26
+
27
+ def purge!
28
+ gateways.clear
29
+ end
30
+
31
+ protected
32
+
33
+ def handle(event_type, event)
34
+ find_listeners(event_type).each { |listener| listener.(event) }
35
+ end
36
+
37
+ private
38
+
39
+ def find_listeners(event_type)
40
+ @listener_map.fetch_values(event_type.to_sym, :any) { [] }.flatten
41
+ end
42
+
43
+ def gateways
44
+ @isolated ? (@gateways ||= []) : (@@gateways ||= []) # rubocop:disable Style/ClassVars
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ class HealthStatus
5
+ CODES = %i[green yellow red].freeze
6
+ attr_reader :code, :issues
7
+
8
+ class << self
9
+ def [](code, *issues)
10
+ new(code, issues.flatten)
11
+ end
12
+
13
+ def from_issues(issues)
14
+ new(issues.blank? ? :green : :red, issues)
15
+ end
16
+
17
+ def combine(component_map)
18
+ code_index = component_map.values.map { |i| CODES.index(i.code) }.max
19
+ new(CODES[code_index || 0], [], component_map)
20
+ end
21
+ end
22
+
23
+ def for_component(*names)
24
+ @component_map.dig(*names)
25
+ end
26
+
27
+ def to_h
28
+ hash = { status: @code }
29
+ hash[:issues] = @issues if @issues.present?
30
+ hash[:components] = @component_map.transform_values(&:to_h) if @component_map.present?
31
+ hash
32
+ end
33
+
34
+ private
35
+
36
+ def initialize(code, issues = [], component_map = {})
37
+ raise ArgumentError, "Invalid status code #{code}" unless CODES.include?(code)
38
+
39
+ @code = code
40
+ @issues = issues.freeze
41
+ @component_map = component_map.freeze
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ module Mixins
5
+ module AggregateMember
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ def aggregate
11
+ self.class.aggregate
12
+ end
13
+
14
+ module ClassMethods
15
+ def aggregate
16
+ @aggregate ||= begin
17
+ module_chain = name.split('::')
18
+ domain_index = module_chain.index('Domain')
19
+ domain_index && module_chain[domain_index + 1].underscore.to_sym
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ module Mixins
5
+ module ContextInjection
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ base.prepend(Initializer)
9
+ end
10
+
11
+ module ClassMethods
12
+ def inject(*names, &block)
13
+ names.each do |name|
14
+ define_method(name) do
15
+ raise Realm::DependencyMissing, name unless context.key?(name)
16
+
17
+ return context[name] unless block
18
+
19
+ var = "@#{name}"
20
+ return instance_variable_get(var) if instance_variable_defined?(var)
21
+
22
+ instance_variable_set(var, block.(context[name]))
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ module Initializer
29
+ def initialize(*args, context: nil, **kwargs)
30
+ @context = context || context_from_root_module || {}
31
+ super(*args, **kwargs)
32
+ end
33
+
34
+ private
35
+
36
+ def context_from_root_module
37
+ root_module = self.class.module_parents[-2]
38
+ root_module.realm.context if root_module.respond_to?(:realm)
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ attr_reader :context
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ module Mixins
5
+ module Controller
6
+ def self.included(base)
7
+ base.class_attribute(:aggregate_name)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ def domain_runtime
12
+ @domain_runtime ||= root_domain_runtime.session(domain_context)
13
+ end
14
+
15
+ def domain_context
16
+ {}
17
+ end
18
+
19
+ def query(identifier, params = {})
20
+ domain_runtime.query(get_dispatchable(identifier), params)
21
+ end
22
+
23
+ def run(identifier, params = {})
24
+ domain_runtime.run(get_dispatchable(identifier), params)
25
+ end
26
+
27
+ def run_as_job(identifier, params = {})
28
+ domain_runtime.run_as_job(get_dispatchable(identifier), params)
29
+ end
30
+
31
+ private
32
+
33
+ def get_dispatchable(identifier)
34
+ return identifier if identifier.respond_to?(:call)
35
+
36
+ [aggregate_name, identifier].compact.join('.')
37
+ end
38
+
39
+ def root_domain_runtime
40
+ self.class.module_parents[-2].realm
41
+ end
42
+
43
+ module ClassMethods
44
+ def with_aggregate(name)
45
+ self.aggregate_name = name
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ module Mixins
5
+ module Decorator
6
+ def self.[](decorated) # rubocop:disable Metrics/MethodLength
7
+ Module.new do
8
+ def method_missing(...)
9
+ _decorated.send(...)
10
+ end
11
+
12
+ def respond_to_missing?(...)
13
+ _decorated.respond_to?(...)
14
+ end
15
+
16
+ if decorated.to_s[0] == '@'
17
+ define_method :initialize do |value|
18
+ instance_variable_set(decorated, value)
19
+ end
20
+
21
+ define_method :_decorated do
22
+ instance_variable_get(decorated)
23
+ end
24
+ else
25
+ define_method :_decorated do
26
+ send(decorated)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ module Mixins
5
+ module DependencyInjection
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def new(*args, **kwargs, &block)
12
+ instance = allocate
13
+ deps.each { |d| define_dependency_method(instance, kwargs, d) }
14
+ kwargs_without_dependencies = kwargs.reject { |k, _| deps.any? { |d| d.name == k } }
15
+ instance.send(:initialize, *args, **kwargs_without_dependencies, &block)
16
+ instance
17
+ end
18
+
19
+ def inject(*dependables, **options)
20
+ deps.concat(dependables.map { |d| Dependency.new(d, **options) })
21
+ end
22
+
23
+ def dependencies
24
+ deps.freeze
25
+ end
26
+
27
+ private
28
+
29
+ def deps
30
+ @deps ||= []
31
+ end
32
+
33
+ def define_dependency_method(instance, kwargs, spec)
34
+ dependency = kwargs[spec.name]
35
+ instance.singleton_class.class_eval do
36
+ define_method(spec.name) do
37
+ return dependency unless spec.lazy?
38
+
39
+ var = "@#{spec.name}"
40
+ return instance_variable_get(var) if instance_variable_defined?(var)
41
+
42
+ instance_variable_set(var, dependency.respond_to?(:call) ? dependency.call : dependency)
43
+ end
44
+ protected spec.name
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ module Mixins
5
+ module Reactive
6
+ protected
7
+
8
+ def run(command, params = {})
9
+ parts = command.to_s.split('.')
10
+ parts.prepend(aggregate) if parts.size == 1 && respond_to?(:aggregate) && aggregate
11
+ @runtime.run(parts.join('.'), params.to_h)
12
+ end
13
+
14
+ def trigger(event_type, attributes = {})
15
+ attributes = attributes.to_h
16
+ head = { origin: origin(caller_locations(1, 1)) }.merge(attributes.fetch(:head, {}))
17
+ final_attrs = attributes.merge(head: head)
18
+ final_attrs[:cause] ||= context[:cause] if context.key?(:cause)
19
+ @runtime.trigger(event_type, final_attrs)
20
+ end
21
+
22
+ private
23
+
24
+ # Detects the class and method from which this event is triggered
25
+ def origin(backtrace)
26
+ [self.class.name, backtrace[0].to_s.match(/`([^']+)'/)&.then { |m| "##{m[1]}" }].join
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ module Mixins
5
+ module RepositoryHelper
6
+ class OnlyOneWriteRepo < Realm::Error['You can have only one read/write repo per handler']; end
7
+ class InjectingRepoOutsideAggregate < Realm::Error['Cannot auto inject repository outside of an aggregate']; end
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ protected
15
+
16
+ def use_repo(*names, readonly: self < Realm::QueryHandler)
17
+ raise OnlyOneWriteRepo if !readonly && (names.size > 1 || defined?(@write_repo_injected))
18
+
19
+ names << default_repo_name if names.empty?
20
+ names.each { |name| inject_repo(name, readonly) }
21
+ @write_repo_injected = true unless readonly
22
+ end
23
+
24
+ private
25
+
26
+ def inject_repo(name, readonly)
27
+ repo_name = "#{name}_repo"
28
+ return inject(repo_name) unless readonly
29
+
30
+ inject(repo_name) { |repo| repo.respond_to?(:readonly) ? repo.readonly : repo }
31
+ end
32
+
33
+ def default_repo_name
34
+ raise InjectingRepoOutsideAggregate unless respond_to?(:aggregate)
35
+
36
+ aggregate
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ class MultiWorker
5
+ def initialize(workers = [])
6
+ @workers = workers
7
+ end
8
+
9
+ def start(*args)
10
+ @workers.each { |w| w.start(*args) }
11
+ self
12
+ end
13
+
14
+ def stop(timeout: 30)
15
+ @workers.each { |w| w.stop(timeout: timeout) }
16
+ end
17
+
18
+ def join
19
+ @workers.each(&:join)
20
+ end
21
+
22
+ def run
23
+ %w[INT TERM].each do |signal|
24
+ Signal.trap(signal) { stop }
25
+ end
26
+ start
27
+ join
28
+ end
29
+ end
30
+ end