dry-system 0.18.2 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,11 +7,11 @@ require "dry-configurable"
7
7
  require "dry-container"
8
8
  require "dry/inflector"
9
9
 
10
+ require "dry/core/constants"
10
11
  require "dry/core/deprecations"
11
12
 
12
13
  require "dry/system"
13
14
  require "dry/system/errors"
14
- require "dry/system/loader"
15
15
  require "dry/system/booter"
16
16
  require "dry/system/auto_registrar"
17
17
  require "dry/system/manual_registrar"
@@ -20,6 +20,9 @@ require "dry/system/component"
20
20
  require "dry/system/constants"
21
21
  require "dry/system/plugins"
22
22
 
23
+ require_relative "component_dir"
24
+ require_relative "config/component_dirs"
25
+
23
26
  module Dry
24
27
  module System
25
28
  # Abstract container class to inherit from
@@ -61,7 +64,7 @@ module Dry
61
64
  # end
62
65
  #
63
66
  # # this will configure $LOAD_PATH to include your `lib` dir
64
- # load_paths!('lib')
67
+ # add_dirs_to_load_paths!('lib')
65
68
  # end
66
69
  #
67
70
  # @api public
@@ -71,19 +74,17 @@ module Dry
71
74
  extend Dry::System::Plugins
72
75
 
73
76
  setting :name
74
- setting :default_namespace
75
- setting(:root, Pathname.pwd.freeze) { |path| Pathname(path) }
76
- setting :system_dir, "system"
77
- setting :bootable_dirs, ["system/boot"]
78
- setting :registrations_dir, "container"
79
- setting :auto_register, []
80
- setting :inflector, Dry::Inflector.new
81
- setting :loader, Dry::System::Loader
82
- setting :booter, Dry::System::Booter
83
- setting :auto_registrar, Dry::System::AutoRegistrar
84
- setting :manual_registrar, Dry::System::ManualRegistrar
85
- setting :importer, Dry::System::Importer
86
- setting(:components, {}, reader: true, &:dup)
77
+ setting :root, default: Pathname.pwd.freeze, constructor: -> path { Pathname(path) }
78
+ setting :system_dir, default: "system"
79
+ setting :bootable_dirs, default: ["system/boot"]
80
+ setting :registrations_dir, default: "container"
81
+ setting :component_dirs, default: Config::ComponentDirs.new, cloneable: true
82
+ setting :inflector, default: Dry::Inflector.new
83
+ setting :booter, default: Dry::System::Booter
84
+ setting :auto_registrar, default: Dry::System::AutoRegistrar
85
+ setting :manual_registrar, default: Dry::System::ManualRegistrar
86
+ setting :importer, default: Dry::System::Importer
87
+ setting :components, default: {}, reader: true, constructor: :dup.to_proc
87
88
 
88
89
  class << self
89
90
  def strategies(value = nil)
@@ -101,13 +102,12 @@ module Dry
101
102
  # @see https://dry-rb.org/gems/dry-configurable
102
103
  #
103
104
  # @api public
104
- def setting(name, *args, &block)
105
- super(name, *args, &block)
105
+ def setting(name, default = Dry::Core::Constants::Undefined, **options, &block)
106
+ super(name, default, **options, &block)
106
107
  # TODO: dry-configurable needs a public API for this
107
108
  config._settings << _settings[name]
108
109
  self
109
110
  end
110
- ruby2_keywords(:setting) if respond_to?(:ruby2_keywords, true)
111
111
 
112
112
  # Configures the container
113
113
  #
@@ -126,7 +126,6 @@ module Dry
126
126
  def configure(&block)
127
127
  hooks[:before_configure].each { |hook| instance_eval(&hook) }
128
128
  super(&block)
129
- load_paths!(config.system_dir)
130
129
  hooks[:after_configure].each { |hook| instance_eval(&hook) }
131
130
  self
132
131
  end
@@ -385,7 +384,7 @@ module Dry
385
384
  self
386
385
  end
387
386
 
388
- # Sets load paths relative to the container's root dir
387
+ # Adds the directories (relative to the container's root) to the Ruby load path
389
388
  #
390
389
  # @example
391
390
  # class MyApp < Dry::System::Container
@@ -393,7 +392,7 @@ module Dry
393
392
  # # ...
394
393
  # end
395
394
  #
396
- # load_paths!('lib')
395
+ # add_to_load_path!('lib')
397
396
  # end
398
397
  #
399
398
  # @param [Array<String>] dirs
@@ -401,12 +400,9 @@ module Dry
401
400
  # @return [self]
402
401
  #
403
402
  # @api public
404
- def load_paths!(*dirs)
405
- dirs.map(&root.method(:join)).each do |path|
406
- next if load_paths.include?(path)
407
-
408
- load_paths << path
409
- $LOAD_PATH.unshift(path.to_s)
403
+ def add_to_load_path!(*dirs)
404
+ dirs.reverse.map(&root.method(:join)).each do |path|
405
+ $LOAD_PATH.prepend(path.to_s) unless $LOAD_PATH.include?(path.to_s)
410
406
  end
411
407
  self
412
408
  end
@@ -417,47 +413,6 @@ module Dry
417
413
  self
418
414
  end
419
415
 
420
- # Auto-registers components from the provided directory
421
- #
422
- # Typically you want to configure auto_register directories, and it will
423
- # work automatically. Use this method in cases where you want to have an
424
- # explicit way where some components are auto-registered, or if you want
425
- # to exclude some components from being auto-registered
426
- #
427
- # @example
428
- # class MyApp < Dry::System::Container
429
- # configure do |config|
430
- # # ...
431
- # end
432
- #
433
- # # with a dir
434
- # auto_register!('lib/core')
435
- #
436
- # # with a dir and a custom registration block
437
- # auto_register!('lib/core') do |config|
438
- # config.instance do |component|
439
- # # custom way of initializing a component
440
- # end
441
- #
442
- # config.exclude do |component|
443
- # # return true to exclude component from auto-registration
444
- # end
445
- # end
446
- # end
447
- #
448
- # @param [String] dir The dir name relative to the root dir
449
- #
450
- # @yield AutoRegistrar::Configuration
451
- # @see AutoRegistrar::Configuration
452
- #
453
- # @return [self]
454
- #
455
- # @api public
456
- def auto_register!(dir, &block)
457
- auto_registrar.(dir, &block)
458
- self
459
- end
460
-
461
416
  # Builds injector for this container
462
417
  #
463
418
  # An injector is a useful mixin which injects dependencies into
@@ -526,8 +481,8 @@ module Dry
526
481
  end
527
482
 
528
483
  # @api public
529
- def resolve(key, &block)
530
- load_component(key, &block) unless finalized?
484
+ def resolve(key)
485
+ load_component(key) unless finalized?
531
486
 
532
487
  super
533
488
  end
@@ -558,8 +513,8 @@ module Dry
558
513
  end
559
514
 
560
515
  # @api private
561
- def load_paths
562
- @load_paths ||= []
516
+ def component_dirs
517
+ config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) }
563
518
  end
564
519
 
565
520
  # @api private
@@ -595,67 +550,6 @@ module Dry
595
550
  @importer ||= config.importer.new(self)
596
551
  end
597
552
 
598
- # @api private
599
- def component(identifier, **options)
600
- if (component = booter.components.detect { |c| c.identifier == identifier })
601
- component
602
- else
603
- Component.new(
604
- identifier,
605
- loader: config.loader,
606
- namespace: config.default_namespace,
607
- separator: config.namespace_separator,
608
- inflector: config.inflector,
609
- **options
610
- )
611
- end
612
- end
613
-
614
- # @api private
615
- def require_component(component)
616
- return if registered?(component.identifier)
617
-
618
- raise FileNotFoundError, component unless component.file_exists?(load_paths)
619
-
620
- require_path(component.path)
621
-
622
- yield
623
- end
624
-
625
- # Allows subclasses to use a different strategy for required files.
626
- #
627
- # E.g. apps that use `ActiveSupport::Dependencies::Loadable#require_dependency`
628
- # will override this method to allow container managed dependencies to be reloaded
629
- # for non-finalized containers.
630
- #
631
- # @api private
632
- def require_path(path)
633
- require path
634
- end
635
-
636
- # @api private
637
- def load_component(key, &block)
638
- return self if registered?(key)
639
-
640
- component(key).tap do |component|
641
- if component.bootable?
642
- booter.start(component)
643
- else
644
- root_key = component.root_key
645
-
646
- if (root_bootable = component(root_key)).bootable?
647
- booter.start(root_bootable)
648
- elsif importer.key?(root_key)
649
- load_imported_component(component.namespaced(root_key))
650
- end
651
-
652
- load_local_component(component, &block) unless registered?(key)
653
- end
654
- end
655
-
656
- self
657
- end
658
-
659
553
  # @api private
660
554
  def after(event, &block)
661
555
  hooks[:"after_#{event}"] << block
@@ -672,46 +566,87 @@ module Dry
672
566
 
673
567
  # @api private
674
568
  def inherited(klass)
675
- new_hooks = Container.hooks.dup
676
-
677
569
  hooks.each do |event, blocks|
678
- new_hooks[event].concat(blocks)
679
- new_hooks[event].concat(klass.hooks[event])
570
+ klass.hooks[event].concat blocks.dup
680
571
  end
681
572
 
682
- klass.instance_variable_set(:@hooks, new_hooks)
683
573
  klass.instance_variable_set(:@__finalized__, false)
574
+
684
575
  super
685
576
  end
686
577
 
687
- private
578
+ protected
688
579
 
689
580
  # @api private
690
- def load_local_component(component, default_namespace_fallback = false, &block)
691
- if booter.bootable?(component) || component.file_exists?(load_paths)
692
- booter.boot_dependency(component) unless finalized?
581
+ def load_component(key)
582
+ return self if registered?(key)
693
583
 
694
- require_component(component) do
695
- register(component.identifier) { component.instance }
696
- end
697
- elsif !default_namespace_fallback
698
- load_local_component(component.prepend(config.default_namespace), true, &block)
584
+ component = component(key)
585
+
586
+ if component.bootable?
587
+ booter.start(component)
588
+ return self
589
+ end
590
+
591
+ booter.boot_dependency(component)
592
+ return self if registered?(key)
593
+
594
+ if component.file_exists?
595
+ load_local_component(component)
699
596
  elsif manual_registrar.file_exists?(component)
700
597
  manual_registrar.(component)
701
- elsif block_given?
702
- yield
703
- else
704
- raise ComponentLoadError, component
598
+ elsif importer.key?(component.identifier.root_key)
599
+ load_imported_component(component.identifier)
705
600
  end
601
+
602
+ self
706
603
  end
707
604
 
708
- # @api private
709
- def load_imported_component(component)
710
- container = importer[component.namespace]
711
- container.load_component(component.identifier)
712
- importer.(component.namespace, container)
605
+ private
606
+
607
+ def load_local_component(component)
608
+ if component.auto_register?
609
+ register(component.identifier, memoize: component.memoize?) { component.instance }
610
+ end
611
+ end
612
+
613
+ def load_imported_component(identifier)
614
+ import_namespace = identifier.root_key
615
+
616
+ container = importer[import_namespace]
617
+
618
+ container.load_component(identifier.dequalified(import_namespace).key)
619
+
620
+ importer.(import_namespace, container)
621
+ end
622
+
623
+ def component(identifier)
624
+ if (bootable_component = booter.find_component(identifier))
625
+ return bootable_component
626
+ end
627
+
628
+ # Find the first matching component from within the configured component dirs.
629
+ # If no matching component is found, return a plain component instance with no
630
+ # associated file path. This fallback is important because the component may
631
+ # still be loadable via the manual registrar or an imported container.
632
+ component_dirs.detect { |dir|
633
+ if (component = dir.component_for_identifier(identifier))
634
+ break component
635
+ end
636
+ } || Component.new(identifier)
713
637
  end
714
638
  end
639
+
640
+ # Default hooks
641
+ after :configure do
642
+ # Add appropriately configured component dirs to the load path
643
+ #
644
+ # Do this in a single pass to preserve ordering (i.e. earliest dirs win)
645
+ paths = config.component_dirs.to_a.each_with_object([]) { |dir, arr|
646
+ arr << dir.path if dir.add_to_load_path
647
+ }
648
+ add_to_load_path!(*paths)
649
+ end
715
650
  end
716
651
  end
717
652
  end
@@ -2,13 +2,22 @@
2
2
 
3
3
  module Dry
4
4
  module System
5
+ # Error raised when a component dir is added to configuration more than once
6
+ #
7
+ # @api public
8
+ ComponentDirAlreadyAddedError = Class.new(StandardError) do
9
+ def initialize(dir)
10
+ super("Component directory #{dir.inspect} already added")
11
+ end
12
+ end
13
+
5
14
  # Error raised when the container tries to load a component with missing
6
15
  # file
7
16
  #
8
17
  # @api public
9
18
  FileNotFoundError = Class.new(StandardError) do
10
19
  def initialize(component)
11
- super("could not resolve require file for #{component.identifier}")
20
+ super("could not resolve require file for component '#{component.identifier}'")
12
21
  end
13
22
  end
14
23
 
@@ -18,20 +27,11 @@ module Dry
18
27
  ComponentFileMismatchError = Class.new(StandardError) do
19
28
  def initialize(component)
20
29
  super(<<-STR)
21
- Bootable component #{component.identifier.inspect} not found
30
+ Bootable component '#{component.identifier}' not found
22
31
  STR
23
32
  end
24
33
  end
25
34
 
26
- # Error raised when a resolved component couldn't be found
27
- #
28
- # @api public
29
- ComponentLoadError = Class.new(StandardError) do
30
- def initialize(component)
31
- super("could not load component #{component.inspect}")
32
- end
33
- end
34
-
35
35
  # Error raised when resolved component couldn't be loaded
36
36
  #
37
37
  # @api public
@@ -81,8 +81,17 @@ module Dry
81
81
  end
82
82
  end
83
83
 
84
- ComponentsDirMissing = Class.new(StandardError)
84
+ # Error raised when a configured component directory could not be found
85
+ #
86
+ # @api public
87
+ ComponentDirNotFoundError = Class.new(StandardError) do
88
+ def initialize(dir)
89
+ super("Component dir '#{dir}' not found")
90
+ end
91
+ end
92
+
85
93
  DuplicatedComponentKeyError = Class.new(ArgumentError)
94
+
86
95
  InvalidSettingsError = Class.new(ArgumentError) do
87
96
  # @api private
88
97
  def initialize(attributes)
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require_relative "constants"
5
+
6
+ module Dry
7
+ module System
8
+ # An identifier representing a component to be registered.
9
+ #
10
+ # Components are eventually registered in the container using plain string
11
+ # identifiers, available as the `identifier` or `key` attribute here. Additional
12
+ # methods are provided to make it easier to evaluate or manipulate these identifiers.
13
+ #
14
+ # @api public
15
+ class Identifier
16
+ include Dry::Equalizer(:identifier, :namespace, :separator)
17
+
18
+ # @return [String] the identifier string
19
+ # @api public
20
+ attr_reader :identifier
21
+
22
+ # @return [String, nil] the namespace for the component
23
+ # @api public
24
+ attr_reader :namespace
25
+
26
+ # @return [String] the configured namespace separator
27
+ # @api public
28
+ attr_reader :separator
29
+
30
+ # @api private
31
+ def initialize(identifier, namespace: nil, separator: DEFAULT_SEPARATOR)
32
+ @identifier = identifier.to_s
33
+ @namespace = namespace
34
+ @separator = separator
35
+ end
36
+
37
+ # @!method key
38
+ # Returns the identifier string
39
+ #
40
+ # @return [String]
41
+ # @see #identifier
42
+ # @api public
43
+ alias_method :key, :identifier
44
+
45
+ # @!method to_s
46
+ # Returns the identifier string
47
+ #
48
+ # @return [String]
49
+ # @see #identifier
50
+ # @api public
51
+ alias_method :to_s, :identifier
52
+
53
+ # Returns the root namespace segment of the identifier string, as a symbol
54
+ #
55
+ # @example
56
+ # identifier.key # => "articles.operations.create"
57
+ # identifier.root_key # => :articles
58
+ #
59
+ # @return [Symbol] the root key
60
+ # @api public
61
+ def root_key
62
+ segments.first.to_sym
63
+ end
64
+
65
+ # Returns a path-delimited representation of the identifier, with the namespace
66
+ # incorporated. This path is intended for usage when requiring the component's
67
+ # source file.
68
+ #
69
+ # @example
70
+ # identifier.key # => "articles.operations.create"
71
+ # identifier.namespace # => "admin"
72
+ #
73
+ # identifier.path # => "admin/articles/operations/create"
74
+ #
75
+ # @return [String] the path
76
+ # @api public
77
+ def path
78
+ @require_path ||= identifier.gsub(separator, PATH_SEPARATOR).yield_self { |path|
79
+ if namespace
80
+ namespace_path = namespace.to_s.gsub(separator, PATH_SEPARATOR)
81
+ "#{namespace_path}#{PATH_SEPARATOR}#{path}"
82
+ else
83
+ path
84
+ end
85
+ }
86
+ end
87
+
88
+ # Returns true if the given namespace prefix is part of the identifier's leading
89
+ # namespaces
90
+ #
91
+ # @example
92
+ # identifier.key # => "articles.operations.create"
93
+ #
94
+ # identifier.start_with?("articles.operations") # => true
95
+ # identifier.start_with?("articles") # => true
96
+ # identifier.start_with?("article") # => false
97
+ #
98
+ # @param leading_namespaces [String] the one or more leading namespaces to check
99
+ # @return [Boolean]
100
+ # @api public
101
+ def start_with?(leading_namespaces)
102
+ identifier.start_with?("#{leading_namespaces}#{separator}") ||
103
+ identifier.eql?(leading_namespaces)
104
+ end
105
+
106
+ # Returns a copy of the identifier with the given leading namespaces removed from
107
+ # the identifier string.
108
+ #
109
+ # Additional options may be provided, which are passed to #initialize when
110
+ # constructing the new copy of the identifier
111
+ #
112
+ # @param leading_namespace [String] the one or more leading namespaces to remove
113
+ # @param options [Hash] additional options for initialization
114
+ #
115
+ # @return [Dry::System::Identifier] the copy of the identifier
116
+ #
117
+ # @see #initialize
118
+ # @api private
119
+ def dequalified(leading_namespaces, **options)
120
+ new_identifier = identifier.gsub(
121
+ /^#{Regexp.escape(leading_namespaces)}#{Regexp.escape(separator)}/,
122
+ EMPTY_STRING
123
+ )
124
+
125
+ return self if new_identifier == identifier
126
+
127
+ self.class.new(
128
+ new_identifier,
129
+ namespace: namespace,
130
+ separator: separator,
131
+ **options
132
+ )
133
+ end
134
+
135
+ # Returns a copy of the identifier with the given options applied
136
+ #
137
+ # @param namespace [String, nil] a new namespace to be used
138
+ #
139
+ # @return [Dry::System::Identifier] the copy of the identifier
140
+ #
141
+ # @see #initialize
142
+ # @api private
143
+ def with(namespace:)
144
+ self.class.new(
145
+ identifier,
146
+ namespace: namespace,
147
+ separator: separator
148
+ )
149
+ end
150
+
151
+ private
152
+
153
+ def segments
154
+ @segments ||= identifier.split(separator)
155
+ end
156
+ end
157
+ end
158
+ end