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