dry-system 0.5.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.
- checksums.yaml +7 -0
- data/.gitignore +39 -0
- data/.rspec +3 -0
- data/.rubocop.yml +34 -0
- data/.rubocop_todo.yml +26 -0
- data/.travis.yml +26 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +158 -0
- data/Gemfile +9 -0
- data/LICENSE +20 -0
- data/README.md +23 -0
- data/Rakefile +12 -0
- data/dry-system.gemspec +30 -0
- data/examples/standalone/Gemfile +5 -0
- data/examples/standalone/lib/user_repo.rb +5 -0
- data/examples/standalone/run.rb +7 -0
- data/examples/standalone/system/boot/persistence.rb +13 -0
- data/examples/standalone/system/container.rb +9 -0
- data/examples/standalone/system/import.rb +3 -0
- data/lib/dry-system.rb +1 -0
- data/lib/dry/system.rb +4 -0
- data/lib/dry/system/auto_registrar.rb +80 -0
- data/lib/dry/system/booter.rb +101 -0
- data/lib/dry/system/component.rb +167 -0
- data/lib/dry/system/constants.rb +9 -0
- data/lib/dry/system/container.rb +500 -0
- data/lib/dry/system/errors.rb +62 -0
- data/lib/dry/system/importer.rb +53 -0
- data/lib/dry/system/injector.rb +68 -0
- data/lib/dry/system/lifecycle.rb +104 -0
- data/lib/dry/system/loader.rb +69 -0
- data/lib/dry/system/version.rb +5 -0
- data/spec/fixtures/components/bar.rb +5 -0
- data/spec/fixtures/components/bar/baz.rb +4 -0
- data/spec/fixtures/components/foo.rb +2 -0
- data/spec/fixtures/import_test/config/application.yml +2 -0
- data/spec/fixtures/import_test/lib/test/bar.rb +4 -0
- data/spec/fixtures/import_test/lib/test/foo.rb +5 -0
- data/spec/fixtures/import_test/system/boot/bar.rb +11 -0
- data/spec/fixtures/lazytest/config/application.yml +2 -0
- data/spec/fixtures/lazytest/lib/test/dep.rb +4 -0
- data/spec/fixtures/lazytest/lib/test/foo.rb +5 -0
- data/spec/fixtures/lazytest/lib/test/models.rb +4 -0
- data/spec/fixtures/lazytest/lib/test/models/book.rb +6 -0
- data/spec/fixtures/lazytest/lib/test/models/user.rb +6 -0
- data/spec/fixtures/lazytest/system/boot/bar.rb +15 -0
- data/spec/fixtures/namespaced_components/namespaced/bar.rb +5 -0
- data/spec/fixtures/namespaced_components/namespaced/foo.rb +4 -0
- data/spec/fixtures/other/config/boot/bar.rb +11 -0
- data/spec/fixtures/other/lib/test/dep.rb +4 -0
- data/spec/fixtures/other/lib/test/foo.rb +5 -0
- data/spec/fixtures/other/lib/test/models.rb +4 -0
- data/spec/fixtures/other/lib/test/models/book.rb +6 -0
- data/spec/fixtures/other/lib/test/models/user.rb +6 -0
- data/spec/fixtures/test/config/application.yml +2 -0
- data/spec/fixtures/test/config/subapp.yml +2 -0
- data/spec/fixtures/test/lib/test/dep.rb +4 -0
- data/spec/fixtures/test/lib/test/foo.rb +5 -0
- data/spec/fixtures/test/lib/test/models.rb +4 -0
- data/spec/fixtures/test/lib/test/models/book.rb +6 -0
- data/spec/fixtures/test/lib/test/models/user.rb +6 -0
- data/spec/fixtures/test/lib/test/singleton_dep.rb +7 -0
- data/spec/fixtures/test/log/.gitkeep +0 -0
- data/spec/fixtures/test/system/boot/bar.rb +11 -0
- data/spec/fixtures/test/system/boot/client.rb +7 -0
- data/spec/fixtures/test/system/boot/db.rb +1 -0
- data/spec/fixtures/test/system/boot/logger.rb +5 -0
- data/spec/fixtures/umbrella/system/boot/db.rb +10 -0
- data/spec/integration/boot_spec.rb +18 -0
- data/spec/integration/import_spec.rb +63 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/unit/component_spec.rb +116 -0
- data/spec/unit/container/auto_register_spec.rb +85 -0
- data/spec/unit/container/finalize_spec.rb +85 -0
- data/spec/unit/container/import_spec.rb +70 -0
- data/spec/unit/container/injector_spec.rb +29 -0
- data/spec/unit/container_spec.rb +165 -0
- data/spec/unit/injector_spec.rb +72 -0
- data/spec/unit/loader_spec.rb +64 -0
- metadata +295 -0
data/lib/dry-system.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'dry/system'
|
data/lib/dry/system.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'dry/system/constants'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module System
|
5
|
+
# Default auto-registration implementation
|
6
|
+
#
|
7
|
+
# This is currently configured by default for every System::Container.
|
8
|
+
# Auto-registrar objects are responsible for loading files from configured
|
9
|
+
# auto-register paths and registering components automatically withing the
|
10
|
+
# container.
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class AutoRegistrar
|
14
|
+
attr_reader :container
|
15
|
+
|
16
|
+
attr_reader :config
|
17
|
+
|
18
|
+
def initialize(container)
|
19
|
+
@container = container
|
20
|
+
@config = container.config
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
def finalize!
|
25
|
+
Array(config.auto_register).each { |dir| call(dir) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def call(dir, &block)
|
30
|
+
components(dir).each do |component|
|
31
|
+
container.require_component(component) do
|
32
|
+
if block
|
33
|
+
register(component.identifier, yield(component))
|
34
|
+
else
|
35
|
+
register(component.identifier) { component.instance }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
def components(dir)
|
45
|
+
paths(dir).
|
46
|
+
map { |path| component(path) }.
|
47
|
+
reject { |component| key?(component.identifier) }
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
def paths(dir)
|
52
|
+
dir_root = root.join(dir.to_s.split('/')[0])
|
53
|
+
|
54
|
+
Dir["#{root}/#{dir}/**/*.rb"].map { |path|
|
55
|
+
path.to_s.sub("#{dir_root}/", '').sub(RB_EXT, EMPTY_STRING)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
# @api private
|
60
|
+
def component(path)
|
61
|
+
container.component(path)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def root
|
66
|
+
container.root
|
67
|
+
end
|
68
|
+
|
69
|
+
# @api private
|
70
|
+
def key?(name)
|
71
|
+
container.key?(name)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @api private
|
75
|
+
def register(*args, &block)
|
76
|
+
container.register(*args, &block)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'dry/system/errors'
|
2
|
+
require 'dry/system/lifecycle'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module System
|
6
|
+
# Default booter implementation
|
7
|
+
#
|
8
|
+
# This is currently configured by default for every System::Container.
|
9
|
+
# Booter objects are responsible for loading system/boot files and expose
|
10
|
+
# an API for calling lifecycle triggers.
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class Booter
|
14
|
+
attr_reader :path
|
15
|
+
|
16
|
+
attr_reader :finalizers
|
17
|
+
|
18
|
+
attr_reader :booted
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
def initialize(path)
|
22
|
+
@path = path
|
23
|
+
@booted = {}
|
24
|
+
@finalizers = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def []=(name, fn)
|
29
|
+
@finalizers[name] = fn
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
def finalize!
|
35
|
+
Dir[boot_files].each do |path|
|
36
|
+
boot!(File.basename(path, '.rb').to_sym)
|
37
|
+
end
|
38
|
+
freeze
|
39
|
+
end
|
40
|
+
|
41
|
+
# @api private
|
42
|
+
def boot(name)
|
43
|
+
Kernel.require(path.join(name.to_s))
|
44
|
+
|
45
|
+
call(name) do |lifecycle|
|
46
|
+
lifecycle.(:init)
|
47
|
+
yield(lifecycle) if block_given?
|
48
|
+
end
|
49
|
+
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
def boot!(name)
|
55
|
+
check_component_identifier(name)
|
56
|
+
|
57
|
+
return self if booted.key?(name)
|
58
|
+
|
59
|
+
boot(name) { |lifecycle| lifecycle.(:start) }
|
60
|
+
booted[name] = true
|
61
|
+
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# @api private
|
66
|
+
def call(name)
|
67
|
+
container, finalizer = finalizers[name]
|
68
|
+
|
69
|
+
if finalizer
|
70
|
+
lifecycle = Lifecycle.new(container, &finalizer)
|
71
|
+
yield(lifecycle) if block_given?
|
72
|
+
lifecycle
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# @api private
|
77
|
+
def boot_dependency(component)
|
78
|
+
boot_file = component.boot_file(path)
|
79
|
+
boot!(boot_file.basename('.*').to_s.to_sym) if boot_file.exist?
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# @api private
|
85
|
+
def boot_files
|
86
|
+
path.join('**/*.rb').to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
# @api private
|
90
|
+
def check_component_identifier(name)
|
91
|
+
unless name.is_a?(Symbol)
|
92
|
+
raise InvalidComponentIdentifierTypeError, name
|
93
|
+
end
|
94
|
+
|
95
|
+
unless path.join("#{name}.rb").exist?
|
96
|
+
raise InvalidComponentIdentifierError, name
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'concurrent/map'
|
2
|
+
|
3
|
+
require 'dry-equalizer'
|
4
|
+
require 'dry/system/loader'
|
5
|
+
require 'dry/system/errors'
|
6
|
+
require 'dry/system/constants'
|
7
|
+
|
8
|
+
module Dry
|
9
|
+
module System
|
10
|
+
# Components are objects providing information about auto-registered files.
|
11
|
+
# They expose an API to query this information and use a configurable
|
12
|
+
# loader object to initialize class instances.
|
13
|
+
#
|
14
|
+
# Components are created automatically through auto-registration and can be
|
15
|
+
# accessed through `Container.auto_register!` which yields them.
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
class Component
|
19
|
+
include Dry::Equalizer(:identifier, :path)
|
20
|
+
|
21
|
+
DEFAULT_OPTIONS = { separator: DEFAULT_SEPARATOR, namespace: nil }.freeze
|
22
|
+
|
23
|
+
# @!attribute [r] identifier
|
24
|
+
# @return [String] component's unique identifier
|
25
|
+
attr_reader :identifier
|
26
|
+
|
27
|
+
# @!attribute [r] path
|
28
|
+
# @return [String] component's relative path
|
29
|
+
attr_reader :path
|
30
|
+
|
31
|
+
# @!attribute [r] file
|
32
|
+
# @return [String] component's file name
|
33
|
+
attr_reader :file
|
34
|
+
|
35
|
+
# @!attribute [r] options
|
36
|
+
# @return [Hash] component's options
|
37
|
+
attr_reader :options
|
38
|
+
|
39
|
+
# @!attribute [r] loader
|
40
|
+
# @return [Object#call] component's loader object
|
41
|
+
attr_reader :loader
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
def self.new(*args)
|
45
|
+
cache.fetch_or_store(args.hash) do
|
46
|
+
name, options = args
|
47
|
+
options = DEFAULT_OPTIONS.merge(options || {})
|
48
|
+
|
49
|
+
ns, sep = options.values_at(:namespace, :separator)
|
50
|
+
|
51
|
+
ns_name = ensure_valid_namespace(ns, sep)
|
52
|
+
identifier = ensure_valid_identifier(name, ns_name, sep)
|
53
|
+
|
54
|
+
path = name.to_s.gsub(sep, PATH_SEPARATOR)
|
55
|
+
loader = options.fetch(:loader, Loader).new(path)
|
56
|
+
|
57
|
+
super(identifier, path, options.merge(loader: loader))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @api private
|
62
|
+
def self.ensure_valid_namespace(ns, sep)
|
63
|
+
ns_name = ns.to_s
|
64
|
+
raise InvalidNamespaceError, ns_name if ns && ns_name.include?(sep)
|
65
|
+
ns_name
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
def self.ensure_valid_identifier(name, ns_name, sep)
|
70
|
+
keys = name.to_s.scan(WORD_REGEX)
|
71
|
+
|
72
|
+
if keys.uniq.size != keys.size
|
73
|
+
raise InvalidComponentError, name, 'duplicated keys in the name'
|
74
|
+
end
|
75
|
+
|
76
|
+
keys.reject { |s| ns_name == s }.join(sep)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @api private
|
80
|
+
def self.cache
|
81
|
+
@cache ||= Concurrent::Map.new
|
82
|
+
end
|
83
|
+
|
84
|
+
# @api private
|
85
|
+
def initialize(identifier, path, options)
|
86
|
+
@identifier, @path = identifier, path
|
87
|
+
@options = options
|
88
|
+
@file = "#{path}.rb".freeze
|
89
|
+
@loader = options.fetch(:loader)
|
90
|
+
freeze
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns components instance
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# class MyApp < Dry::System::Container
|
97
|
+
# configure do |config|
|
98
|
+
# config.name = :my_app
|
99
|
+
# config.root = Pathname('/my/app')
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# auto_register!('lib/clients') do |component|
|
103
|
+
# # some custom initialization logic, ie:
|
104
|
+
# constant = component.loader.constant
|
105
|
+
# constant.create
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# @return [Object] component's class instance
|
110
|
+
#
|
111
|
+
# @api public
|
112
|
+
def instance(*args)
|
113
|
+
loader.call(*args)
|
114
|
+
end
|
115
|
+
|
116
|
+
# @api private
|
117
|
+
def bootable?(path)
|
118
|
+
boot_file(path).exist?
|
119
|
+
end
|
120
|
+
|
121
|
+
# @api private
|
122
|
+
def boot_file(path)
|
123
|
+
path.join("#{root_key}.rb")
|
124
|
+
end
|
125
|
+
|
126
|
+
# @api private
|
127
|
+
def file_exists?(paths)
|
128
|
+
paths.any? { |path| path.join(file).exist? }
|
129
|
+
end
|
130
|
+
|
131
|
+
# @api private
|
132
|
+
def prepend(name)
|
133
|
+
self.class.new(
|
134
|
+
[name, identifier].join(separator), options.merge(loader: loader.class)
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
# @api private
|
139
|
+
def namespaced(namespace)
|
140
|
+
self.class.new(
|
141
|
+
path, options.merge(loader: loader.class, namespace: namespace)
|
142
|
+
)
|
143
|
+
end
|
144
|
+
|
145
|
+
# @api private
|
146
|
+
def separator
|
147
|
+
options[:separator]
|
148
|
+
end
|
149
|
+
|
150
|
+
# @api private
|
151
|
+
def namespace
|
152
|
+
options[:namespace]
|
153
|
+
end
|
154
|
+
|
155
|
+
# @api private
|
156
|
+
def root_key
|
157
|
+
namespaces.first
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def namespaces
|
163
|
+
identifier.split(separator).map(&:to_sym)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,500 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
require 'dry-configurable'
|
4
|
+
require 'dry-container'
|
5
|
+
|
6
|
+
require 'dry/system/errors'
|
7
|
+
require 'dry/system/injector'
|
8
|
+
require 'dry/system/loader'
|
9
|
+
require 'dry/system/booter'
|
10
|
+
require 'dry/system/auto_registrar'
|
11
|
+
require 'dry/system/importer'
|
12
|
+
require 'dry/system/component'
|
13
|
+
require 'dry/system/constants'
|
14
|
+
|
15
|
+
module Dry
|
16
|
+
module System
|
17
|
+
# Abstract container class to inherit from
|
18
|
+
#
|
19
|
+
# Container class is treated as a global registry with all system components.
|
20
|
+
# Container can also import dependencies from other containers, which is
|
21
|
+
# useful in complex systems that are split into sub-systems.
|
22
|
+
#
|
23
|
+
# Container can be finalized, which triggers loading of all the defined
|
24
|
+
# components within a system, after finalization it becomes frozen. This
|
25
|
+
# typically happens in cases like booting a web application.
|
26
|
+
#
|
27
|
+
# Before finalization, Container can lazy-load components on demand. A
|
28
|
+
# component can be a simple class defined in a single file, or a complex
|
29
|
+
# component which has init/start/stop lifecycle, and it's defined in a boot
|
30
|
+
# file. Components which specify their dependencies using. Import module can
|
31
|
+
# be safely required in complete isolation, and Container will resolve and
|
32
|
+
# load these dependencies automatically.
|
33
|
+
#
|
34
|
+
# Furthermore, Container supports auto-registering components based on
|
35
|
+
# dir/file naming conventions. This reduces a lot of boilerplate code as all
|
36
|
+
# you have to do is to put your classes under configured directories and
|
37
|
+
# their instances will be automatically registered within a container.
|
38
|
+
#
|
39
|
+
# Every container needs to be configured with following settings:
|
40
|
+
#
|
41
|
+
# * `:name` - a unique container identifier
|
42
|
+
# * `:root` - a system root directory (defaults to `pwd`)
|
43
|
+
# * `:system_dir` - directory name relative to root, where bootable components
|
44
|
+
# can be defined in `boot` dir this defaults to `component`
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# class MyApp < Dry::System::Container
|
48
|
+
# configure do |config|
|
49
|
+
# config.name = :my_app
|
50
|
+
#
|
51
|
+
# # this will auto-register classes from 'lib/components'. ie if you add
|
52
|
+
# # `lib/components/repo.rb` which defines `Repo` class, then it's
|
53
|
+
# # instance will be automatically available as `MyApp['repo']`
|
54
|
+
# config.auto_register = %w(lib/components)
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# # this will configure $LOAD_PATH to include your `lib` dir
|
58
|
+
# load_paths!('lib)
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
class Container
|
63
|
+
extend Dry::Configurable
|
64
|
+
extend Dry::Container::Mixin
|
65
|
+
|
66
|
+
setting :name
|
67
|
+
setting :default_namespace
|
68
|
+
setting :root, Pathname.pwd.freeze
|
69
|
+
setting :system_dir, 'system'.freeze
|
70
|
+
setting :auto_register, []
|
71
|
+
setting :loader, Dry::System::Loader
|
72
|
+
setting :booter, Dry::System::Booter
|
73
|
+
setting :auto_registrar, Dry::System::AutoRegistrar
|
74
|
+
setting :importer, Dry::System::Importer
|
75
|
+
|
76
|
+
class << self
|
77
|
+
# Configures the container
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# class MyApp < Dry::System::Container
|
81
|
+
# configure do |config|
|
82
|
+
# config.root = Pathname("/path/to/app")
|
83
|
+
# config.name = :my_app
|
84
|
+
# config.auto_register = %w(lib/apis lib/core)
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# @return [self]
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
def configure(&block)
|
92
|
+
super(&block)
|
93
|
+
load_paths!(config.system_dir)
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
# Registers another container for import
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# # system/container.rb
|
101
|
+
# class Core < Dry::System::Container
|
102
|
+
# configure do |config|
|
103
|
+
# config.root = Pathname("/path/to/app")
|
104
|
+
# config.name = :core
|
105
|
+
# config.auto_register = %w(lib/apis lib/core)
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# # apps/my_app/system/container.rb
|
110
|
+
# require 'system/container'
|
111
|
+
#
|
112
|
+
# class MyApp < Dry::System::Container
|
113
|
+
# configure do |config|
|
114
|
+
# config.root = Pathname("/path/to/app")
|
115
|
+
# config.name = :core
|
116
|
+
# config.auto_register = %w(lib/apis lib/core)
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# import core: Core
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# @param other [Hash,Dry::Container::Namespace,Dry::System::Container]
|
123
|
+
#
|
124
|
+
# @api public
|
125
|
+
def import(other)
|
126
|
+
case other
|
127
|
+
when Hash then importer.register(other)
|
128
|
+
when Dry::Container::Namespace then super
|
129
|
+
else
|
130
|
+
if other < System::Container
|
131
|
+
importer.register(other.config.name => other)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Registers finalization function for a bootable component
|
137
|
+
#
|
138
|
+
# By convention, boot files for components should be placed in
|
139
|
+
# `%{system_dir}/boot` and they will be loaded on demand when components
|
140
|
+
# are loaded in isolation, or during finalization process.
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# # system/container.rb
|
144
|
+
# class MyApp < Dry::System::Container
|
145
|
+
# configure do |config|
|
146
|
+
# config.root = Pathname("/path/to/app")
|
147
|
+
# config.name = :core
|
148
|
+
# config.auto_register = %w(lib/apis lib/core)
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# # system/boot/db.rb
|
152
|
+
# #
|
153
|
+
# # Simple component registration
|
154
|
+
# MyApp.finalize(:db) do |container|
|
155
|
+
# require 'db'
|
156
|
+
#
|
157
|
+
# container.register(:db, DB.new)
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# # system/boot/db.rb
|
161
|
+
# #
|
162
|
+
# # Component registration with lifecycle triggers
|
163
|
+
# MyApp.finalize(:db) do |container|
|
164
|
+
# init do
|
165
|
+
# require 'db'
|
166
|
+
# DB.configure(ENV['DB_URL'])
|
167
|
+
# container.register(:db, DB.new)
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# start do
|
171
|
+
# db.establish_connection
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# stop do
|
175
|
+
# db.close_connection
|
176
|
+
# end
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# # system/boot/db.rb
|
180
|
+
# #
|
181
|
+
# # Component registration which uses another bootable component
|
182
|
+
# MyApp.finalize(:db) do |container|
|
183
|
+
# use :logger
|
184
|
+
#
|
185
|
+
# start do
|
186
|
+
# require 'db'
|
187
|
+
# DB.configure(ENV['DB_URL'], logger: logger)
|
188
|
+
# container.register(:db, DB.new)
|
189
|
+
# end
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# # system/boot/db.rb
|
193
|
+
# #
|
194
|
+
# # Component registration under a namespace. This will register the
|
195
|
+
# # db object under `persistence.db` key
|
196
|
+
# MyApp.namespace(:persistence) do |persistence|
|
197
|
+
# require 'db'
|
198
|
+
# DB.configure(ENV['DB_URL'], logger: logger)
|
199
|
+
# persistence.register(:db, DB.new)
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# @param name [Symbol] a unique identifier for a bootable component
|
203
|
+
#
|
204
|
+
# @see Lifecycle
|
205
|
+
#
|
206
|
+
# @return [self]
|
207
|
+
#
|
208
|
+
# @api public
|
209
|
+
def finalize(name, &block)
|
210
|
+
booter[name] = [self, block]
|
211
|
+
self
|
212
|
+
end
|
213
|
+
|
214
|
+
# Finalizes the container
|
215
|
+
#
|
216
|
+
# This triggers importing components from other containers, booting
|
217
|
+
# registered components and auto-registering components. It should be
|
218
|
+
# called only in places where you want to finalize your system as a
|
219
|
+
# whole, ie when booting a web application
|
220
|
+
#
|
221
|
+
# @example
|
222
|
+
# # system/container.rb
|
223
|
+
# class MyApp < Dry::System::Container
|
224
|
+
# configure do |config|
|
225
|
+
# config.root = Pathname("/path/to/app")
|
226
|
+
# config.name = :my_app
|
227
|
+
# config.auto_register = %w(lib/apis lib/core)
|
228
|
+
# end
|
229
|
+
# end
|
230
|
+
#
|
231
|
+
# # You can put finalization file anywhere you want, ie system/boot.rb
|
232
|
+
# MyApp.finalize!
|
233
|
+
#
|
234
|
+
# # If you need last-moment adjustements just before the finalization
|
235
|
+
# # you can pass a block and do it there
|
236
|
+
# MyApp.finalize! do |container|
|
237
|
+
# # stuff that only needs to happen for finalization
|
238
|
+
# end
|
239
|
+
#
|
240
|
+
# @return [self] frozen container
|
241
|
+
#
|
242
|
+
# @api public
|
243
|
+
def finalize!(&block)
|
244
|
+
return self if frozen?
|
245
|
+
|
246
|
+
yield(self) if block
|
247
|
+
|
248
|
+
importer.finalize!
|
249
|
+
booter.finalize!
|
250
|
+
auto_registrar.finalize!
|
251
|
+
|
252
|
+
freeze
|
253
|
+
end
|
254
|
+
|
255
|
+
# Boots a specific component
|
256
|
+
#
|
257
|
+
# As a result, `init` and `start` lifecycle triggers are called
|
258
|
+
#
|
259
|
+
# @example
|
260
|
+
# MyApp.boot!(:persistence)
|
261
|
+
#
|
262
|
+
# @param name [Symbol] the name of a registered bootable component
|
263
|
+
#
|
264
|
+
# @return [self]
|
265
|
+
#
|
266
|
+
# @api public
|
267
|
+
def boot!(name)
|
268
|
+
booter.boot!(name)
|
269
|
+
self
|
270
|
+
end
|
271
|
+
|
272
|
+
# Boots a specific component but calls only `init` lifecycle trigger
|
273
|
+
#
|
274
|
+
# This way of booting is useful in places where a heavy dependency is
|
275
|
+
# needed but its started environment is not required
|
276
|
+
#
|
277
|
+
# @example
|
278
|
+
# MyApp.boot(:persistence)
|
279
|
+
#
|
280
|
+
# @param [Symbol] name The name of a registered bootable component
|
281
|
+
#
|
282
|
+
# @return [self]
|
283
|
+
#
|
284
|
+
# @api public
|
285
|
+
def boot(name)
|
286
|
+
booter.boot(name)
|
287
|
+
self
|
288
|
+
end
|
289
|
+
|
290
|
+
# Sets load paths relative to the container's root dir
|
291
|
+
#
|
292
|
+
# @example
|
293
|
+
# class MyApp < Dry::System::Container
|
294
|
+
# configure do |config|
|
295
|
+
# # ...
|
296
|
+
# end
|
297
|
+
#
|
298
|
+
# load_paths!('lib')
|
299
|
+
# end
|
300
|
+
#
|
301
|
+
# @param [Array<String>] *dirs
|
302
|
+
#
|
303
|
+
# @return [self]
|
304
|
+
#
|
305
|
+
# @api public
|
306
|
+
def load_paths!(*dirs)
|
307
|
+
dirs.map(&root.method(:join)).each do |path|
|
308
|
+
next if load_paths.include?(path)
|
309
|
+
load_paths << path
|
310
|
+
$LOAD_PATH.unshift(path.to_s)
|
311
|
+
end
|
312
|
+
self
|
313
|
+
end
|
314
|
+
|
315
|
+
# Auto-registers components from the provided directory
|
316
|
+
#
|
317
|
+
# Typically you want to configure auto_register directories, and it will
|
318
|
+
# work automatically. Use this method in cases where you want to have an
|
319
|
+
# explicit way where some components are auto-registered.
|
320
|
+
#
|
321
|
+
# @example
|
322
|
+
# class MyApp < Dry::System::Container
|
323
|
+
# configure do |config|
|
324
|
+
# # ...
|
325
|
+
# end
|
326
|
+
#
|
327
|
+
# # with a dir
|
328
|
+
# auto_register!('lib/core')
|
329
|
+
#
|
330
|
+
# # with a dir and a custom registration block
|
331
|
+
# auto_register!('lib/core') do |component|
|
332
|
+
# # custom way of initializing a component
|
333
|
+
# end
|
334
|
+
# end
|
335
|
+
#
|
336
|
+
# @param [String] dir The dir name relative to the root dir
|
337
|
+
#
|
338
|
+
# @yield [Component]
|
339
|
+
# @see [Component]
|
340
|
+
#
|
341
|
+
# @return [self]
|
342
|
+
#
|
343
|
+
# @api public
|
344
|
+
def auto_register!(dir, &block)
|
345
|
+
auto_registrar.(dir, &block)
|
346
|
+
self
|
347
|
+
end
|
348
|
+
|
349
|
+
# Builds injector for this container
|
350
|
+
#
|
351
|
+
# An injector is a useful mixin which injects dependencies into
|
352
|
+
# automatically defined constructor.
|
353
|
+
#
|
354
|
+
# @example
|
355
|
+
# # Define an injection mixin
|
356
|
+
# #
|
357
|
+
# # system/import.rb
|
358
|
+
# Import = MyApp.injector
|
359
|
+
#
|
360
|
+
# # Use it in your auto-registered classes
|
361
|
+
# #
|
362
|
+
# # lib/user_repo.rb
|
363
|
+
# require 'import'
|
364
|
+
#
|
365
|
+
# class UserRepo
|
366
|
+
# include Import['persistence.db']
|
367
|
+
# end
|
368
|
+
#
|
369
|
+
# MyApp['user_repo].db # instance under 'persistence.db' key
|
370
|
+
#
|
371
|
+
# @param options [Hash] injector options
|
372
|
+
#
|
373
|
+
# @api public
|
374
|
+
def injector(options = {})
|
375
|
+
Injector.new(self, options: options)
|
376
|
+
end
|
377
|
+
|
378
|
+
# Requires one or more files relative to the container's root
|
379
|
+
#
|
380
|
+
# @example
|
381
|
+
# # sinle file
|
382
|
+
# MyApp.require('lib/core')
|
383
|
+
#
|
384
|
+
# # glob
|
385
|
+
# MyApp.require('lib/**/*')
|
386
|
+
#
|
387
|
+
# @param *paths [Array<String>] one or more paths, supports globs too
|
388
|
+
#
|
389
|
+
# @api public
|
390
|
+
def require(*paths)
|
391
|
+
paths.flat_map { |path|
|
392
|
+
path.to_s.include?('*') ? Dir[root.join(path)] : root.join(path)
|
393
|
+
}.each { |path|
|
394
|
+
Kernel.require path.to_s
|
395
|
+
}
|
396
|
+
end
|
397
|
+
|
398
|
+
# Returns container's root path
|
399
|
+
#
|
400
|
+
# @example
|
401
|
+
# class MyApp < Dry::System::Container
|
402
|
+
# configure do |config|
|
403
|
+
# config.root = Pathname('/my/app')
|
404
|
+
# end
|
405
|
+
# end
|
406
|
+
#
|
407
|
+
# MyApp.root # returns '/my/app' pathname
|
408
|
+
#
|
409
|
+
# @return [Pathname]
|
410
|
+
#
|
411
|
+
# @api public
|
412
|
+
def root
|
413
|
+
config.root
|
414
|
+
end
|
415
|
+
|
416
|
+
# @api private
|
417
|
+
def load_paths
|
418
|
+
@load_paths ||= []
|
419
|
+
end
|
420
|
+
|
421
|
+
# @api private
|
422
|
+
def booter
|
423
|
+
@booter ||= config.booter.new(root.join("#{config.system_dir}/boot"))
|
424
|
+
end
|
425
|
+
|
426
|
+
# @api private
|
427
|
+
def auto_registrar
|
428
|
+
@auto_registrar ||= config.auto_registrar.new(self)
|
429
|
+
end
|
430
|
+
|
431
|
+
# @api private
|
432
|
+
def importer
|
433
|
+
@importer ||= config.importer.new(self)
|
434
|
+
end
|
435
|
+
|
436
|
+
# @api private
|
437
|
+
def component(key)
|
438
|
+
Component.new(
|
439
|
+
key,
|
440
|
+
loader: config.loader,
|
441
|
+
namespace: config.default_namespace,
|
442
|
+
separator: config.namespace_separator
|
443
|
+
)
|
444
|
+
end
|
445
|
+
|
446
|
+
# @api private
|
447
|
+
def require_component(component)
|
448
|
+
return if key?(component.identifier)
|
449
|
+
|
450
|
+
unless component.file_exists?(load_paths)
|
451
|
+
raise FileNotFoundError, component
|
452
|
+
end
|
453
|
+
|
454
|
+
Kernel.require(component.path) && yield
|
455
|
+
end
|
456
|
+
|
457
|
+
# @api private
|
458
|
+
def load_component(key)
|
459
|
+
return self if key?(key)
|
460
|
+
|
461
|
+
component(key).tap do |component|
|
462
|
+
root_key = component.root_key
|
463
|
+
|
464
|
+
if importer.key?(root_key)
|
465
|
+
load_external_component(component.namespaced(root_key))
|
466
|
+
else
|
467
|
+
load_local_component(component)
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
self
|
472
|
+
end
|
473
|
+
|
474
|
+
private
|
475
|
+
|
476
|
+
# @api private
|
477
|
+
def load_local_component(component, fallback = false)
|
478
|
+
if component.bootable?(booter.path) || component.file_exists?(load_paths)
|
479
|
+
booter.boot_dependency(component) unless frozen?
|
480
|
+
|
481
|
+
require_component(component) do
|
482
|
+
register(component.identifier) { component.instance }
|
483
|
+
end
|
484
|
+
elsif !fallback
|
485
|
+
load_local_component(component.prepend(config.default_namespace), true)
|
486
|
+
else
|
487
|
+
raise ComponentLoadError, component
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
# @api private
|
492
|
+
def load_external_component(component)
|
493
|
+
container = importer[component.namespace]
|
494
|
+
container.load_component(component.identifier)
|
495
|
+
importer.(component.namespace, container)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|