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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed86f20c6700f29644fe61cc3bd215f50b93177963eb67076523cbf295f18051
4
- data.tar.gz: b7ec3d205804cc17033450f7585c809f6a9942ea89fea906799cb02c94e2b64b
3
+ metadata.gz: 1c0977524fa33277150e8611a851c557461c14357e1a5d9c1f1929c283f3c9b3
4
+ data.tar.gz: 12de5eaa25ede9f5f959cd77e2af8f1d3b4b287aaebea1554c2915433743c290
5
5
  SHA512:
6
- metadata.gz: b422ce21011d20c5fedd6eab5cae91c0f03e7ee42d2f7e279b915fbba6826477bfa3393933f4e56417a6f284a2782f56ec184843cae8364a69e1073199d70c05
7
- data.tar.gz: f2ded5f38dd0fffc72f6262f0c844b430e26f5b411ffd576284c02ec379bbd4b0638c83cae4070d283891f99376b1a6c5700fe453ba938ae4ef17e26cdbf47c5
6
+ metadata.gz: 6976cb749c98ed536c5fb3e6ca67674aee2707b40cd5d196b6b82472edadfa10877bebac77631b0ebcc5fabe008514ed60e7d9be17ccd1343e8967836f319a11
7
+ data.tar.gz: fd894f982bf214dac3ccfa0de1d4fd7f95beeea1f91234cd2868dfab4687b76ea831c153b429d8ba9bd9f04bcb46dd2815f87a7829e6b347b2d8992cf0ebd478
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
2
 
3
+ ## 0.21.0 2021-11-01
4
+
5
+
6
+ ### Added
7
+
8
+ - Added **component dir namespaces** as a way to specify multiple, ordered, independent namespace rules within a given component dir. This replaces and expands upon the namespace support we previously provided via the singular `default_namespace` component dir setting (@timriley in #181)
9
+
10
+ ### Changed
11
+
12
+ - `default_namespace` setting on component dirs has been deprecated. Add a component dir namespace instead, e.g. instead of:
13
+
14
+ ```ruby
15
+ # Inside Dry::System::Container.configure
16
+ config.component_dirs.add "lib" do |dir|
17
+ dir.default_namespace = "admin"
18
+ end
19
+ ```
20
+
21
+ Add this:
22
+
23
+ ```ruby
24
+ config.component_dirs.add "lib" do |dir|
25
+ dir.namespaces.add "admin", key: nil
26
+ end
27
+ ```
28
+
29
+ (@timriley in #181)
30
+ - `Dry::System::Component#path` has been removed and replaced by `Component#require_path` and `Component#const_path` (@timriley in #181)
31
+ - Unused `Dry::System::FileNotFoundError` and `Dry::System::InvalidComponentIdentifierTypeError` errors have been removed (@timriley in #194)
32
+
33
+ [Compare v0.20.0...v0.21.0](https://github.com/dry-rb/dry-system/compare/v0.20.0...v0.21.0)
34
+
3
35
  ## 0.20.0 2021-09-12
4
36
 
5
37
 
data/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
  This library officially supports the following Ruby versions:
24
24
 
25
25
  * MRI `>= 2.6.0`
26
- * ~~jruby~~ `>= 9.3` (we are waiting for [2.6 support](https://github.com/jruby/jruby/issues/6161))
26
+ * jruby `>= 9.3`
27
27
 
28
28
  ## License
29
29
 
@@ -29,7 +29,7 @@ module Dry
29
29
 
30
30
  # @api private
31
31
  def call(component_dir)
32
- components(component_dir).each do |component|
32
+ component_dir.each_component do |component|
33
33
  next unless register_component?(component)
34
34
 
35
35
  container.register(component.key, memoize: component.memoize?) { component.instance }
@@ -38,18 +38,6 @@ module Dry
38
38
 
39
39
  private
40
40
 
41
- def components(component_dir)
42
- files(component_dir.full_path).map { |file_path|
43
- component_dir.component_for_path(file_path)
44
- }
45
- end
46
-
47
- def files(dir)
48
- raise ComponentDirNotFoundError, dir unless Dir.exist?(dir)
49
-
50
- Dir["#{dir}/**/#{RB_GLOB}"].sort
51
- end
52
-
53
41
  def register_component?(component)
54
42
  !container.registered?(component.key) && component.auto_register?
55
43
  end
@@ -21,13 +21,13 @@ module Dry
21
21
  end
22
22
 
23
23
  def exists?(name)
24
- components.any? { |component| component.identifier == name }
24
+ components.any? { |component| component.name == name }
25
25
  end
26
26
 
27
27
  def [](name)
28
- component = components.detect { |component| component.identifier == name }
28
+ component = components.detect { |c| c.name == name }
29
29
 
30
- component || raise(InvalidComponentIdentifierError, name)
30
+ component || raise(InvalidComponentNameError, name)
31
31
  end
32
32
  end
33
33
  end
@@ -179,15 +179,15 @@ module Dry
179
179
  end
180
180
 
181
181
  def load_component(path)
182
- identifier = Pathname(path).basename(RB_EXT).to_s.to_sym
182
+ name = Pathname(path).basename(RB_EXT).to_s.to_sym
183
183
 
184
- Kernel.require path unless components.exists?(identifier)
184
+ Kernel.require path unless components.exists?(name)
185
185
 
186
186
  self
187
187
  end
188
188
 
189
- def require_boot_file(identifier)
190
- boot_file = find_boot_file(identifier)
189
+ def require_boot_file(name)
190
+ boot_file = find_boot_file(name)
191
191
 
192
192
  Kernel.require boot_file if boot_file
193
193
  end
@@ -17,7 +17,7 @@ module Dry
17
17
  #
18
18
  # @api public
19
19
  class Component
20
- include Dry::Equalizer(:identifier, :file_path, :options)
20
+ include Dry::Equalizer(:identifier, :namespace, :options)
21
21
 
22
22
  DEFAULT_OPTIONS = {
23
23
  separator: DEFAULT_SEPARATOR,
@@ -26,43 +26,34 @@ module Dry
26
26
  }.freeze
27
27
 
28
28
  # @!attribute [r] identifier
29
- # @return [String] component's unique identifier
29
+ # @return [String] the component's unique identifier
30
30
  attr_reader :identifier
31
31
 
32
- # @!attribute [r] file_path
33
- # @return [String, nil] full path to the component's file, if found
34
- attr_reader :file_path
32
+ # @!attribute [r] namespace
33
+ # @return [Dry::System::Config::Namespace] the component's namespace
34
+ attr_reader :namespace
35
35
 
36
36
  # @!attribute [r] options
37
- # @return [Hash] component's options
37
+ # @return [Hash] the component's options
38
38
  attr_reader :options
39
39
 
40
40
  # @api private
41
- def self.new(identifier, options = EMPTY_HASH)
42
- options = DEFAULT_OPTIONS.merge(options)
43
-
44
- namespace = options.delete(:namespace)
45
- separator = options.delete(:separator)
46
-
47
- identifier =
48
- if identifier.is_a?(Identifier)
49
- identifier
50
- else
51
- Identifier.new(
52
- identifier,
53
- namespace: namespace,
54
- separator: separator
55
- )
56
- end
57
-
58
- super(identifier, **options)
41
+ def initialize(identifier, namespace:, **options)
42
+ @identifier = identifier
43
+ @namespace = namespace
44
+ @options = DEFAULT_OPTIONS.merge(options)
59
45
  end
60
46
 
47
+ # Returns true, indicating that the component is directly loadable from the files
48
+ # managed by the container
49
+ #
50
+ # This is the inverse of {IndirectComponent#loadable?}
51
+ #
52
+ # @return [TrueClass]
53
+ #
61
54
  # @api private
62
- def initialize(identifier, file_path: nil, **options)
63
- @identifier = identifier
64
- @file_path = file_path
65
- @options = options
55
+ def loadable?
56
+ true
66
57
  end
67
58
 
68
59
  # Returns the component's instance
@@ -74,39 +65,95 @@ module Dry
74
65
  end
75
66
  ruby2_keywords(:instance) if respond_to?(:ruby2_keywords, true)
76
67
 
77
- # @api private
78
- def bootable?
79
- false
80
- end
81
-
68
+ # Returns the component's unique key
69
+ #
70
+ # @return [String] the key
71
+ #
72
+ # @see Identifier#key
73
+ #
74
+ # @api public
82
75
  def key
83
- identifier.to_s
84
- end
85
-
86
- def path
87
- identifier.path
76
+ identifier.key
88
77
  end
89
78
 
79
+ # Returns the root namespace segment of the component's key, as a symbol
80
+ #
81
+ # @see Identifier#root_key
82
+ #
83
+ # @return [Symbol] the root key
84
+ #
85
+ # @api public
90
86
  def root_key
91
87
  identifier.root_key
92
88
  end
93
89
 
94
- # Returns true if the component has a corresponding file
90
+ # Returns a path-delimited representation of the compnent, appropriate for passing
91
+ # to `Kernel#require` to require its source file
95
92
  #
96
- # @return [Boolean]
97
- # @api private
98
- def file_exists?
99
- !!file_path
93
+ # The path takes into account the rules of the namespace used to load the component.
94
+ #
95
+ # @example Component from a root namespace
96
+ # component.key # => "articles.create"
97
+ # component.require_path # => "articles/create"
98
+ #
99
+ # @example Component from an "admin/" path namespace (with `key: nil`)
100
+ # component.key # => "articles.create"
101
+ # component.require_path # => "admin/articles/create"
102
+ #
103
+ # @see Config::Namespaces#add
104
+ # @see Config::Namespace
105
+ #
106
+ # @return [String] the require path
107
+ #
108
+ # @api public
109
+ def require_path
110
+ if namespace.path
111
+ "#{namespace.path}#{PATH_SEPARATOR}#{path_in_namespace}"
112
+ else
113
+ path_in_namespace
114
+ end
115
+ end
116
+
117
+ # Returns an "underscored", path-delimited representation of the component,
118
+ # appropriate for passing to the inflector for constantizing
119
+ #
120
+ # The const path takes into account the rules of the namespace used to load the
121
+ # component.
122
+ #
123
+ # @example Component from a namespace with `const: nil`
124
+ # component.key # => "articles.create_article"
125
+ # component.const_path # => "articles/create_article"
126
+ # component.inflector.constantize(component.const_path) # => Articles::CreateArticle
127
+ #
128
+ # @example Component from a namespace with `const: "admin"`
129
+ # component.key # => "articles.create_article"
130
+ # component.const_path # => "admin/articles/create_article"
131
+ # component.inflector.constantize(component.const_path) # => Admin::Articles::CreateArticle
132
+ #
133
+ # @see Config::Namespaces#add
134
+ # @see Config::Namespace
135
+ #
136
+ # @return [String] the const path
137
+ #
138
+ # @api public
139
+ def const_path
140
+ namespace_const_path = namespace.const&.gsub(identifier.separator, PATH_SEPARATOR)
141
+
142
+ if namespace_const_path
143
+ "#{namespace_const_path}#{PATH_SEPARATOR}#{path_in_namespace}"
144
+ else
145
+ path_in_namespace
146
+ end
100
147
  end
101
148
 
102
149
  # @api private
103
150
  def loader
104
- options[:loader]
151
+ options.fetch(:loader)
105
152
  end
106
153
 
107
154
  # @api private
108
155
  def inflector
109
- options[:inflector]
156
+ options.fetch(:inflector)
110
157
  end
111
158
 
112
159
  # @api private
@@ -121,6 +168,17 @@ module Dry
121
168
 
122
169
  private
123
170
 
171
+ def path_in_namespace
172
+ identifier_in_namespace =
173
+ if namespace.key
174
+ identifier.namespaced(from: namespace.key, to: nil)
175
+ else
176
+ identifier
177
+ end
178
+
179
+ identifier_in_namespace.key_with_separator(PATH_SEPARATOR)
180
+ end
181
+
124
182
  def callable_option?(value)
125
183
  if value.respond_to?(:call)
126
184
  !!value.call(self)
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pathname"
4
+ require "dry/system/constants"
2
5
  require_relative "constants"
3
6
  require_relative "identifier"
4
7
  require_relative "magic_comments_parser"
@@ -28,40 +31,101 @@ module Dry
28
31
  @container = container
29
32
  end
30
33
 
31
- # Returns a component for a given identifier if a matching component file could be
32
- # found within the component dir
34
+ # Returns a component for the given key if a matching source file is found within
35
+ # the component dir
33
36
  #
34
- # This will search within the component dir's configured default_namespace first,
35
- # then fall back to searching for a non-namespaced file
37
+ # This searches according to the component dir's configured namespaces, in order of
38
+ # definition, with the first match returned as the component.
36
39
  #
37
- # @param identifier [String] the identifier string
40
+ # @param key [String] the component's key
38
41
  # @return [Dry::System::Component, nil] the component, if found
39
42
  #
40
43
  # @api private
41
- def component_for_identifier(identifier)
42
- identifier = Identifier.new(
43
- identifier,
44
- namespace: default_namespace,
45
- separator: container.config.namespace_separator
46
- )
47
-
48
- if (file_path = find_component_file(identifier.path))
49
- return build_component(identifier, file_path)
44
+ def component_for_key(key)
45
+ namespaces.each do |namespace|
46
+ identifier = Identifier.new(key, separator: container.config.namespace_separator)
47
+
48
+ next unless identifier.start_with?(namespace.key)
49
+
50
+ if (file_path = find_component_file(identifier, namespace))
51
+ return build_component(identifier, namespace, file_path)
52
+ end
53
+ end
54
+
55
+ nil
56
+ end
57
+
58
+ def each_component
59
+ return enum_for(:each_component) unless block_given?
60
+
61
+ each_file do |file_path, namespace|
62
+ yield component_for_path(file_path, namespace)
50
63
  end
64
+ end
65
+
66
+ private
67
+
68
+ def namespaces
69
+ config.namespaces.to_a.map { |namespace| normalize_namespace(namespace) }
70
+ end
51
71
 
52
- identifier = identifier.with(namespace: nil)
53
- if (file_path = find_component_file(identifier.path))
54
- build_component(identifier, file_path)
72
+ # Returns an array of "normalized" namespaces, safe for loading components
73
+ #
74
+ # This works around the issue of a namespace being added for a nested path but
75
+ # _without_ specifying a key namespace. In this case, the key namespace will defaut
76
+ # to match the path, meaning it will contain path separators instead of the
77
+ # container's configured `namespace_separator` (due to `Config::Namespaces` not
78
+ # being able to know the configured `namespace_separator`), so we need to replace
79
+ # the path separators with the proper `namespace_separator` here (where we _do_ know
80
+ # what it is).
81
+ def normalize_namespace(namespace)
82
+ if namespace.path&.include?(PATH_SEPARATOR) && namespace.default_key?
83
+ namespace = namespace.class.new(
84
+ path: namespace.path,
85
+ key: namespace.key.gsub(PATH_SEPARATOR, container.config.namespace_separator),
86
+ const: namespace.const
87
+ )
55
88
  end
89
+
90
+ namespace
91
+ end
92
+
93
+ def each_file
94
+ return enum_for(:each_file) unless block_given?
95
+
96
+ raise ComponentDirNotFoundError, full_path unless Dir.exist?(full_path)
97
+
98
+ namespaces.each do |namespace|
99
+ files(namespace).each do |file|
100
+ yield file, namespace
101
+ end
102
+ end
103
+ end
104
+
105
+ def files(namespace)
106
+ if namespace.path?
107
+ Dir[File.join(full_path, namespace.path, "**", RB_GLOB)].sort
108
+ else
109
+ non_root_paths = namespaces.to_a.reject(&:root?).map(&:path)
110
+
111
+ Dir[File.join(full_path, "**", RB_GLOB)].reject { |file_path|
112
+ Pathname(file_path).relative_path_from(full_path).to_s.start_with?(*non_root_paths)
113
+ }.sort
114
+ end
115
+ end
116
+
117
+ # Returns the full path of the component directory
118
+ #
119
+ # @return [Pathname]
120
+ def full_path
121
+ container.root.join(path)
56
122
  end
57
123
 
58
124
  # Returns a component for a full path to a Ruby source file within the component dir
59
125
  #
60
126
  # @param path [String] the full path to the file
61
127
  # @return [Dry::System::Component] the component
62
- #
63
- # @api private
64
- def component_for_path(path)
128
+ def component_for_path(path, namespace)
65
129
  separator = container.config.namespace_separator
66
130
 
67
131
  key = Pathname(path).relative_path_from(full_path).to_s
@@ -70,46 +134,49 @@ module Dry
70
134
  .join(separator)
71
135
 
72
136
  identifier = Identifier.new(key, separator: separator)
137
+ .namespaced(
138
+ from: namespace.path&.gsub(PATH_SEPARATOR, separator),
139
+ to: namespace.key
140
+ )
141
+
142
+ build_component(identifier, namespace, path)
143
+ end
73
144
 
74
- if identifier.start_with?(default_namespace)
75
- identifier = identifier.dequalified(default_namespace, namespace: default_namespace)
145
+ def find_component_file(identifier, namespace)
146
+ # To properly find the file within a namespace with a key, we should strip the key
147
+ # from beginning of our given identifier
148
+ if namespace.key
149
+ identifier = identifier.namespaced(from: namespace.key, to: nil)
76
150
  end
77
151
 
78
- build_component(identifier, path)
79
- end
152
+ file_name = "#{identifier.key_with_separator(PATH_SEPARATOR)}#{RB_EXT}"
80
153
 
81
- # Returns the full path of the component directory
82
- #
83
- # @return [Pathname]
84
- # @api private
85
- def full_path
86
- container.root.join(path)
87
- end
154
+ component_file =
155
+ if namespace.path?
156
+ full_path.join(namespace.path, file_name)
157
+ else
158
+ full_path.join(file_name)
159
+ end
88
160
 
89
- # @api private
90
- def component_options
91
- {
92
- auto_register: auto_register,
93
- loader: loader,
94
- memoize: memoize
95
- }
161
+ component_file if component_file.exist?
96
162
  end
97
163
 
98
- private
99
-
100
- def build_component(identifier, file_path)
164
+ def build_component(identifier, namespace, file_path)
101
165
  options = {
102
166
  inflector: container.config.inflector,
103
167
  **component_options,
104
168
  **MagicCommentsParser.(file_path)
105
169
  }
106
170
 
107
- Component.new(identifier, file_path: file_path, **options)
171
+ Component.new(identifier, namespace: namespace, **options)
108
172
  end
109
173
 
110
- def find_component_file(component_path)
111
- component_file = full_path.join("#{component_path}#{RB_EXT}")
112
- component_file if component_file.exist?
174
+ def component_options
175
+ {
176
+ auto_register: auto_register,
177
+ loader: loader,
178
+ memoize: memoize
179
+ }
113
180
  end
114
181
 
115
182
  def method_missing(name, *args, &block)
@@ -47,9 +47,9 @@ module Dry
47
47
  class Bootable
48
48
  DEFAULT_FINALIZE = proc {}
49
49
 
50
- # @!attribute [r] identifier
51
- # @return [Symbol] component's unique identifier
52
- attr_reader :identifier
50
+ # @!attribute [r] key
51
+ # @return [Symbol] component's unique name
52
+ attr_reader :name
53
53
 
54
54
  # @!attribute [r] options
55
55
  # @return [Hash] component's options
@@ -66,10 +66,10 @@ module Dry
66
66
  TRIGGER_MAP = Hash.new { |h, k| h[k] = [] }.freeze
67
67
 
68
68
  # @api private
69
- def initialize(identifier, options = {}, &block)
69
+ def initialize(name, options = {}, &block)
70
70
  @config = nil
71
71
  @config_block = nil
72
- @identifier = identifier
72
+ @name = name
73
73
  @triggers = {before: TRIGGER_MAP.dup, after: TRIGGER_MAP.dup}
74
74
  @options = block ? options.merge(block: block) : options
75
75
  @namespace = options[:namespace]
@@ -149,7 +149,7 @@ module Dry
149
149
  if block
150
150
  @settings_block = block
151
151
  elsif @settings_block
152
- @settings = Settings::DSL.new(identifier, &@settings_block).call
152
+ @settings = Settings::DSL.new(&@settings_block).call
153
153
  else
154
154
  @settings
155
155
  end
@@ -211,8 +211,8 @@ module Dry
211
211
  # @return [Dry::Struct]
212
212
  #
213
213
  # @api private
214
- def new(identifier, new_options = EMPTY_HASH)
215
- self.class.new(identifier, options.merge(new_options))
214
+ def new(name, new_options = EMPTY_HASH)
215
+ self.class.new(name, options.merge(new_options))
216
216
  end
217
217
 
218
218
  # Return a new instance with updated options
@@ -221,16 +221,7 @@ module Dry
221
221
  #
222
222
  # @api private
223
223
  def with(new_options)
224
- self.class.new(identifier, options.merge(new_options))
225
- end
226
-
227
- # Return true
228
- #
229
- # @return [TrueClass]
230
- #
231
- # @api private
232
- def bootable?
233
- true
224
+ self.class.new(name, options.merge(new_options))
234
225
  end
235
226
 
236
227
  private
@@ -256,7 +247,7 @@ module Dry
256
247
  when String, Symbol
257
248
  container.namespace(namespace) { |c| return c }
258
249
  when true
259
- container.namespace(identifier) { |c| return c }
250
+ container.namespace(name) { |c| return c }
260
251
  when nil
261
252
  container
262
253
  else