dry-system 0.15.0 → 0.19.1

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