dry-system 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|