dry-system 0.15.0 → 0.19.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -2
  3. data/LICENSE +1 -1
  4. data/README.md +1 -1
  5. data/dry-system.gemspec +5 -4
  6. data/lib/dry-system.rb +1 -1
  7. data/lib/dry/system.rb +2 -2
  8. data/lib/dry/system/auto_registrar.rb +17 -59
  9. data/lib/dry/system/booter.rb +68 -41
  10. data/lib/dry/system/component.rb +62 -100
  11. data/lib/dry/system/component_dir.rb +128 -0
  12. data/lib/dry/system/components.rb +2 -2
  13. data/lib/dry/system/components/bootable.rb +6 -34
  14. data/lib/dry/system/components/config.rb +2 -2
  15. data/lib/dry/system/config/component_dir.rb +202 -0
  16. data/lib/dry/system/config/component_dirs.rb +184 -0
  17. data/lib/dry/system/constants.rb +5 -5
  18. data/lib/dry/system/container.rb +133 -184
  19. data/lib/dry/system/errors.rb +21 -16
  20. data/lib/dry/system/identifier.rb +157 -0
  21. data/lib/dry/system/lifecycle.rb +2 -2
  22. data/lib/dry/system/loader.rb +40 -41
  23. data/lib/dry/system/loader/autoloading.rb +26 -0
  24. data/lib/dry/system/magic_comments_parser.rb +2 -2
  25. data/lib/dry/system/manual_registrar.rb +1 -1
  26. data/lib/dry/system/plugins.rb +7 -7
  27. data/lib/dry/system/plugins/bootsnap.rb +3 -3
  28. data/lib/dry/system/plugins/dependency_graph.rb +3 -3
  29. data/lib/dry/system/plugins/dependency_graph/strategies.rb +1 -1
  30. data/lib/dry/system/plugins/logging.rb +5 -5
  31. data/lib/dry/system/plugins/monitoring.rb +3 -3
  32. data/lib/dry/system/plugins/monitoring/proxy.rb +3 -3
  33. data/lib/dry/system/plugins/notifications.rb +1 -1
  34. data/lib/dry/system/provider.rb +3 -3
  35. data/lib/dry/system/settings.rb +6 -6
  36. data/lib/dry/system/settings/file_loader.rb +2 -2
  37. data/lib/dry/system/settings/file_parser.rb +1 -1
  38. data/lib/dry/system/stubs.rb +1 -1
  39. data/lib/dry/system/system_components/settings.rb +1 -1
  40. data/lib/dry/system/version.rb +1 -1
  41. metadata +21 -25
  42. data/lib/dry/system/auto_registrar/configuration.rb +0 -43
@@ -0,0 +1,184 @@
1
+ require "concurrent/map"
2
+ require "dry/configurable"
3
+ require "dry/system/constants"
4
+ require "dry/system/errors"
5
+ require_relative "component_dir"
6
+
7
+ module Dry
8
+ module System
9
+ module Config
10
+ class ComponentDirs
11
+ include Dry::Configurable
12
+
13
+ # Settings from ComponentDir are configured here as defaults for all added dirs
14
+ ComponentDir._settings.each do |setting|
15
+ _settings << setting.dup
16
+ end
17
+
18
+ # @!group Settings
19
+
20
+ # @!method auto_register=(value)
21
+ #
22
+ # Sets a default `auto_register` for all added component dirs
23
+ #
24
+ # @see ComponentDir.auto_register
25
+ # @see auto_register
26
+ #
27
+ # @!method auto_register
28
+ #
29
+ # Returns the configured default `auto_register`
30
+ #
31
+ # @see auto_register=
32
+
33
+ # @!method add_to_load_path=(value)
34
+ #
35
+ # Sets a default `add_to_load_path` value for all added component dirs
36
+ #
37
+ # @see ComponentDir.add_to_load_path
38
+ # @see add_to_load_path
39
+ #
40
+ # @!method add_to_load_path
41
+ #
42
+ # Returns the configured default `add_to_load_path`
43
+ #
44
+ # @see add_to_load_path=
45
+
46
+ # @!method default_namespace=(value)
47
+ #
48
+ # Sets a default `default_namespace` value for all added component dirs
49
+ #
50
+ # @see ComponentDir.default_namespace
51
+ # @see default_namespace
52
+ #
53
+ # @!method default_namespace
54
+ #
55
+ # Returns the configured default `default_namespace`
56
+ #
57
+ # @see default_namespace=
58
+
59
+ # @!method loader=(value)
60
+ #
61
+ # Sets a default `loader` value for all added component dirs
62
+ #
63
+ # @see ComponentDir.loader
64
+ # @see loader
65
+ #
66
+ # @!method loader
67
+ #
68
+ # Returns the configured default `loader`
69
+ #
70
+ # @see loader=
71
+
72
+ # @!method memoize=(value)
73
+ #
74
+ # Sets a default `memoize` value for all added component dirs
75
+ #
76
+ # @see ComponentDir.memoize
77
+ # @see memoize
78
+ #
79
+ # @!method memoize
80
+ #
81
+ # Returns the configured default `memoize`
82
+ #
83
+ # @see memoize=
84
+
85
+ # @!endgroup
86
+
87
+ # @api private
88
+ def initialize
89
+ @dirs = Concurrent::Map.new
90
+ end
91
+
92
+ # @api private
93
+ def initialize_copy(source)
94
+ super
95
+ @dirs = source.dirs.dup
96
+ end
97
+
98
+ # Adds and configures a component dir
99
+ #
100
+ # @param path [String] the path for the component dir, relative to the configured
101
+ # container root
102
+ #
103
+ # @yieldparam dir [ComponentDir] the component dir to configure
104
+ #
105
+ # @return [ComponentDir] the added component dir
106
+ #
107
+ # @example
108
+ # component_dirs.add "lib" do |dir|
109
+ # dir.default_namespace = "my_app"
110
+ # end
111
+ #
112
+ # @see ComponentDir
113
+ def add(path)
114
+ raise ComponentDirAlreadyAddedError, path if dirs.key?(path)
115
+
116
+ dirs[path] = ComponentDir.new(path).tap do |dir|
117
+ apply_defaults_to_dir(dir)
118
+ yield dir if block_given?
119
+ end
120
+ end
121
+
122
+ # Returns the added component dirs, with default settings applied
123
+ #
124
+ # @return [Hash<String, ComponentDir>] the component dirs as a hash, keyed by path
125
+ def dirs
126
+ @dirs.each { |_, dir| apply_defaults_to_dir(dir) }
127
+ end
128
+
129
+ # Returns the added component dirs, with default settings applied
130
+ #
131
+ # @return [Array<ComponentDir>]
132
+ def to_a
133
+ dirs.values
134
+ end
135
+
136
+ # Calls the given block once for each added component dir, passing the dir as an
137
+ # argument.
138
+ #
139
+ # @yieldparam dir [ComponentDir] the yielded component dir
140
+ def each(&block)
141
+ to_a.each(&block)
142
+ end
143
+
144
+ private
145
+
146
+ # Apply default settings to a component dir. This is run every time the dirs are
147
+ # accessed to ensure defaults are applied regardless of when new component dirs
148
+ # are added. This method must be idempotent.
149
+ #
150
+ # @return [void]
151
+ def apply_defaults_to_dir(dir)
152
+ dir.config.values.each do |key, _value|
153
+ if configured?(key) && !dir.configured?(key)
154
+ dir.public_send(:"#{key}=", public_send(key))
155
+ end
156
+ end
157
+ end
158
+
159
+ # Returns true if a setting has been explicitly configured and is not returning
160
+ # just a default value.
161
+ #
162
+ # This is used to determine which settings should be applied to added component
163
+ # dirs as additional defaults.
164
+ #
165
+ # @api private
166
+ def configured?(key)
167
+ config._settings[key].input_defined?
168
+ end
169
+
170
+ def method_missing(name, *args, &block)
171
+ if config.respond_to?(name)
172
+ config.public_send(name, *args, &block)
173
+ else
174
+ super
175
+ end
176
+ end
177
+
178
+ def respond_to_missing?(name, include_all = false)
179
+ config.respond_to?(name) || super
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/constants'
3
+ require "dry/core/constants"
4
4
 
5
5
  module Dry
6
6
  module System
7
7
  include Dry::Core::Constants
8
8
 
9
- RB_EXT = '.rb'
10
- RB_GLOB = '*.rb'
11
- PATH_SEPARATOR = '/'
12
- DEFAULT_SEPARATOR = '.'
9
+ RB_EXT = ".rb"
10
+ RB_GLOB = "*.rb"
11
+ PATH_SEPARATOR = "/"
12
+ DEFAULT_SEPARATOR = "."
13
13
  WORD_REGEX = /\w+/.freeze
14
14
  end
15
15
  end
@@ -1,24 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pathname'
4
-
5
- require 'dry-auto_inject'
6
- require 'dry-configurable'
7
- require 'dry-container'
8
- require 'dry/inflector'
9
-
10
- require 'dry/core/deprecations'
11
-
12
- require 'dry/system'
13
- require 'dry/system/errors'
14
- require 'dry/system/loader'
15
- require 'dry/system/booter'
16
- require 'dry/system/auto_registrar'
17
- require 'dry/system/manual_registrar'
18
- require 'dry/system/importer'
19
- require 'dry/system/component'
20
- require 'dry/system/constants'
21
- require 'dry/system/plugins'
3
+ require "pathname"
4
+
5
+ require "dry-auto_inject"
6
+ require "dry-configurable"
7
+ require "dry-container"
8
+ require "dry/inflector"
9
+
10
+ require "dry/core/deprecations"
11
+
12
+ require "dry/system"
13
+ require "dry/system/errors"
14
+ require "dry/system/booter"
15
+ require "dry/system/auto_registrar"
16
+ require "dry/system/manual_registrar"
17
+ require "dry/system/importer"
18
+ require "dry/system/component"
19
+ require "dry/system/constants"
20
+ require "dry/system/plugins"
21
+
22
+ require_relative "component_dir"
23
+ require_relative "config/component_dirs"
22
24
 
23
25
  module Dry
24
26
  module System
@@ -48,8 +50,6 @@ module Dry
48
50
  #
49
51
  # * `:name` - a unique container identifier
50
52
  # * `:root` - a system root directory (defaults to `pwd`)
51
- # * `:system_dir` - directory name relative to root, where bootable components
52
- # can be defined in `boot` dir this defaults to `system`
53
53
  #
54
54
  # @example
55
55
  # class MyApp < Dry::System::Container
@@ -63,7 +63,7 @@ module Dry
63
63
  # end
64
64
  #
65
65
  # # this will configure $LOAD_PATH to include your `lib` dir
66
- # load_paths!('lib')
66
+ # add_dirs_to_load_paths!('lib')
67
67
  # end
68
68
  #
69
69
  # @api public
@@ -73,13 +73,12 @@ module Dry
73
73
  extend Dry::System::Plugins
74
74
 
75
75
  setting :name
76
- setting :default_namespace
77
76
  setting(:root, Pathname.pwd.freeze) { |path| Pathname(path) }
78
- setting :system_dir, 'system'
79
- setting :registrations_dir, 'container'
80
- setting :auto_register, []
77
+ setting :system_dir, "system"
78
+ setting :bootable_dirs, ["system/boot"]
79
+ setting :registrations_dir, "container"
80
+ setting :component_dirs, Config::ComponentDirs.new, cloneable: true
81
81
  setting :inflector, Dry::Inflector.new
82
- setting :loader, Dry::System::Loader
83
82
  setting :booter, Dry::System::Booter
84
83
  setting :auto_registrar, Dry::System::AutoRegistrar
85
84
  setting :manual_registrar, Dry::System::ManualRegistrar
@@ -95,7 +94,19 @@ module Dry
95
94
  end
96
95
  end
97
96
 
98
- extend Dry::Core::Deprecations['Dry::System::Container']
97
+ extend Dry::Core::Deprecations["Dry::System::Container"]
98
+
99
+ # Define a new configuration setting
100
+ #
101
+ # @see https://dry-rb.org/gems/dry-configurable
102
+ #
103
+ # @api public
104
+ def setting(name, *args, &block)
105
+ super(name, *args, &block)
106
+ # TODO: dry-configurable needs a public API for this
107
+ config._settings << _settings[name]
108
+ self
109
+ end
99
110
 
100
111
  # Configures the container
101
112
  #
@@ -114,7 +125,6 @@ module Dry
114
125
  def configure(&block)
115
126
  hooks[:before_configure].each { |hook| instance_eval(&hook) }
116
127
  super(&block)
117
- load_paths!(config.system_dir)
118
128
  hooks[:after_configure].each { |hook| instance_eval(&hook) }
119
129
  self
120
130
  end
@@ -158,9 +168,10 @@ module Dry
158
168
 
159
169
  # Registers finalization function for a bootable component
160
170
  #
161
- # By convention, boot files for components should be placed in
162
- # `%{system_dir}/boot` and they will be loaded on demand when components
163
- # are loaded in isolation, or during finalization process.
171
+ # By convention, boot files for components should be placed in a
172
+ # `bootable_dirs` entry and they will be loaded on demand when
173
+ # components are loaded in isolation, or during the finalization
174
+ # process.
164
175
  #
165
176
  # @example
166
177
  # # system/container.rb
@@ -243,30 +254,24 @@ module Dry
243
254
  boot_local(name, **opts, &block)
244
255
  end
245
256
 
257
+ booter.register_component component
258
+
246
259
  components[name] = component
247
260
  end
248
261
  deprecate :finalize, :boot
249
262
 
250
263
  # @api private
251
264
  def boot_external(identifier, from:, key: nil, namespace: nil, &block)
252
- component = System.providers[from].component(
265
+ System.providers[from].component(
253
266
  identifier, key: key, namespace: namespace, finalize: block, container: self
254
267
  )
255
-
256
- booter.register_component(component)
257
-
258
- component
259
268
  end
260
269
 
261
270
  # @api private
262
271
  def boot_local(identifier, namespace: nil, &block)
263
- component = Components::Bootable.new(
272
+ Components::Bootable.new(
264
273
  identifier, container: self, namespace: namespace, &block
265
274
  )
266
-
267
- booter.register_component(component)
268
-
269
- component
270
275
  end
271
276
 
272
277
  # Return if a container was finalized
@@ -378,7 +383,7 @@ module Dry
378
383
  self
379
384
  end
380
385
 
381
- # Sets load paths relative to the container's root dir
386
+ # Adds the directories (relative to the container's root) to the Ruby load path
382
387
  #
383
388
  # @example
384
389
  # class MyApp < Dry::System::Container
@@ -386,7 +391,7 @@ module Dry
386
391
  # # ...
387
392
  # end
388
393
  #
389
- # load_paths!('lib')
394
+ # add_to_load_path!('lib')
390
395
  # end
391
396
  #
392
397
  # @param [Array<String>] dirs
@@ -394,12 +399,9 @@ module Dry
394
399
  # @return [self]
395
400
  #
396
401
  # @api public
397
- def load_paths!(*dirs)
398
- dirs.map(&root.method(:join)).each do |path|
399
- next if load_paths.include?(path)
400
-
401
- load_paths << path
402
- $LOAD_PATH.unshift(path.to_s)
402
+ def add_to_load_path!(*dirs)
403
+ dirs.reverse.map(&root.method(:join)).each do |path|
404
+ $LOAD_PATH.prepend(path.to_s) unless $LOAD_PATH.include?(path.to_s)
403
405
  end
404
406
  self
405
407
  end
@@ -410,47 +412,6 @@ module Dry
410
412
  self
411
413
  end
412
414
 
413
- # Auto-registers components from the provided directory
414
- #
415
- # Typically you want to configure auto_register directories, and it will
416
- # work automatically. Use this method in cases where you want to have an
417
- # explicit way where some components are auto-registered, or if you want
418
- # to exclude some components from being auto-registered
419
- #
420
- # @example
421
- # class MyApp < Dry::System::Container
422
- # configure do |config|
423
- # # ...
424
- # end
425
- #
426
- # # with a dir
427
- # auto_register!('lib/core')
428
- #
429
- # # with a dir and a custom registration block
430
- # auto_register!('lib/core') do |config|
431
- # config.instance do |component|
432
- # # custom way of initializing a component
433
- # end
434
- #
435
- # config.exclude do |component|
436
- # # return true to exclude component from auto-registration
437
- # end
438
- # end
439
- # end
440
- #
441
- # @param [String] dir The dir name relative to the root dir
442
- #
443
- # @yield AutoRegistrar::Configuration
444
- # @see AutoRegistrar::Configuration
445
- #
446
- # @return [self]
447
- #
448
- # @api public
449
- def auto_register!(dir, &block)
450
- auto_registrar.(dir, &block)
451
- self
452
- end
453
-
454
415
  # Builds injector for this container
455
416
  #
456
417
  # An injector is a useful mixin which injects dependencies into
@@ -476,7 +437,7 @@ module Dry
476
437
  # @param options [Hash] injector options
477
438
  #
478
439
  # @api public
479
- def injector(options = { strategies: strategies })
440
+ def injector(options = {strategies: strategies})
480
441
  Dry::AutoInject(self, options)
481
442
  end
482
443
 
@@ -494,7 +455,7 @@ module Dry
494
455
  # @api public
495
456
  def require_from_root(*paths)
496
457
  paths.flat_map { |path|
497
- path.to_s.include?('*') ? ::Dir[root.join(path)].sort : root.join(path)
458
+ path.to_s.include?("*") ? ::Dir[root.join(path)].sort : root.join(path)
498
459
  }.each { |path|
499
460
  Kernel.require path.to_s
500
461
  }
@@ -519,8 +480,8 @@ module Dry
519
480
  end
520
481
 
521
482
  # @api public
522
- def resolve(key, &block)
523
- load_component(key, &block) unless finalized?
483
+ def resolve(key)
484
+ load_component(key) unless finalized?
524
485
 
525
486
  super
526
487
  end
@@ -551,18 +512,26 @@ module Dry
551
512
  end
552
513
 
553
514
  # @api private
554
- def load_paths
555
- @load_paths ||= []
515
+ def component_dirs
516
+ config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) }
556
517
  end
557
518
 
558
519
  # @api private
559
520
  def booter
560
- @booter ||= config.booter.new(boot_path)
521
+ @booter ||= config.booter.new(boot_paths)
561
522
  end
562
523
 
563
524
  # @api private
564
- def boot_path
565
- root.join("#{config.system_dir}/boot")
525
+ def boot_paths
526
+ config.bootable_dirs.map { |dir|
527
+ dir = Pathname(dir)
528
+
529
+ if dir.relative?
530
+ root.join(dir)
531
+ else
532
+ dir
533
+ end
534
+ }
566
535
  end
567
536
 
568
537
  # @api private
@@ -580,67 +549,6 @@ module Dry
580
549
  @importer ||= config.importer.new(self)
581
550
  end
582
551
 
583
- # @api private
584
- def component(identifier, **options)
585
- if (component = booter.components.detect { |c| c.identifier == identifier })
586
- component
587
- else
588
- Component.new(
589
- identifier,
590
- loader: config.loader,
591
- namespace: config.default_namespace,
592
- separator: config.namespace_separator,
593
- inflector: config.inflector,
594
- **options
595
- )
596
- end
597
- end
598
-
599
- # @api private
600
- def require_component(component)
601
- return if registered?(component.identifier)
602
-
603
- raise FileNotFoundError, component unless component.file_exists?(load_paths)
604
-
605
- require_path(component.path)
606
-
607
- yield
608
- end
609
-
610
- # Allows subclasses to use a different strategy for required files.
611
- #
612
- # E.g. apps that use `ActiveSupport::Dependencies::Loadable#require_dependency`
613
- # will override this method to allow container managed dependencies to be reloaded
614
- # for non-finalized containers.
615
- #
616
- # @api private
617
- def require_path(path)
618
- require path
619
- end
620
-
621
- # @api private
622
- def load_component(key, &block)
623
- return self if registered?(key)
624
-
625
- component(key).tap do |component|
626
- if component.boot?
627
- booter.start(component)
628
- else
629
- root_key = component.root_key
630
-
631
- if (bootable_dep = component(root_key)).boot?
632
- booter.start(bootable_dep)
633
- elsif importer.key?(root_key)
634
- load_imported_component(component.namespaced(root_key))
635
- end
636
-
637
- load_local_component(component, &block) unless registered?(key)
638
- end
639
- end
640
-
641
- self
642
- end
643
-
644
552
  # @api private
645
553
  def after(event, &block)
646
554
  hooks[:"after_#{event}"] << block
@@ -657,46 +565,87 @@ module Dry
657
565
 
658
566
  # @api private
659
567
  def inherited(klass)
660
- new_hooks = Container.hooks.dup
661
-
662
568
  hooks.each do |event, blocks|
663
- new_hooks[event].concat(blocks)
664
- new_hooks[event].concat(klass.hooks[event])
569
+ klass.hooks[event].concat blocks.dup
665
570
  end
666
571
 
667
- klass.instance_variable_set(:@hooks, new_hooks)
668
572
  klass.instance_variable_set(:@__finalized__, false)
573
+
669
574
  super
670
575
  end
671
576
 
672
- private
577
+ protected
673
578
 
674
579
  # @api private
675
- def load_local_component(component, default_namespace_fallback = false, &block)
676
- if booter.bootable?(component) || component.file_exists?(load_paths)
677
- booter.boot_dependency(component) unless finalized?
580
+ def load_component(key)
581
+ return self if registered?(key)
678
582
 
679
- require_component(component) do
680
- register(component.identifier) { component.instance }
681
- end
682
- elsif !default_namespace_fallback
683
- load_local_component(component.prepend(config.default_namespace), true, &block)
583
+ component = component(key)
584
+
585
+ if component.bootable?
586
+ booter.start(component)
587
+ return self
588
+ end
589
+
590
+ booter.boot_dependency(component)
591
+ return self if registered?(key)
592
+
593
+ if component.file_exists?
594
+ load_local_component(component)
684
595
  elsif manual_registrar.file_exists?(component)
685
596
  manual_registrar.(component)
686
- elsif block_given?
687
- yield
688
- else
689
- raise ComponentLoadError, component
597
+ elsif importer.key?(component.identifier.root_key)
598
+ load_imported_component(component.identifier)
690
599
  end
600
+
601
+ self
691
602
  end
692
603
 
693
- # @api private
694
- def load_imported_component(component)
695
- container = importer[component.namespace]
696
- container.load_component(component.identifier)
697
- importer.(component.namespace, container)
604
+ private
605
+
606
+ def load_local_component(component)
607
+ if component.auto_register?
608
+ register(component.identifier, memoize: component.memoize?) { component.instance }
609
+ end
610
+ end
611
+
612
+ def load_imported_component(identifier)
613
+ import_namespace = identifier.root_key
614
+
615
+ container = importer[import_namespace]
616
+
617
+ container.load_component(identifier.dequalified(import_namespace).key)
618
+
619
+ importer.(import_namespace, container)
620
+ end
621
+
622
+ def component(identifier)
623
+ if (bootable_component = booter.find_component(identifier))
624
+ return bootable_component
625
+ end
626
+
627
+ # Find the first matching component from within the configured component dirs.
628
+ # If no matching component is found, return a plain component instance with no
629
+ # associated file path. This fallback is important because the component may
630
+ # still be loadable via the manual registrar or an imported container.
631
+ component_dirs.detect { |dir|
632
+ if (component = dir.component_for_identifier(identifier))
633
+ break component
634
+ end
635
+ } || Component.new(identifier)
698
636
  end
699
637
  end
638
+
639
+ # Default hooks
640
+ after :configure do
641
+ # Add appropriately configured component dirs to the load path
642
+ #
643
+ # Do this in a single pass to preserve ordering (i.e. earliest dirs win)
644
+ paths = config.component_dirs.to_a.each_with_object([]) { |dir, arr|
645
+ arr << dir.path if dir.add_to_load_path
646
+ }
647
+ add_to_load_path!(*paths)
648
+ end
700
649
  end
701
650
  end
702
651
  end