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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +39 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +34 -0
  5. data/.rubocop_todo.yml +26 -0
  6. data/.travis.yml +26 -0
  7. data/.yardopts +5 -0
  8. data/CHANGELOG.md +158 -0
  9. data/Gemfile +9 -0
  10. data/LICENSE +20 -0
  11. data/README.md +23 -0
  12. data/Rakefile +12 -0
  13. data/dry-system.gemspec +30 -0
  14. data/examples/standalone/Gemfile +5 -0
  15. data/examples/standalone/lib/user_repo.rb +5 -0
  16. data/examples/standalone/run.rb +7 -0
  17. data/examples/standalone/system/boot/persistence.rb +13 -0
  18. data/examples/standalone/system/container.rb +9 -0
  19. data/examples/standalone/system/import.rb +3 -0
  20. data/lib/dry-system.rb +1 -0
  21. data/lib/dry/system.rb +4 -0
  22. data/lib/dry/system/auto_registrar.rb +80 -0
  23. data/lib/dry/system/booter.rb +101 -0
  24. data/lib/dry/system/component.rb +167 -0
  25. data/lib/dry/system/constants.rb +9 -0
  26. data/lib/dry/system/container.rb +500 -0
  27. data/lib/dry/system/errors.rb +62 -0
  28. data/lib/dry/system/importer.rb +53 -0
  29. data/lib/dry/system/injector.rb +68 -0
  30. data/lib/dry/system/lifecycle.rb +104 -0
  31. data/lib/dry/system/loader.rb +69 -0
  32. data/lib/dry/system/version.rb +5 -0
  33. data/spec/fixtures/components/bar.rb +5 -0
  34. data/spec/fixtures/components/bar/baz.rb +4 -0
  35. data/spec/fixtures/components/foo.rb +2 -0
  36. data/spec/fixtures/import_test/config/application.yml +2 -0
  37. data/spec/fixtures/import_test/lib/test/bar.rb +4 -0
  38. data/spec/fixtures/import_test/lib/test/foo.rb +5 -0
  39. data/spec/fixtures/import_test/system/boot/bar.rb +11 -0
  40. data/spec/fixtures/lazytest/config/application.yml +2 -0
  41. data/spec/fixtures/lazytest/lib/test/dep.rb +4 -0
  42. data/spec/fixtures/lazytest/lib/test/foo.rb +5 -0
  43. data/spec/fixtures/lazytest/lib/test/models.rb +4 -0
  44. data/spec/fixtures/lazytest/lib/test/models/book.rb +6 -0
  45. data/spec/fixtures/lazytest/lib/test/models/user.rb +6 -0
  46. data/spec/fixtures/lazytest/system/boot/bar.rb +15 -0
  47. data/spec/fixtures/namespaced_components/namespaced/bar.rb +5 -0
  48. data/spec/fixtures/namespaced_components/namespaced/foo.rb +4 -0
  49. data/spec/fixtures/other/config/boot/bar.rb +11 -0
  50. data/spec/fixtures/other/lib/test/dep.rb +4 -0
  51. data/spec/fixtures/other/lib/test/foo.rb +5 -0
  52. data/spec/fixtures/other/lib/test/models.rb +4 -0
  53. data/spec/fixtures/other/lib/test/models/book.rb +6 -0
  54. data/spec/fixtures/other/lib/test/models/user.rb +6 -0
  55. data/spec/fixtures/test/config/application.yml +2 -0
  56. data/spec/fixtures/test/config/subapp.yml +2 -0
  57. data/spec/fixtures/test/lib/test/dep.rb +4 -0
  58. data/spec/fixtures/test/lib/test/foo.rb +5 -0
  59. data/spec/fixtures/test/lib/test/models.rb +4 -0
  60. data/spec/fixtures/test/lib/test/models/book.rb +6 -0
  61. data/spec/fixtures/test/lib/test/models/user.rb +6 -0
  62. data/spec/fixtures/test/lib/test/singleton_dep.rb +7 -0
  63. data/spec/fixtures/test/log/.gitkeep +0 -0
  64. data/spec/fixtures/test/system/boot/bar.rb +11 -0
  65. data/spec/fixtures/test/system/boot/client.rb +7 -0
  66. data/spec/fixtures/test/system/boot/db.rb +1 -0
  67. data/spec/fixtures/test/system/boot/logger.rb +5 -0
  68. data/spec/fixtures/umbrella/system/boot/db.rb +10 -0
  69. data/spec/integration/boot_spec.rb +18 -0
  70. data/spec/integration/import_spec.rb +63 -0
  71. data/spec/spec_helper.rb +47 -0
  72. data/spec/unit/component_spec.rb +116 -0
  73. data/spec/unit/container/auto_register_spec.rb +85 -0
  74. data/spec/unit/container/finalize_spec.rb +85 -0
  75. data/spec/unit/container/import_spec.rb +70 -0
  76. data/spec/unit/container/injector_spec.rb +29 -0
  77. data/spec/unit/container_spec.rb +165 -0
  78. data/spec/unit/injector_spec.rb +72 -0
  79. data/spec/unit/loader_spec.rb +64 -0
  80. metadata +295 -0
@@ -0,0 +1,9 @@
1
+ require 'dry/system/container'
2
+
3
+ class App < Dry::System::Container
4
+ configure do |config|
5
+ config.auto_register = %w(lib)
6
+ end
7
+
8
+ load_paths!('lib')
9
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'container'
2
+
3
+ Import = App.injector
@@ -0,0 +1 @@
1
+ require 'dry/system'
@@ -0,0 +1,4 @@
1
+ module Dry
2
+ module System
3
+ end
4
+ end
@@ -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,9 @@
1
+ module Dry
2
+ module System
3
+ RB_EXT = '.rb'.freeze
4
+ EMPTY_STRING = ''.freeze
5
+ PATH_SEPARATOR = '/'.freeze
6
+ DEFAULT_SEPARATOR = '.'.freeze
7
+ WORD_REGEX = /\w+/.freeze
8
+ end
9
+ 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