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.
- checksums.yaml +4 -4
- data/lib/realm-core.rb +11 -0
- data/lib/realm.rb +20 -0
- data/lib/realm/action_handler.rb +84 -0
- data/lib/realm/action_handler/result.rb +32 -0
- data/lib/realm/builder.rb +84 -0
- data/lib/realm/command_handler.rb +18 -0
- data/lib/realm/config.rb +56 -0
- data/lib/realm/container.rb +65 -0
- data/lib/realm/context.rb +35 -0
- data/lib/realm/dependency.rb +22 -0
- data/lib/realm/dispatcher.rb +66 -0
- data/lib/realm/domain_resolver.rb +53 -0
- data/lib/realm/error.rb +62 -0
- data/lib/realm/event.rb +55 -0
- data/lib/realm/event_factory.rb +49 -0
- data/lib/realm/event_handler.rb +94 -0
- data/lib/realm/event_router.rb +91 -0
- data/lib/realm/event_router/gateway.rb +50 -0
- data/lib/realm/event_router/internal_loop_gateway.rb +48 -0
- data/lib/realm/health_status.rb +44 -0
- data/lib/realm/mixins/aggregate_member.rb +25 -0
- data/lib/realm/mixins/context_injection.rb +47 -0
- data/lib/realm/mixins/controller.rb +50 -0
- data/lib/realm/mixins/decorator.rb +33 -0
- data/lib/realm/mixins/dependency_injection.rb +50 -0
- data/lib/realm/mixins/reactive.rb +30 -0
- data/lib/realm/mixins/repository_helper.rb +41 -0
- data/lib/realm/multi_worker.rb +30 -0
- data/lib/realm/persistence.rb +51 -0
- data/lib/realm/persistence/repository_query_handler_adapter.rb +21 -0
- data/lib/realm/plugin.rb +17 -0
- data/lib/realm/query_handler.rb +6 -0
- data/lib/realm/runtime.rb +51 -0
- data/lib/realm/runtime/session.rb +31 -0
- data/lib/realm/types.rb +9 -0
- metadata +36 -3
- data/README.md +0 -40
- 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
|