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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +678 -0
- data/LICENSE +1 -1
- data/README.md +5 -4
- data/dry-system.gemspec +18 -21
- data/lib/dry/system/auto_registrar.rb +9 -64
- data/lib/dry/system/component.rb +124 -104
- data/lib/dry/system/component_dir.rb +171 -0
- data/lib/dry/system/config/component_dir.rb +228 -0
- data/lib/dry/system/config/component_dirs.rb +289 -0
- data/lib/dry/system/config/namespace.rb +75 -0
- data/lib/dry/system/config/namespaces.rb +196 -0
- data/lib/dry/system/constants.rb +2 -4
- data/lib/dry/system/container.rb +305 -345
- data/lib/dry/system/errors.rb +73 -56
- data/lib/dry/system/identifier.rb +176 -0
- data/lib/dry/system/importer.rb +89 -12
- data/lib/dry/system/indirect_component.rb +63 -0
- data/lib/dry/system/loader/autoloading.rb +24 -0
- data/lib/dry/system/loader.rb +49 -41
- data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +13 -14
- data/lib/dry/system/plugins/bootsnap.rb +3 -2
- data/lib/dry/system/plugins/dependency_graph/strategies.rb +38 -2
- data/lib/dry/system/plugins/dependency_graph.rb +25 -21
- data/lib/dry/system/plugins/env.rb +3 -2
- data/lib/dry/system/plugins/logging.rb +9 -8
- data/lib/dry/system/plugins/monitoring.rb +1 -2
- data/lib/dry/system/plugins/notifications.rb +1 -1
- data/lib/dry/system/plugins/plugin.rb +61 -0
- data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
- data/lib/dry/system/plugins/zeitwerk.rb +109 -0
- data/lib/dry/system/plugins.rb +5 -73
- data/lib/dry/system/provider/source.rb +276 -0
- data/lib/dry/system/provider/source_dsl.rb +55 -0
- data/lib/dry/system/provider.rb +261 -23
- data/lib/dry/system/provider_registrar.rb +251 -0
- data/lib/dry/system/provider_source_registry.rb +56 -0
- data/lib/dry/system/provider_sources/settings/config.rb +73 -0
- data/lib/dry/system/provider_sources/settings/loader.rb +44 -0
- data/lib/dry/system/provider_sources/settings.rb +40 -0
- data/lib/dry/system/provider_sources.rb +5 -0
- data/lib/dry/system/stubs.rb +6 -2
- data/lib/dry/system/version.rb +1 -1
- data/lib/dry/system.rb +35 -13
- metadata +48 -97
- data/lib/dry/system/auto_registrar/configuration.rb +0 -43
- data/lib/dry/system/booter/component_registry.rb +0 -35
- data/lib/dry/system/booter.rb +0 -181
- data/lib/dry/system/components/bootable.rb +0 -289
- data/lib/dry/system/components/config.rb +0 -35
- data/lib/dry/system/components.rb +0 -8
- data/lib/dry/system/lifecycle.rb +0 -135
- data/lib/dry/system/provider_registry.rb +0 -27
- data/lib/dry/system/settings/file_loader.rb +0 -30
- data/lib/dry/system/settings/file_parser.rb +0 -51
- data/lib/dry/system/settings.rb +0 -67
- data/lib/dry/system/system_components/settings.rb +0 -11
data/lib/dry/system/errors.rb
CHANGED
@@ -2,73 +2,71 @@
|
|
2
2
|
|
3
3
|
module Dry
|
4
4
|
module System
|
5
|
-
# Error raised when
|
6
|
-
# file
|
5
|
+
# Error raised when import is called on an already finalized container
|
7
6
|
#
|
8
7
|
# @api public
|
9
|
-
|
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
|
10
|
+
# Error raised when a component dir is added to configuration more than once
|
16
11
|
#
|
17
12
|
# @api public
|
18
|
-
|
19
|
-
def initialize(
|
20
|
-
super(
|
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
|
19
|
+
# Error raised when a configured component directory could not be found
|
27
20
|
#
|
28
21
|
# @api public
|
29
|
-
|
30
|
-
def initialize(
|
31
|
-
super("
|
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
|
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
|
-
|
39
|
-
def initialize(
|
40
|
-
|
41
|
-
|
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
|
40
|
+
# Error raised when attempting to register provider using a name that has already been
|
41
|
+
# registered
|
47
42
|
#
|
48
43
|
# @api public
|
49
|
-
|
50
|
-
def initialize(
|
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
|
50
|
+
# Error raised when a named provider could not be found
|
58
51
|
#
|
59
52
|
# @api public
|
60
|
-
|
53
|
+
ProviderNotFoundError = Class.new(ArgumentError) do
|
61
54
|
def initialize(name)
|
62
|
-
super("
|
55
|
+
super("Provider #{name.inspect} not found")
|
63
56
|
end
|
64
57
|
end
|
65
58
|
|
66
|
-
# Error raised when
|
59
|
+
# Error raised when a named provider source could not be found
|
67
60
|
#
|
68
61
|
# @api public
|
69
|
-
|
70
|
-
def initialize(
|
71
|
-
|
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
|
data/lib/dry/system/importer.rb
CHANGED
@@ -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
|
-
|
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 :
|
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
|
29
|
-
registry
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
47
|
-
|
58
|
+
def finalize!
|
59
|
+
registry.each_key { import(_1) }
|
60
|
+
self
|
48
61
|
end
|
49
62
|
|
50
63
|
# @api private
|
51
|
-
def
|
52
|
-
|
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
|
data/lib/dry/system/loader.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "dry/
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
def initialize(path, inflector = Dry::Inflector.new)
|
38
|
-
@path = path
|
39
|
-
@inflector = inflector
|
40
|
-
end
|
49
|
+
constant = self.constant(component)
|
41
50
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
58
|
-
ruby2_keywords(:call) if respond_to?(:ruby2_keywords, true)
|
57
|
+
ruby2_keywords(:call) if respond_to?(:ruby2_keywords, true)
|
59
58
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
77
|
+
private
|
70
78
|
|
71
|
-
|
72
|
-
|
73
|
-
|
79
|
+
def singleton?(constant)
|
80
|
+
constant.respond_to?(:instance) && !constant.respond_to?(:new)
|
81
|
+
end
|
74
82
|
end
|
75
83
|
end
|
76
84
|
end
|