dry-system 0.5.0

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