dry-system 0.19.2 → 0.23.0

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