dry-system 0.19.2 → 0.23.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +472 -1
  3. data/LICENSE +1 -1
  4. data/README.md +4 -3
  5. data/dry-system.gemspec +16 -15
  6. data/lib/dry/system/auto_registrar.rb +1 -13
  7. data/lib/dry/system/component.rb +104 -47
  8. data/lib/dry/system/component_dir.rb +88 -47
  9. data/lib/dry/system/components.rb +8 -4
  10. data/lib/dry/system/config/component_dir.rb +141 -53
  11. data/lib/dry/system/config/component_dirs.rb +176 -70
  12. data/lib/dry/system/config/namespace.rb +76 -0
  13. data/lib/dry/system/config/namespaces.rb +208 -0
  14. data/lib/dry/system/constants.rb +2 -2
  15. data/lib/dry/system/container.rb +279 -201
  16. data/lib/dry/system/errors.rb +72 -61
  17. data/lib/dry/system/identifier.rb +99 -79
  18. data/lib/dry/system/importer.rb +83 -12
  19. data/lib/dry/system/indirect_component.rb +65 -0
  20. data/lib/dry/system/loader.rb +8 -4
  21. data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +12 -13
  22. data/lib/dry/system/plugins/bootsnap.rb +3 -2
  23. data/lib/dry/system/plugins/dependency_graph/strategies.rb +37 -1
  24. data/lib/dry/system/plugins/dependency_graph.rb +26 -20
  25. data/lib/dry/system/plugins/env.rb +3 -2
  26. data/lib/dry/system/plugins/logging.rb +9 -5
  27. data/lib/dry/system/plugins/monitoring.rb +1 -1
  28. data/lib/dry/system/plugins/notifications.rb +1 -1
  29. data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
  30. data/lib/dry/system/plugins/zeitwerk.rb +109 -0
  31. data/lib/dry/system/plugins.rb +8 -7
  32. data/lib/dry/system/provider/source.rb +324 -0
  33. data/lib/dry/system/provider/source_dsl.rb +94 -0
  34. data/lib/dry/system/provider.rb +264 -24
  35. data/lib/dry/system/provider_registrar.rb +276 -0
  36. data/lib/dry/system/provider_source_registry.rb +70 -0
  37. data/lib/dry/system/provider_sources/settings/config.rb +86 -0
  38. data/lib/dry/system/provider_sources/settings/loader.rb +53 -0
  39. data/lib/dry/system/provider_sources/settings.rb +40 -0
  40. data/lib/dry/system/provider_sources.rb +5 -0
  41. data/lib/dry/system/stubs.rb +1 -1
  42. data/lib/dry/system/version.rb +1 -1
  43. data/lib/dry/system.rb +45 -13
  44. metadata +25 -22
  45. data/lib/dry/system/booter/component_registry.rb +0 -35
  46. data/lib/dry/system/booter.rb +0 -200
  47. data/lib/dry/system/components/bootable.rb +0 -289
  48. data/lib/dry/system/components/config.rb +0 -35
  49. data/lib/dry/system/lifecycle.rb +0 -135
  50. data/lib/dry/system/provider_registry.rb +0 -27
  51. data/lib/dry/system/settings/file_loader.rb +0 -30
  52. data/lib/dry/system/settings/file_parser.rb +0 -51
  53. data/lib/dry/system/settings.rb +0 -67
  54. data/lib/dry/system/system_components/settings.rb +0 -11
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dry/core/deprecations"
4
+
3
5
  module Dry
4
6
  module System
7
+ extend Dry::Core::Deprecations["dry-system"]
8
+
5
9
  # Error raised when a component dir is added to configuration more than once
6
10
  #
7
11
  # @api public
@@ -11,64 +15,61 @@ module Dry
11
15
  end
12
16
  end
13
17
 
14
- # Error raised when the container tries to load a component with missing
15
- # file
18
+ # Error raised when a configured component directory could not be found
16
19
  #
17
20
  # @api public
18
- FileNotFoundError = Class.new(StandardError) do
19
- def initialize(component)
20
- super("could not resolve require file for component '#{component.identifier}'")
21
+ ComponentDirNotFoundError = Class.new(StandardError) do
22
+ def initialize(dir)
23
+ super("Component dir '#{dir}' not found")
21
24
  end
22
25
  end
23
26
 
24
- # Error raised when booter file do not match with register component
27
+ # Error raised when a namespace for a component dir is added to configuration more
28
+ # than once
25
29
  #
26
30
  # @api public
27
- ComponentFileMismatchError = Class.new(StandardError) do
28
- def initialize(component)
29
- super(<<-STR)
30
- Bootable component '#{component.identifier}' not found
31
- STR
32
- end
33
- end
31
+ NamespaceAlreadyAddedError = Class.new(StandardError) do
32
+ def initialize(path)
33
+ path_label = path ? "path #{path.inspect}" : "root path"
34
34
 
35
- # Error raised when resolved component couldn't be loaded
36
- #
37
- # @api public
38
- InvalidComponentError = Class.new(ArgumentError) do
39
- def initialize(name, reason = nil)
40
- super(
41
- "Tried to create an invalid #{name.inspect} component - #{reason}"
42
- )
35
+ super("Namespace for #{path_label} already added")
43
36
  end
44
37
  end
45
38
 
46
- # Error raised when component's identifier is not valid
39
+ # Error raised when attempting to register provider using a name that has already been
40
+ # registered
47
41
  #
48
42
  # @api public
49
- InvalidComponentIdentifierError = Class.new(ArgumentError) do
50
- def initialize(name)
51
- super(
52
- "component identifier +#{name}+ is invalid or boot file is missing"
53
- )
43
+ ProviderAlreadyRegisteredError = Class.new(ArgumentError) do
44
+ def initialize(provider_name)
45
+ super("Provider #{provider_name.inspect} has already been registered")
54
46
  end
55
47
  end
48
+ DuplicatedComponentKeyError = ProviderAlreadyRegisteredError
49
+ deprecate_constant :DuplicatedComponentKeyError
56
50
 
57
- # Error raised when component's identifier for booting is not a symbol
51
+ # Error raised when a named provider could not be found
58
52
  #
59
53
  # @api public
60
- InvalidComponentIdentifierTypeError = Class.new(ArgumentError) do
54
+ ProviderNotFoundError = Class.new(ArgumentError) do
61
55
  def initialize(name)
62
- super("component identifier #{name.inspect} must be a symbol")
56
+ super("Provider #{name.inspect} not found")
63
57
  end
64
58
  end
59
+ InvalidComponentError = ProviderNotFoundError
60
+ deprecate_constant :InvalidComponentError
65
61
 
66
- # Error raised when trying to stop a component that hasn't started yet
62
+ # Error raised when a named provider source could not be found
67
63
  #
68
64
  # @api public
69
- ComponentNotStartedError = Class.new(StandardError) do
70
- def initialize(component_name)
71
- super("component +#{component_name}+ has not been started")
65
+ ProviderSourceNotFoundError = Class.new(StandardError) do
66
+ def initialize(name:, group:, keys:)
67
+ msg = "Provider source not found: #{name.inspect}, group: #{group.inspect}"
68
+
69
+ key_list = keys.map { |key| "- #{key[:name].inspect}, group: #{key[:group].inspect}" }
70
+ msg += "Available provider sources:\n\n#{key_list}"
71
+
72
+ super(msg)
72
73
  end
73
74
  end
74
75
 
@@ -81,43 +82,53 @@ module Dry
81
82
  end
82
83
  end
83
84
 
84
- # Error raised when a configured component directory could not be found
85
+ # Exception raise when a plugin dependency failed to load
85
86
  #
86
87
  # @api public
87
- ComponentDirNotFoundError = Class.new(StandardError) do
88
- def initialize(dir)
89
- super("Component dir '#{dir}' not found")
88
+ PluginDependencyMissing = Class.new(StandardError) do
89
+ # @api private
90
+ def initialize(plugin, message, gem = nil)
91
+ details = gem ? "#{message} - add #{gem} to your Gemfile" : message
92
+ super("dry-system plugin #{plugin.inspect} failed to load its dependencies: #{details}")
90
93
  end
91
94
  end
92
95
 
93
- DuplicatedComponentKeyError = Class.new(ArgumentError)
94
-
95
- InvalidSettingsError = Class.new(ArgumentError) do
96
+ # Exception raised when auto-registerable component is not loadable
97
+ #
98
+ # @api public
99
+ ComponentNotLoadableError = Class.new(NameError) do
96
100
  # @api private
97
- def initialize(attributes)
98
- message = <<~STR
99
- Could not initialize settings. The following settings were invalid:
101
+ def initialize(component, error,
102
+ corrections: DidYouMean::ClassNameChecker.new(error).corrections)
103
+ full_class_name = [error.receiver, error.name].join("::")
100
104
 
101
- #{attributes_errors(attributes).join("\n")}
102
- STR
103
- super(message)
104
- end
105
+ message = [
106
+ "Component '#{component.key}' is not loadable.",
107
+ "Looking for #{full_class_name}."
108
+ ]
105
109
 
106
- private
110
+ if corrections.any?
111
+ case_correction = corrections.find { |correction| correction.casecmp?(full_class_name) }
112
+ if case_correction
113
+ acronyms_needed = case_correction.split("::").difference(full_class_name.split("::"))
114
+ stringified_acronyms_needed = acronyms_needed.map { |acronym|
115
+ "'#{acronym}'"
116
+ } .join(", ")
117
+ message <<
118
+ <<~ERROR_MESSAGE
107
119
 
108
- def attributes_errors(attributes)
109
- attributes.map { |key, error| "#{key.name}: #{error}" }
110
- end
111
- end
120
+ You likely need to add:
112
121
 
113
- # Exception raise when a plugin dependency failed to load
114
- #
115
- # @api public
116
- PluginDependencyMissing = Class.new(StandardError) do
117
- # @api private
118
- def initialize(plugin, message, gem = nil)
119
- details = gem ? "#{message} - add #{gem} to your Gemfile" : message
120
- super("dry-system plugin #{plugin.inspect} failed to load its dependencies: #{details}")
122
+ acronym(#{stringified_acronyms_needed})
123
+
124
+ to your container's inflector, since we found a #{case_correction} class.
125
+ ERROR_MESSAGE
126
+ else
127
+ message << DidYouMean.formatter.message_for(corrections)
128
+ end
129
+ end
130
+
131
+ super message.join("\n")
121
132
  end
122
133
  end
123
134
  end
@@ -13,42 +13,24 @@ module Dry
13
13
  #
14
14
  # @api public
15
15
  class Identifier
16
- include Dry::Equalizer(:identifier, :namespace, :separator)
16
+ include Dry::Equalizer(:key)
17
17
 
18
- # @return [String] the identifier string
18
+ # @return [String] the identifier's string key
19
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
20
+ attr_reader :key
29
21
 
30
22
  # @api private
31
- def initialize(identifier, namespace: nil, separator: DEFAULT_SEPARATOR)
32
- @identifier = identifier.to_s
33
- @namespace = namespace
34
- @separator = separator
23
+ def initialize(key)
24
+ @key = key.to_s
35
25
  end
36
26
 
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
27
  # @!method to_s
46
- # Returns the identifier string
28
+ # Returns the identifier string key
47
29
  #
48
30
  # @return [String]
49
- # @see #identifier
31
+ # @see #key
50
32
  # @api public
51
- alias_method :to_s, :identifier
33
+ alias_method :to_s, :key
52
34
 
53
35
  # Returns the root namespace segment of the identifier string, as a symbol
54
36
  #
@@ -62,95 +44,133 @@ module Dry
62
44
  segments.first.to_sym
63
45
  end
64
46
 
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.
47
+ # Returns true if the given leading segments string is a leading part of the {key}.
48
+ #
49
+ # Also returns true if nil or an empty string is given.
68
50
  #
69
51
  # @example
70
52
  # identifier.key # => "articles.operations.create"
71
- # identifier.namespace # => "admin"
72
53
  #
73
- # identifier.path # => "admin/articles/operations/create"
54
+ # identifier.start_with?("articles.operations") # => true
55
+ # identifier.start_with?("articles") # => true
56
+ # identifier.start_with?("article") # => false
57
+ # identifier.start_with?(nil) # => true
74
58
  #
75
- # @return [String] the path
59
+ # @param leading_segments [String] the one or more leading segments to check
60
+ # @return [Boolean]
76
61
  # @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
- }
62
+ def start_with?(leading_segments)
63
+ leading_segments.to_s.empty? ||
64
+ key.start_with?("#{leading_segments}#{KEY_SEPARATOR}") ||
65
+ key.eql?(leading_segments)
86
66
  end
87
67
 
88
- # Returns true if the given namespace prefix is part of the identifier's leading
89
- # namespaces
68
+ # Returns true if the given trailing segments string is the end part of the {key}.
69
+ #
70
+ # Also returns true if nil or an empty string is given.
90
71
  #
91
72
  # @example
92
73
  # identifier.key # => "articles.operations.create"
93
74
  #
94
- # identifier.start_with?("articles.operations") # => true
95
- # identifier.start_with?("articles") # => true
96
- # identifier.start_with?("article") # => false
75
+ # identifier.end_with?("create") # => true
76
+ # identifier.end_with?("operations.create") # => true
77
+ # identifier.end_with?("ate") # => false, not a whole segment
78
+ # identifier.end_with?("nup") # => false, not in key at all
97
79
  #
98
- # @param leading_namespaces [String] the one or more leading namespaces to check
80
+ # @param trailing_segments [String] the one or more trailing key segments to check
99
81
  # @return [Boolean]
100
82
  # @api public
101
- def start_with?(leading_namespaces)
102
- identifier.start_with?("#{leading_namespaces}#{separator}")
83
+ def end_with?(trailing_segments)
84
+ trailing_segments.to_s.empty? ||
85
+ key.end_with?("#{KEY_SEPARATOR}#{trailing_segments}") ||
86
+ key.eql?(trailing_segments)
103
87
  end
104
88
 
105
- # Returns a copy of the identifier with the given leading namespaces removed from
106
- # the identifier string.
89
+ # Returns true if the given segments string matches whole segments within the {key}.
107
90
  #
108
- # Additional options may be provided, which are passed to #initialize when
109
- # constructing the new copy of the identifier
91
+ # @example
92
+ # identifier.key # => "articles.operations.create"
110
93
  #
111
- # @param leading_namespace [String] the one or more leading namespaces to remove
112
- # @param options [Hash] additional options for initialization
94
+ # identifier.include?("operations") # => true
95
+ # identifier.include?("articles.operations") # => true
96
+ # identifier.include?("operations.create") # => true
113
97
  #
114
- # @return [Dry::System::Identifier] the copy of the identifier
98
+ # identifier.include?("article") # => false, not a whole segment
99
+ # identifier.include?("update") # => false, not in key at all
115
100
  #
116
- # @see #initialize
117
- # @api private
118
- def dequalified(leading_namespaces, **options)
119
- new_identifier = identifier.gsub(
120
- /^#{Regexp.escape(leading_namespaces)}#{Regexp.escape(separator)}/,
121
- EMPTY_STRING
101
+ # @param segments [String] the one of more key segments to check
102
+ # @return [Boolean]
103
+ # @api public
104
+ def include?(segments)
105
+ return false if segments.to_s.empty?
106
+
107
+ sep_re = Regexp.escape(KEY_SEPARATOR)
108
+ key.match?(
109
+ /
110
+ (\A|#{sep_re})
111
+ #{Regexp.escape(segments)}
112
+ (\Z|#{sep_re})
113
+ /x
122
114
  )
115
+ end
123
116
 
124
- return self if new_identifier == identifier
125
-
126
- self.class.new(
127
- new_identifier,
128
- namespace: namespace,
129
- separator: separator,
130
- **options
131
- )
117
+ # Returns the key with its segments separated by the given separator
118
+ #
119
+ # @example
120
+ # identifier.key # => "articles.operations.create"
121
+ # identifier.key_with_separator("/") # => "articles/operations/create"
122
+ #
123
+ # @return [String] the key using the separator
124
+ # @api private
125
+ def key_with_separator(separator)
126
+ segments.join(separator)
132
127
  end
133
128
 
134
- # Returns a copy of the identifier with the given options applied
129
+ # Returns a copy of the identifier with the key's leading namespace(s) replaced
135
130
  #
136
- # @param namespace [String, nil] a new namespace to be used
131
+ # @example Changing a namespace
132
+ # identifier.key # => "articles.operations.create"
133
+ # identifier.namespaced(from: "articles", to: "posts").key # => "posts.commands.create"
134
+ #
135
+ # @example Removing a namespace
136
+ # identifier.key # => "articles.operations.create"
137
+ # identifier.namespaced(from: "articles", to: nil).key # => "operations.create"
138
+ #
139
+ # @example Adding a namespace
140
+ # identifier.key # => "articles.operations.create"
141
+ # identifier.namespaced(from: nil, to: "admin").key # => "admin.articles.operations.create"
142
+ #
143
+ # @param from [String, nil] the leading namespace(s) to replace
144
+ # @param to [String, nil] the replacement for the leading namespace
137
145
  #
138
146
  # @return [Dry::System::Identifier] the copy of the identifier
139
147
  #
140
148
  # @see #initialize
141
149
  # @api private
142
- def with(namespace:)
143
- self.class.new(
144
- identifier,
145
- namespace: namespace,
146
- separator: separator
147
- )
150
+ def namespaced(from:, to:)
151
+ return self if from == to
152
+
153
+ separated_to = "#{to}#{KEY_SEPARATOR}" if to
154
+
155
+ new_key =
156
+ if from.nil?
157
+ "#{separated_to}#{key}"
158
+ else
159
+ key.sub(
160
+ /^#{Regexp.escape(from.to_s)}#{Regexp.escape(KEY_SEPARATOR)}/,
161
+ separated_to || EMPTY_STRING
162
+ )
163
+ end
164
+
165
+ return self if new_key == key
166
+
167
+ self.class.new(new_key)
148
168
  end
149
169
 
150
170
  private
151
171
 
152
172
  def segments
153
- @segments ||= identifier.split(separator)
173
+ @segments ||= key.split(KEY_SEPARATOR)
154
174
  end
155
175
  end
156
176
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dry/container"
4
+ require_relative "constants"
5
+
3
6
  module Dry
4
7
  module System
5
8
  # Default importer implementation
@@ -11,25 +14,34 @@ module Dry
11
14
  #
12
15
  # @api private
13
16
  class Importer
14
- attr_reader :container
17
+ # @api private
18
+ class Item
19
+ attr_reader :namespace, :container, :import_keys
20
+
21
+ def initialize(namespace:, container:, import_keys:)
22
+ @namespace = namespace
23
+ @container = container
24
+ @import_keys = import_keys
25
+ end
26
+ end
15
27
 
16
- attr_reader :separator
28
+ attr_reader :container
17
29
 
18
30
  attr_reader :registry
19
31
 
20
32
  # @api private
21
33
  def initialize(container)
22
34
  @container = container
23
- @separator = container.config.namespace_separator
24
35
  @registry = {}
25
36
  end
26
37
 
27
38
  # @api private
28
- def finalize!
29
- registry.each do |name, container|
30
- call(name, container.finalize!)
31
- end
32
- self
39
+ def register(namespace:, container:, keys: nil)
40
+ registry[namespace] = Item.new(
41
+ namespace: namespace,
42
+ container: container,
43
+ import_keys: keys
44
+ )
33
45
  end
34
46
 
35
47
  # @api private
@@ -41,15 +53,74 @@ module Dry
41
53
  def key?(name)
42
54
  registry.key?(name)
43
55
  end
56
+ alias_method :namespace?, :key?
44
57
 
45
58
  # @api private
46
- def call(ns, other)
47
- container.merge(other, namespace: ns)
59
+ def finalize!
60
+ registry.each_key { import(_1) }
61
+ self
48
62
  end
49
63
 
50
64
  # @api private
51
- def register(other)
52
- registry.update(other)
65
+ def import(namespace, keys: Undefined)
66
+ item = self[namespace]
67
+ keys = Undefined.default(keys, item.import_keys)
68
+
69
+ if keys
70
+ import_keys(item.container, namespace, keys_to_import(keys, item))
71
+ else
72
+ import_all(item.container, namespace)
73
+ end
74
+
75
+ self
76
+ end
77
+
78
+ private
79
+
80
+ def keys_to_import(keys, item)
81
+ keys
82
+ .then { (arr = item.import_keys) ? _1 & arr : _1 }
83
+ .then { (arr = item.container.exports) ? _1 & arr : _1 }
84
+ end
85
+
86
+ def import_keys(other, namespace, keys)
87
+ container.merge(build_merge_container(other, keys), namespace: namespace)
88
+ end
89
+
90
+ def import_all(other, namespace)
91
+ merge_container =
92
+ if other.exports
93
+ build_merge_container(other, other.exports)
94
+ else
95
+ build_merge_container(other.finalize!, other.keys)
96
+ end
97
+
98
+ container.merge(merge_container, namespace: namespace)
99
+ end
100
+
101
+ def build_merge_container(other, keys)
102
+ keys.each_with_object(Dry::Container.new) { |key, ic|
103
+ next unless other.key?(key)
104
+
105
+ # Access the other container's items directly so that we can preserve all their
106
+ # options when we merge them with the target container (e.g. if a component in
107
+ # the provider container was registered with a block, we want block registration
108
+ # behavior to be exhibited when later resolving that component from the target
109
+ # container). TODO: Make this part of dry-system's public API.
110
+ item = other._container[key]
111
+
112
+ # By default, we "protect" components that were themselves imported into the
113
+ # other container from being implicitly exported; imported components are
114
+ # considered "private" and must be explicitly included in `exports` to be
115
+ # exported.
116
+ next if item.options[:imported] && !other.exports
117
+
118
+ if item.callable?
119
+ ic.register(key, **item.options, imported: true, &item.item)
120
+ else
121
+ ic.register(key, item.item, **item.options, imported: true)
122
+ end
123
+ }
53
124
  end
54
125
  end
55
126
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+
5
+ module Dry
6
+ module System
7
+ # An indirect component is a component that cannot be directly from a source file
8
+ # directly managed by the container. It may be component that needs to be loaded
9
+ # indirectly, either via a registration manifest file or an imported container
10
+ #
11
+ # Indirect components are an internal abstraction and, unlike ordinary components, are
12
+ # not exposed to users via component dir configuration hooks.
13
+ #
14
+ # @see Container#load_component
15
+ # @see Container#find_component
16
+ #
17
+ # @api private
18
+ class IndirectComponent
19
+ include Dry::Equalizer(:identifier)
20
+
21
+ # @!attribute [r] identifier
22
+ # @return [String] the component's unique identifier
23
+ attr_reader :identifier
24
+
25
+ # @api private
26
+ def initialize(identifier)
27
+ @identifier = identifier
28
+ end
29
+
30
+ # Returns false, indicating that the component is not directly loadable from the
31
+ # files managed by the container
32
+ #
33
+ # This is the inverse of {Component#loadable?}
34
+ #
35
+ # @return [FalseClass]
36
+ #
37
+ # @api private
38
+ def loadable?
39
+ false
40
+ end
41
+
42
+ # Returns the component's unique key
43
+ #
44
+ # @return [String] the key
45
+ #
46
+ # @see Identifier#key
47
+ #
48
+ # @api private
49
+ def key
50
+ identifier.to_s
51
+ end
52
+
53
+ # Returns the root namespace segment of the component's key, as a symbol
54
+ #
55
+ # @see Identifier#root_key
56
+ #
57
+ # @return [Symbol] the root key
58
+ #
59
+ # @api private
60
+ def root_key
61
+ identifier.root_key
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dry/system/errors"
4
+
3
5
  module Dry
4
6
  module System
5
7
  # Default component loader implementation
@@ -17,7 +19,7 @@ module Dry
17
19
  # class MyApp < Dry::System::Container
18
20
  # configure do |config|
19
21
  # # ...
20
- # config.loader MyLoader
22
+ # config.component_dirs.loader = MyLoader
21
23
  # end
22
24
  # end
23
25
  #
@@ -28,7 +30,7 @@ module Dry
28
30
  #
29
31
  # @api public
30
32
  def require!(component)
31
- require(component.path) if component.file_exists?
33
+ require(component.require_path)
32
34
  self
33
35
  end
34
36
 
@@ -61,8 +63,10 @@ module Dry
61
63
  # @api public
62
64
  def constant(component)
63
65
  inflector = component.inflector
64
-
65
- inflector.constantize(inflector.camelize(component.path))
66
+ const_name = inflector.camelize(component.const_path)
67
+ inflector.constantize(const_name)
68
+ rescue NameError => e
69
+ raise ComponentNotLoadableError.new(component, e)
66
70
  end
67
71
 
68
72
  private