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