realm-core 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0f61dae355ea08d78da40bc227540ce47635d09ac0f8645d3f70e1e53201ff54
4
+ data.tar.gz: 57bfe266b33d2f7744b87af84b14a2bd0d7a3c25c0bfc6b452457488067ad181
5
+ SHA512:
6
+ metadata.gz: 3568eca1b26cd974a6d860e233cb50d4911e49bd7f61f4d6c43d865e78cbded0d48dd40e6331fa8d46ce3eb8906a7093c797ce340e16e33c4fad441085199c72
7
+ data.tar.gz: cca39336c0d199d46254e15293439b9e81f401b8e4a586f68eb90658a5e027888f4bce1911dad21dbbd6fb2a6c4c68e4380d47935d1492c962e84a092496f8de
data/lib/realm-core.rb ADDED
@@ -0,0 +1,6 @@
1
+ # rubocop:disable Naming/FileName
2
+ # frozen_string_literal: true
3
+
4
+ require 'realm'
5
+
6
+ # rubocop:enable Naming/FileName
data/lib/realm.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(File.dirname(__FILE__), 'realm', '**', '*.rb')].sort.each do |f|
4
+ require f
5
+ end
6
+
7
+ module Realm
8
+ class << self
9
+ # Setup realm in test/console
10
+ def setup(root_module, **options)
11
+ config = Realm::Config.new(root_module: root_module, **options)
12
+ Realm::Builder.setup(config)
13
+ end
14
+
15
+ # Bind realm in service/engine
16
+ def bind(root_module, **options)
17
+ setup(root_module, **options).tap do |builder|
18
+ root_module.define_singleton_method(:realm) { builder.runtime }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-validation'
4
+ require 'active_support/core_ext/module/delegation'
5
+ require 'realm/error'
6
+ require 'realm/mixins/context_injection'
7
+ require 'realm/mixins/repository_helper'
8
+ require 'realm/mixins/aggregate_member'
9
+ require_relative 'action_handler/result'
10
+
11
+ module Realm
12
+ class ActionHandler
13
+ extend Mixins::ContextInjection::ClassMethods
14
+ include Mixins::AggregateMember
15
+ include Mixins::RepositoryHelper
16
+
17
+ class << self
18
+ attr_reader :contracts
19
+
20
+ def call(action: :handle, params: {}, runtime: nil)
21
+ new(runtime: runtime).(action: action, params: params)
22
+ end
23
+
24
+ protected
25
+
26
+ def require_permission(*names)
27
+ # TODO: implement
28
+ end
29
+
30
+ def contract(&block)
31
+ @method_contract = Class.new(Dry::Validation::Contract, &block).new
32
+ end
33
+
34
+ def contract_schema(&block)
35
+ contract { schema(&block) }
36
+ end
37
+
38
+ def contract_params(&block)
39
+ contract { params(&block) }
40
+ end
41
+
42
+ def contract_json(&block)
43
+ contract { json(&block) }
44
+ end
45
+
46
+ def method_added(method_name)
47
+ super
48
+ return unless defined?(@method_contract)
49
+
50
+ @contracts ||= {}
51
+ @contracts[method_name] = @method_contract
52
+ remove_instance_variable(:@method_contract)
53
+ end
54
+ end
55
+
56
+ def initialize(runtime: nil)
57
+ @runtime = runtime
58
+ end
59
+
60
+ def call(action: :handle, params: {})
61
+ # TODO: check permissions
62
+ raise CannotHandleAction.new(self, action) unless respond_to?(action)
63
+
64
+ safe_params = validate(action, params.to_h)
65
+ send(action, **safe_params)
66
+ end
67
+
68
+ protected
69
+
70
+ delegate :context, to: :@runtime
71
+
72
+ private
73
+
74
+ def validate(action, params)
75
+ contract = self.class.contracts && self.class.contracts[action]
76
+ return params unless contract
77
+
78
+ result = contract.(params)
79
+ raise Realm::InvalidParams, result if result.failure?
80
+
81
+ result.to_h
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Realm
4
+ class ActionHandler
5
+ # Tuple of label and value
6
+ class Result < Array
7
+ def self.[](first, second = nil)
8
+ return new(first, second).freeze if first.is_a?(Symbol) || first.is_a?(Realm::Event)
9
+
10
+ new(second || :ok, first).freeze
11
+ end
12
+
13
+ def label
14
+ self[0]
15
+ end
16
+
17
+ def event
18
+ label if label.is_a?(Realm::Event)
19
+ end
20
+
21
+ def value
22
+ self[1]
23
+ end
24
+
25
+ private
26
+
27
+ def initialize(label, value)
28
+ super([label, value])
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+ require 'realm/container'
5
+ require 'realm/runtime'
6
+ require 'realm/domain_resolver'
7
+ require 'realm/persistence'
8
+ require 'realm/dispatcher'
9
+ require 'realm/event_router'
10
+ require 'realm/plugin'
11
+
12
+ module Realm
13
+ class Builder
14
+ def self.setup(config)
15
+ new(config).setup
16
+ end
17
+
18
+ def initialize(config)
19
+ @config = config
20
+ end
21
+
22
+ def setup
23
+ logger.info("Setting up #{cfg.root_module} realm")
24
+ register_domain_resolver
25
+ register_event_router
26
+ register_runtime
27
+ register_logger
28
+ register_dependencies
29
+ setup_plugins
30
+ config_persistence
31
+ self
32
+ end
33
+
34
+ def runtime
35
+ @container.resolve(Runtime)
36
+ end
37
+
38
+ private
39
+
40
+ def register_domain_resolver
41
+ container.register_factory(DomainResolver, constantize(cfg.domain_module))
42
+ end
43
+
44
+ def register_event_router
45
+ return if cfg.event_gateways.empty?
46
+
47
+ container.register_factory(EventRouter, cfg.event_gateways, prefix: cfg.prefix)
48
+ end
49
+
50
+ def register_runtime
51
+ container.register_factory(Runtime, container)
52
+ end
53
+
54
+ def register_logger
55
+ container.register(:logger, logger)
56
+ end
57
+
58
+ def register_dependencies
59
+ container.register_all(**cfg.dependencies)
60
+ end
61
+
62
+ def setup_plugins
63
+ Plugin.descendants.each do |plugin|
64
+ plugin.setup(cfg, container) if cfg.plugins.include?(plugin.plugin_name)
65
+ end
66
+ end
67
+
68
+ def config_persistence
69
+ return unless cfg.persistence_gateway.present?
70
+
71
+ Persistence.setup(container, cfg.persistence_gateway[:repositories])
72
+ end
73
+
74
+ def constantize(*parts)
75
+ return parts[0] unless parts[0].is_a?(String)
76
+
77
+ parts.join('::').safe_constantize
78
+ end
79
+
80
+ def cfg
81
+ @config
82
+ end
83
+
84
+ def container
85
+ @container ||= Container.new
86
+ end
87
+
88
+ def logger
89
+ @logger ||= cfg.logger || (defined?(Rails) && Rails.logger) ||
90
+ Logger.new($stdout, level: ENV.fetch('LOG_LEVEL', :info).to_sym)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/module/delegation'
4
+ require 'active_support/core_ext/string'
5
+ require 'realm/action_handler'
6
+ require 'realm/mixins/reactive'
7
+
8
+ module Realm
9
+ class CommandHandler < Realm::ActionHandler
10
+ include Mixins::Reactive
11
+
12
+ def call(*)
13
+ gateway = context[:rom]&.gateways&.dig(:default)
14
+ gateway ? gateway.transaction { super } : super
15
+ end
16
+
17
+ protected
18
+
19
+ def result(first, second = nil)
20
+ Result[first, second]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+ require 'dry-initializer'
5
+
6
+ module Realm
7
+ class Config
8
+ extend Dry::Initializer
9
+
10
+ option :root_module
11
+ option :database_url, default: proc {}
12
+ option :prefix, default: proc {}
13
+ option :namespace, default: proc { root_module.to_s.underscore }
14
+ option :domain_module, default: proc { "#{root_module}::Domain" }
15
+ option :engine_class, default: proc { "#{root_module}::Engine" }
16
+ option :engine_path, default: proc { engine_class&.to_s&.safe_constantize&.root }
17
+ option :logger, default: proc {}
18
+ option :plugins, default: proc { [] }, reader: false
19
+ option :dependencies, default: proc { {} }
20
+ option :persistence_gateway, default: proc { database_url && { type: :rom, url: database_url } }, reader: false
21
+ option :event_gateway, default: proc {}, reader: false
22
+ option :event_gateways, default: proc {
23
+ @event_gateway ? { default: { **@event_gateway, default: true } } : {}
24
+ }
25
+
26
+ def plugins
27
+ Array(@plugins)
28
+ end
29
+
30
+ def persistence_gateway
31
+ return {} unless @persistence_gateway
32
+
33
+ class_path = engine_path && "#{engine_path}/app/persistence/#{namespace}"
34
+ repos_path = class_path && "#{class_path}/repositories"
35
+ repos_module = "#{root_module}::Repositories"
36
+ {
37
+ root_module: root_module,
38
+ class_path: class_path,
39
+ repos_path: repos_path,
40
+ repos_module: repos_module,
41
+ migration_path: engine_path && "#{engine_path}/db/migrate",
42
+ repositories: repositories(repos_path, repos_module),
43
+ }.merge(@persistence_gateway)
44
+ end
45
+
46
+ private
47
+
48
+ def repositories(repos_path, repos_module)
49
+ return [] unless repos_path
50
+
51
+ Dir[File.join(repos_path, '**', '*.rb')].each_with_object([]) do |filename, all|
52
+ matches = %r{^#{repos_path}/(.+)\.rb$}.match(filename)
53
+ all << "#{repos_module}::#{matches[1].camelize}".constantize if matches
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-container'
4
+ require 'active_support/core_ext/string'
5
+ require 'active_support/core_ext/object/try'
6
+ require 'realm/error'
7
+
8
+ module Realm
9
+ class Container
10
+ include Dry::Container::Mixin
11
+ include Enumerable
12
+
13
+ def self.[](object)
14
+ object.is_a?(Container) ? object : Container.new(object)
15
+ end
16
+
17
+ def initialize(hash = {})
18
+ register_all(hash)
19
+ end
20
+
21
+ def register(key, contents = nil, options = {}, &block)
22
+ options[:klass] ||= contents.class if contents && !contents.is_a?(::Hash)
23
+ super(key, contents, options, &block)
24
+ end
25
+
26
+ def register_all(hash)
27
+ hash.each_pair do |key, value|
28
+ register(key, value)
29
+ end
30
+ end
31
+
32
+ def register_factory(klass, *args, as: nil, memoize: true, **kwargs) # rubocop:disable Naming/MethodParameterName
33
+ register(as || klass, klass: klass, memoize: memoize) do
34
+ create(klass, *args, **kwargs)
35
+ end
36
+ end
37
+
38
+ def create(klass, *args, **kwargs)
39
+ (klass.try(:dependencies) || []).each do |d|
40
+ fn = -> { resolve_dependable(sanitize_dependable(d.dependable), d.optional?) }
41
+ kwargs[d.name] = d.lazy? ? fn : fn.call
42
+ end
43
+ klass.new(*args, **kwargs)
44
+ end
45
+
46
+ def [](key)
47
+ resolve(key) if key?(key)
48
+ end
49
+
50
+ def resolve_all(klass)
51
+ _container.each_with_object([]) do |(_, item), all|
52
+ all << item.call if item.options[:klass] <= klass
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def sanitize_dependable(dependable)
59
+ dependable.is_a?(String) && dependable.match(/^[A-Z]/) ? dependable.constantize : dependable
60
+ end
61
+
62
+ def resolve_dependable(dependable, optional)
63
+ raise DependencyMissing, dependable unless optional || key?(dependable)
64
+
65
+ self[dependable]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'realm/container'
4
+
5
+ module Realm
6
+ class Context
7
+ include Enumerable
8
+
9
+ def initialize(*containers)
10
+ @containers = containers.map { |c| Container[c] }
11
+ end
12
+
13
+ def [](name)
14
+ @containers.each do |container|
15
+ return container[name] if container.key?(name)
16
+ end
17
+ nil
18
+ end
19
+
20
+ def key?(name)
21
+ @containers.any? { |container| container.key?(name) }
22
+ end
23
+
24
+ def merge(container_like)
25
+ container_like.blank? ? self : self.class.new(container_like, *@containers)
26
+ end
27
+
28
+ def each(&block)
29
+ @containers.each { |container| container.each(&block) }
30
+ end
31
+
32
+ # Just for testing
33
+ def override!(container)
34
+ @containers.prepend(container)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+
5
+ module Realm
6
+ class Dependency
7
+ attr_reader :dependable, :name
8
+
9
+ def initialize(dependable, as: nil, optional: false, lazy: false) # rubocop:disable Naming/MethodParameterName
10
+ @dependable = dependable
11
+ @name = as || dependable.to_s.demodulize.underscore.to_sym
12
+ @optional = optional
13
+ @lazy = lazy
14
+ end
15
+
16
+ def optional?
17
+ @optional
18
+ end
19
+
20
+ def lazy?
21
+ @lazy
22
+ end
23
+ end
24
+ end