dry-system 0.20.0 → 0.21.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.
@@ -1,5 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "dry/configurable"
4
+ require "dry/core/deprecations"
5
+ require "dry/system/constants"
2
6
  require "dry/system/loader"
7
+ require_relative "namespaces"
3
8
 
4
9
  module Dry
5
10
  module System
@@ -27,7 +32,7 @@ module Dry
27
32
  #
28
33
  # @example
29
34
  # dir.auto_register = proc do |component|
30
- # !component.start_with?("entities")
35
+ # !component.identifier.start_with?("entities")
31
36
  # end
32
37
  #
33
38
  # @see auto_register
@@ -65,39 +70,10 @@ module Dry
65
70
  # @see add_to_load_path=
66
71
  setting :add_to_load_path, default: true
67
72
 
68
- # @!method default_namespace=(leading_namespace)
69
- #
70
- # Sets the leading namespace segments to be stripped when registering components
71
- # from the dir in the container.
72
- #
73
- # This is useful to configure when the dir contains components in a module
74
- # namespace that you don't want repeated in their identifiers.
75
- #
76
- # Defaults to `nil`.
77
- #
78
- # @param leading_namespace [String, nil]
79
- # @return [String, nil]
80
- #
81
- # @example
82
- # dir.default_namespace = "my_app"
83
- #
84
- # @example
85
- # dir.default_namespace = "my_app.admin"
86
- #
87
- # @see default_namespace
88
- #
89
- # @!method default_namespace
90
- #
91
- # Returns the configured value.
92
- #
93
- # @return [String, nil]
94
- #
95
- # @see default_namespace=
96
- setting :default_namespace
97
-
98
73
  # @!method loader=(loader)
99
74
  #
100
- # Sets the loader to use when registering coponents from the dir in the container.
75
+ # Sets the loader to use when registering components from the dir in the
76
+ # container.
101
77
  #
102
78
  # Defaults to `Dry::System::Loader`.
103
79
  #
@@ -138,7 +114,7 @@ module Dry
138
114
  #
139
115
  # @example
140
116
  # dir.memoize = proc do |component|
141
- # !component.start_with?("providers")
117
+ # !component.identifier.start_with?("providers")
142
118
  # end
143
119
  #
144
120
  # @see memoize
@@ -153,6 +129,49 @@ module Dry
153
129
  # @see memoize=
154
130
  setting :memoize, default: false
155
131
 
132
+ # @!method namespaces
133
+ #
134
+ # Returns the configured namespaces for the component dir.
135
+ #
136
+ # Allows namespaces to added on the returned object via {Namespaces#add}.
137
+ #
138
+ # @see Namespaces#add
139
+ #
140
+ # @return [Namespaces] the namespaces
141
+ setting :namespaces, default: Namespaces.new, cloneable: true
142
+
143
+ def default_namespace=(namespace)
144
+ Dry::Core::Deprecations.announce(
145
+ "Dry::System::Config::ComponentDir#default_namespace=",
146
+ "Add a namespace instead: `dir.namespaces.add #{namespace.to_s.inspect}, key: nil`",
147
+ tag: "dry-system",
148
+ uplevel: 1
149
+ )
150
+
151
+ # We don't have the configured separator here, so the best we can do is guess
152
+ # that it's a dot
153
+ namespace_path = namespace.gsub(".", PATH_SEPARATOR)
154
+
155
+ return if namespaces.namespaces[namespace_path]
156
+
157
+ namespaces.add namespace_path, key: nil
158
+ end
159
+
160
+ def default_namespace
161
+ Dry::Core::Deprecations.announce(
162
+ "Dry::System::Config::ComponentDir#default_namespace",
163
+ "Use namespaces instead, e.g. `dir.namespaces`",
164
+ tag: "dry-system",
165
+ uplevel: 1
166
+ )
167
+
168
+ ns_path = namespaces.to_a.reject(&:root?).first&.path
169
+
170
+ # We don't have the configured separator here, so the best we can do is guess
171
+ # that it's a dot
172
+ ns_path&.gsub(PATH_SEPARATOR, ".")
173
+ end
174
+
156
175
  # @!endgroup
157
176
 
158
177
  # Returns the component dir path, relative to the configured container root
@@ -172,15 +191,28 @@ module Dry
172
191
  !!config.auto_register
173
192
  end
174
193
 
175
- # Returns true if a setting has been explicitly configured and is not returning
176
- # just a default value.
194
+ # Returns true if the given setting has been explicitly configured by the user
177
195
  #
178
- # This is used to determine which settings from `ComponentDirs` should be applied
179
- # as additional defaults.
196
+ # This is used when determining whether to apply system-wide default values to a
197
+ # component dir (explicitly configured settings will not be overridden by
198
+ # defaults)
180
199
  #
200
+ # @param key [Symbol] the setting name
201
+ #
202
+ # @return [Boolean]
203
+ #
204
+ # @see Dry::System::Config::ComponentDirs#apply_defaults_to_dir
181
205
  # @api private
182
206
  def configured?(key)
183
- config._settings[key].input_defined?
207
+ case key
208
+ when :namespaces
209
+ # Because we mutate the default value for the `namespaces` setting, rather
210
+ # than assign a new one, to check if it's configured we must see whether any
211
+ # namespaces have been added
212
+ !config.namespaces.empty?
213
+ else
214
+ config._settings[key].input_defined?
215
+ end
184
216
  end
185
217
 
186
218
  private
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "concurrent/map"
2
- require "dry/configurable"
3
4
  require "dry/system/constants"
4
5
  require "dry/system/errors"
5
6
  require_relative "component_dir"
@@ -8,13 +9,6 @@ module Dry
8
9
  module System
9
10
  module Config
10
11
  class ComponentDirs
11
- include Dry::Configurable
12
-
13
- # Settings from ComponentDir are configured here as defaults for all added dirs
14
- ComponentDir._settings.each do |setting|
15
- _settings << setting.dup
16
- end
17
-
18
12
  # @!group Settings
19
13
 
20
14
  # @!method auto_register=(value)
@@ -43,19 +37,6 @@ module Dry
43
37
  #
44
38
  # @see add_to_load_path=
45
39
 
46
- # @!method default_namespace=(value)
47
- #
48
- # Sets a default `default_namespace` value for all added component dirs
49
- #
50
- # @see ComponentDir.default_namespace
51
- # @see default_namespace
52
- #
53
- # @!method default_namespace
54
- #
55
- # Returns the configured default `default_namespace`
56
- #
57
- # @see default_namespace=
58
-
59
40
  # @!method loader=(value)
60
41
  #
61
42
  # Sets a default `loader` value for all added component dirs
@@ -82,17 +63,34 @@ module Dry
82
63
  #
83
64
  # @see memoize=
84
65
 
66
+ # @!method namespaces
67
+ #
68
+ # Returns the default configured namespaces for all added component dirs
69
+ #
70
+ # Allows namespaces to added on the returned object via {Namespaces#add}.
71
+ #
72
+ # @see Namespaces#add
73
+ #
74
+ # @return [Namespaces] the namespaces
75
+
85
76
  # @!endgroup
86
77
 
78
+ # A ComponentDir for configuring the default values to apply to all added
79
+ # component dirs
80
+ #
81
+ # @api private
82
+ attr_reader :defaults
83
+
87
84
  # @api private
88
85
  def initialize
89
86
  @dirs = Concurrent::Map.new
87
+ @defaults = ComponentDir.new(nil)
90
88
  end
91
89
 
92
90
  # @api private
93
91
  def initialize_copy(source)
94
- super
95
92
  @dirs = source.dirs.dup
93
+ @defaults = source.defaults.dup
96
94
  end
97
95
 
98
96
  # Adds and configures a component dir
@@ -114,8 +112,8 @@ module Dry
114
112
  raise ComponentDirAlreadyAddedError, path if dirs.key?(path)
115
113
 
116
114
  dirs[path] = ComponentDir.new(path).tap do |dir|
117
- apply_defaults_to_dir(dir)
118
115
  yield dir if block_given?
116
+ apply_defaults_to_dir(dir)
119
117
  end
120
118
  end
121
119
 
@@ -143,40 +141,29 @@ module Dry
143
141
 
144
142
  private
145
143
 
146
- # Apply default settings to a component dir. This is run every time the dirs are
144
+ # Applies default settings to a component dir. This is run every time the dirs are
147
145
  # accessed to ensure defaults are applied regardless of when new component dirs
148
146
  # are added. This method must be idempotent.
149
147
  #
150
148
  # @return [void]
151
149
  def apply_defaults_to_dir(dir)
152
- dir.config.values.each do |key, _value|
153
- if configured?(key) && !dir.configured?(key)
154
- dir.public_send(:"#{key}=", public_send(key))
150
+ defaults.config.values.each do |key, _|
151
+ if defaults.configured?(key) && !dir.configured?(key)
152
+ dir.public_send(:"#{key}=", defaults.public_send(key).dup)
155
153
  end
156
154
  end
157
155
  end
158
156
 
159
- # Returns true if a setting has been explicitly configured and is not returning
160
- # just a default value.
161
- #
162
- # This is used to determine which settings should be applied to added component
163
- # dirs as additional defaults.
164
- #
165
- # @api private
166
- def configured?(key)
167
- config._settings[key].input_defined?
168
- end
169
-
170
157
  def method_missing(name, *args, &block)
171
- if config.respond_to?(name)
172
- config.public_send(name, *args, &block)
158
+ if defaults.respond_to?(name)
159
+ defaults.public_send(name, *args, &block)
173
160
  else
174
161
  super
175
162
  end
176
163
  end
177
164
 
178
165
  def respond_to_missing?(name, include_all = false)
179
- config.respond_to?(name) || super
166
+ defaults.respond_to?(name) || super
180
167
  end
181
168
  end
182
169
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+
5
+ module Dry
6
+ module System
7
+ module Config
8
+ # A configured namespace for a component dir
9
+ #
10
+ # Namespaces consist of three elements:
11
+ #
12
+ # - The `path` within the component dir to which its namespace rules should apply.
13
+ # - A `key`, which determines the leading part of the key used to register
14
+ # each component in the container.
15
+ # - A `const`, which is the Ruby namespace expected to contain the class constants
16
+ # defined within each component's source file. This value is expected to be an
17
+ # "underscored" string, intended to be run through the configured inflector to be
18
+ # converted into a real constant (e.g. `"foo_bar/baz"` will become `FooBar::Baz`)
19
+ #
20
+ # Namespaces are added and configured for a component dir via {Namespaces#add}.
21
+ #
22
+ # @see Namespaces#add
23
+ #
24
+ # @api private
25
+ class Namespace
26
+ ROOT_PATH = nil
27
+
28
+ include Dry::Equalizer(:path, :key, :const)
29
+
30
+ attr_reader :path
31
+
32
+ attr_reader :key
33
+
34
+ attr_reader :const
35
+
36
+ # Returns a namespace configured to serve as the default root namespace for a
37
+ # component dir, ensuring that all code within the dir can be loaded, regardless
38
+ # of any other explictly configured namespaces
39
+ #
40
+ # @return [Namespace] the root namespace
41
+ #
42
+ # @api private
43
+ def self.default_root
44
+ new(
45
+ path: ROOT_PATH,
46
+ key: nil,
47
+ const: nil
48
+ )
49
+ end
50
+
51
+ def initialize(path:, key:, const:)
52
+ @path = path
53
+ @key = key
54
+ @const = const
55
+ end
56
+
57
+ def root?
58
+ path == ROOT_PATH
59
+ end
60
+
61
+ def path?
62
+ !root?
63
+ end
64
+
65
+ def default_key?
66
+ key == path
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system/errors"
4
+ require_relative "namespace"
5
+
6
+ module Dry
7
+ module System
8
+ module Config
9
+ # The configured namespaces for a ComponentDir
10
+ #
11
+ # @see Config::ComponentDir#namespaces
12
+ #
13
+ # @api private
14
+ class Namespaces
15
+ # @api private
16
+ attr_reader :namespaces
17
+
18
+ # @api private
19
+ def initialize
20
+ @namespaces = {}
21
+ end
22
+
23
+ # @api private
24
+ def initialize_copy(source)
25
+ super
26
+ @namespaces = source.namespaces.dup
27
+ end
28
+
29
+ # rubocop:disable Layout/LineLength
30
+
31
+ # Adds a component dir namespace
32
+ #
33
+ # A namespace encompasses a given sub-directory of the component dir, and
34
+ # determines (1) the leading segments of its components' registered identifiers,
35
+ # and (2) the expected constant namespace of their class constants.
36
+ #
37
+ # A namespace for a path can only be added once.
38
+ #
39
+ # @example Adding a namespace with top-level identifiers
40
+ # # Components defined within admin/ (e.g. admin/my_component.rb) will be:
41
+ # #
42
+ # # - Registered with top-level identifiers ("my_component")
43
+ # # - Expected to have constants in `Admin`, matching the namespace's path (Admin::MyComponent)
44
+ #
45
+ # namespaces.add "admin", key: nil
46
+ #
47
+ # @example Adding a namespace with top-level class constants
48
+ # # Components defined within adapters/ (e.g. adapters/my_adapter.rb) will be:
49
+ # #
50
+ # # - Registered with leading identifiers matching the namespace's path ("adapters.my_adapter")
51
+ # # - Expected to have top-level constants (::MyAdapter)
52
+ #
53
+ # namespaces.add "adapters", const: nil
54
+ #
55
+ # @example Adding a namespace with distinct identifiers and class constants
56
+ # # Components defined within `bananas/` (e.g. bananas/banana_split.rb) will be:
57
+ # #
58
+ # # - Registered with the given leading identifier ("desserts.banana_split")
59
+ # # - Expected to have constants within the given namespace (EatMe::Now::BananaSplit)
60
+ #
61
+ # namespaces.add "bananas", key: "desserts", const: "eat_me/now"
62
+ #
63
+ # @param path [String] the path to the sub-directory of source files to which this
64
+ # namespace should apply, relative to the component dir
65
+ # @param identifier [String, nil] the leading namespace to apply to the registered
66
+ # identifiers for the components. Set `nil` for the identifiers to be top-level.
67
+ # @param const [String, nil] the Ruby constant namespace to expect for constants
68
+ # defined within the components. This should be provided in underscored string
69
+ # form, e.g. "hello_there/world" for a Ruby constant of `HelloThere::World`. Set
70
+ # `nil` for the constants to be top-level.
71
+ #
72
+ # @return [Namespace] the added namespace
73
+ #
74
+ # @see Namespace
75
+ #
76
+ # @api public
77
+ def add(path, key: path, const: path)
78
+ raise NamespaceAlreadyAddedError, path if namespaces.key?(path)
79
+
80
+ namespaces[path] = Namespace.new(path: path, key: key, const: const)
81
+ end
82
+
83
+ # rubocop:enable Layout/LineLength
84
+
85
+ # Adds a root component dir namespace
86
+ #
87
+ # @see #add
88
+ #
89
+ # @api public
90
+ def root(key: nil, const: nil)
91
+ add(Namespace::ROOT_PATH, key: key, const: const)
92
+ end
93
+
94
+ # @api private
95
+ def empty?
96
+ namespaces.empty?
97
+ end
98
+
99
+ # Returns the configured namespaces as an array
100
+ #
101
+ # This adds a root namespace to the end of the array if one was not configured
102
+ # manually. This fallback ensures that all components in the component dir can be
103
+ # loaded.
104
+ #
105
+ # @return [Array<Namespace>] the namespaces
106
+ #
107
+ # @api private
108
+ def to_a
109
+ namespaces.values.tap do |arr|
110
+ arr << Namespace.default_root unless arr.any?(&:root?)
111
+ end
112
+ end
113
+
114
+ # @api private
115
+ def each(&block)
116
+ to_a.each(&block)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -8,7 +8,7 @@ module Dry
8
8
 
9
9
  RB_EXT = ".rb"
10
10
  RB_GLOB = "*.rb"
11
- PATH_SEPARATOR = "/"
11
+ PATH_SEPARATOR = File::SEPARATOR
12
12
  DEFAULT_SEPARATOR = "."
13
13
  WORD_REGEX = /\w+/.freeze
14
14
  end
@@ -11,13 +11,15 @@ require "dry/core/constants"
11
11
  require "dry/core/deprecations"
12
12
 
13
13
  require "dry/system"
14
- require "dry/system/errors"
15
- require "dry/system/booter"
16
14
  require "dry/system/auto_registrar"
17
- require "dry/system/manual_registrar"
18
- require "dry/system/importer"
15
+ require "dry/system/booter"
19
16
  require "dry/system/component"
20
17
  require "dry/system/constants"
18
+ require "dry/system/errors"
19
+ require "dry/system/identifier"
20
+ require "dry/system/importer"
21
+ require "dry/system/indirect_component"
22
+ require "dry/system/manual_registrar"
21
23
  require "dry/system/plugins"
22
24
 
23
25
  require_relative "component_dir"
@@ -49,7 +51,7 @@ module Dry
49
51
  #
50
52
  # Every container needs to be configured with following settings:
51
53
  #
52
- # * `:name` - a unique container identifier
54
+ # * `:name` - a unique container name
53
55
  # * `:root` - a system root directory (defaults to `pwd`)
54
56
  #
55
57
  # @example
@@ -234,7 +236,7 @@ module Dry
234
236
  # persistence.register(:db, DB.new)
235
237
  # end
236
238
  #
237
- # @param name [Symbol] a unique identifier for a bootable component
239
+ # @param name [Symbol] a unique name for a bootable component
238
240
  #
239
241
  # @see Lifecycle
240
242
  #
@@ -262,17 +264,15 @@ module Dry
262
264
  deprecate :finalize, :boot
263
265
 
264
266
  # @api private
265
- def boot_external(identifier, from:, key: nil, namespace: nil, &block)
267
+ def boot_external(name, from:, key: nil, namespace: nil, &block)
266
268
  System.providers[from].component(
267
- identifier, key: key, namespace: namespace, finalize: block, container: self
269
+ name, key: key, namespace: namespace, finalize: block, container: self
268
270
  )
269
271
  end
270
272
 
271
273
  # @api private
272
- def boot_local(identifier, namespace: nil, &block)
273
- Components::Bootable.new(
274
- identifier, container: self, namespace: namespace, &block
275
- )
274
+ def boot_local(name, namespace: nil, &block)
275
+ Components::Bootable.new(name, container: self, namespace: namespace, &block)
276
276
  end
277
277
 
278
278
  # Return if a container was finalized
@@ -491,7 +491,7 @@ module Dry
491
491
  #
492
492
  # @!method registered?(key)
493
493
  # Whether a +key+ is registered (doesn't trigger loading)
494
- # @param [String,Symbol] key Identifier
494
+ # @param [String,Symbol] key The key
495
495
  # @return [Boolean]
496
496
  # @api public
497
497
  #
@@ -581,17 +581,17 @@ module Dry
581
581
  def load_component(key)
582
582
  return self if registered?(key)
583
583
 
584
- component = component(key)
585
-
586
- if component.bootable?
587
- booter.start(component)
584
+ if (bootable_component = booter.find_component(key))
585
+ booter.start(bootable_component)
588
586
  return self
589
587
  end
590
588
 
589
+ component = find_component(key)
590
+
591
591
  booter.boot_dependency(component)
592
592
  return self if registered?(key)
593
593
 
594
- if component.file_exists?
594
+ if component.loadable?
595
595
  load_local_component(component)
596
596
  elsif manual_registrar.file_exists?(component)
597
597
  manual_registrar.(component)
@@ -615,25 +615,21 @@ module Dry
615
615
 
616
616
  container = importer[import_namespace]
617
617
 
618
- container.load_component(identifier.dequalified(import_namespace).key)
618
+ container.load_component(identifier.namespaced(from: import_namespace, to: nil).key)
619
619
 
620
620
  importer.(import_namespace, container)
621
621
  end
622
622
 
623
- def component(identifier)
624
- if (bootable_component = booter.find_component(identifier))
625
- return bootable_component
626
- end
627
-
623
+ def find_component(key)
628
624
  # Find the first matching component from within the configured component dirs.
629
- # If no matching component is found, return a plain component instance with no
630
- # associated file path. This fallback is important because the component may
631
- # still be loadable via the manual registrar or an imported container.
625
+ # If no matching component is found, return a null component; this fallback is
626
+ # important because the component may still be loadable via the manual registrar
627
+ # or an imported container.
632
628
  component_dirs.detect { |dir|
633
- if (component = dir.component_for_identifier(identifier))
629
+ if (component = dir.component_for_key(key))
634
630
  break component
635
631
  end
636
- } || Component.new(identifier)
632
+ } || IndirectComponent.new(Identifier.new(key, separator: config.namespace_separator))
637
633
  end
638
634
  end
639
635
 
@@ -11,13 +11,13 @@ module Dry
11
11
  end
12
12
  end
13
13
 
14
- # Error raised when the container tries to load a component with missing
15
- # file
16
- #
17
- # @api public
18
- FileNotFoundError = Class.new(StandardError) do
19
- def initialize(component)
20
- super("could not resolve require file for component '#{component.identifier}'")
14
+ # Error raised when a namespace for a component dir is added to configuration more
15
+ # than once
16
+ NamespaceAlreadyAddedError = Class.new(StandardError) do
17
+ def initialize(path)
18
+ path_label = path ? "path #{path.inspect}" : "root path"
19
+
20
+ super("Namespace for #{path_label} already added")
21
21
  end
22
22
  end
23
23
 
@@ -27,7 +27,7 @@ module Dry
27
27
  ComponentFileMismatchError = Class.new(StandardError) do
28
28
  def initialize(component)
29
29
  super(<<-STR)
30
- Bootable component '#{component.identifier}' not found
30
+ Bootable component '#{component.name}' not found
31
31
  STR
32
32
  end
33
33
  end
@@ -43,26 +43,17 @@ module Dry
43
43
  end
44
44
  end
45
45
 
46
- # Error raised when component's identifier is not valid
46
+ # Error raised when component's name is not valid
47
47
  #
48
48
  # @api public
49
- InvalidComponentIdentifierError = Class.new(ArgumentError) do
49
+ InvalidComponentNameError = Class.new(ArgumentError) do
50
50
  def initialize(name)
51
51
  super(
52
- "component identifier +#{name}+ is invalid or boot file is missing"
52
+ "component +#{name}+ is invalid or boot file is missing"
53
53
  )
54
54
  end
55
55
  end
56
56
 
57
- # Error raised when component's identifier for booting is not a symbol
58
- #
59
- # @api public
60
- InvalidComponentIdentifierTypeError = Class.new(ArgumentError) do
61
- def initialize(name)
62
- super("component identifier #{name.inspect} must be a symbol")
63
- end
64
- end
65
-
66
57
  # Error raised when trying to stop a component that hasn't started yet
67
58
  #
68
59
  # @api public