dry-system 0.20.0 → 0.21.0

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