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