dry-system 0.19.2 → 0.23.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +472 -1
  3. data/LICENSE +1 -1
  4. data/README.md +4 -3
  5. data/dry-system.gemspec +16 -15
  6. data/lib/dry/system/auto_registrar.rb +1 -13
  7. data/lib/dry/system/component.rb +104 -47
  8. data/lib/dry/system/component_dir.rb +88 -47
  9. data/lib/dry/system/components.rb +8 -4
  10. data/lib/dry/system/config/component_dir.rb +141 -53
  11. data/lib/dry/system/config/component_dirs.rb +176 -70
  12. data/lib/dry/system/config/namespace.rb +76 -0
  13. data/lib/dry/system/config/namespaces.rb +208 -0
  14. data/lib/dry/system/constants.rb +2 -2
  15. data/lib/dry/system/container.rb +279 -201
  16. data/lib/dry/system/errors.rb +72 -61
  17. data/lib/dry/system/identifier.rb +99 -79
  18. data/lib/dry/system/importer.rb +83 -12
  19. data/lib/dry/system/indirect_component.rb +65 -0
  20. data/lib/dry/system/loader.rb +8 -4
  21. data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +12 -13
  22. data/lib/dry/system/plugins/bootsnap.rb +3 -2
  23. data/lib/dry/system/plugins/dependency_graph/strategies.rb +37 -1
  24. data/lib/dry/system/plugins/dependency_graph.rb +26 -20
  25. data/lib/dry/system/plugins/env.rb +3 -2
  26. data/lib/dry/system/plugins/logging.rb +9 -5
  27. data/lib/dry/system/plugins/monitoring.rb +1 -1
  28. data/lib/dry/system/plugins/notifications.rb +1 -1
  29. data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
  30. data/lib/dry/system/plugins/zeitwerk.rb +109 -0
  31. data/lib/dry/system/plugins.rb +8 -7
  32. data/lib/dry/system/provider/source.rb +324 -0
  33. data/lib/dry/system/provider/source_dsl.rb +94 -0
  34. data/lib/dry/system/provider.rb +264 -24
  35. data/lib/dry/system/provider_registrar.rb +276 -0
  36. data/lib/dry/system/provider_source_registry.rb +70 -0
  37. data/lib/dry/system/provider_sources/settings/config.rb +86 -0
  38. data/lib/dry/system/provider_sources/settings/loader.rb +53 -0
  39. data/lib/dry/system/provider_sources/settings.rb +40 -0
  40. data/lib/dry/system/provider_sources.rb +5 -0
  41. data/lib/dry/system/stubs.rb +1 -1
  42. data/lib/dry/system/version.rb +1 -1
  43. data/lib/dry/system.rb +45 -13
  44. metadata +25 -22
  45. data/lib/dry/system/booter/component_registry.rb +0 -35
  46. data/lib/dry/system/booter.rb +0 -200
  47. data/lib/dry/system/components/bootable.rb +0 -289
  48. data/lib/dry/system/components/config.rb +0 -35
  49. data/lib/dry/system/lifecycle.rb +0 -135
  50. data/lib/dry/system/provider_registry.rb +0 -27
  51. data/lib/dry/system/settings/file_loader.rb +0 -30
  52. data/lib/dry/system/settings/file_parser.rb +0 -51
  53. data/lib/dry/system/settings.rb +0 -67
  54. data/lib/dry/system/system_components/settings.rb +0 -11
@@ -17,52 +17,42 @@ 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
- separator: DEFAULT_SEPARATOR,
24
23
  inflector: Dry::Inflector.new,
25
24
  loader: Loader
26
25
  }.freeze
27
26
 
28
27
  # @!attribute [r] identifier
29
- # @return [String] component's unique identifier
28
+ # @return [String] the component's unique identifier
30
29
  attr_reader :identifier
31
30
 
32
- # @!attribute [r] file_path
33
- # @return [String, nil] full path to the component's file, if found
34
- attr_reader :file_path
31
+ # @!attribute [r] namespace
32
+ # @return [Dry::System::Config::Namespace] the component's namespace
33
+ attr_reader :namespace
35
34
 
36
35
  # @!attribute [r] options
37
- # @return [Hash] component's options
36
+ # @return [Hash] the component's options
38
37
  attr_reader :options
39
38
 
40
39
  # @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)
40
+ def initialize(identifier, namespace:, **options)
41
+ @identifier = identifier
42
+ @namespace = namespace
43
+ @options = DEFAULT_OPTIONS.merge(options)
59
44
  end
60
45
 
46
+ # Returns true, indicating that the component is directly loadable from the files
47
+ # managed by the container
48
+ #
49
+ # This is the inverse of {IndirectComponent#loadable?}
50
+ #
51
+ # @return [TrueClass]
52
+ #
61
53
  # @api private
62
- def initialize(identifier, file_path: nil, **options)
63
- @identifier = identifier
64
- @file_path = file_path
65
- @options = options
54
+ def loadable?
55
+ true
66
56
  end
67
57
 
68
58
  # Returns the component's instance
@@ -70,43 +60,99 @@ module Dry
70
60
  # @return [Object] component's class instance
71
61
  # @api public
72
62
  def instance(*args)
73
- loader.call(self, *args)
63
+ options[:instance]&.call(self, *args) || loader.call(self, *args)
74
64
  end
75
65
  ruby2_keywords(:instance) if respond_to?(:ruby2_keywords, true)
76
66
 
77
- # @api private
78
- def bootable?
79
- false
80
- end
81
-
67
+ # Returns the component's unique key
68
+ #
69
+ # @return [String] the key
70
+ #
71
+ # @see Identifier#key
72
+ #
73
+ # @api public
82
74
  def key
83
- identifier.to_s
84
- end
85
-
86
- def path
87
- identifier.path
75
+ identifier.key
88
76
  end
89
77
 
78
+ # Returns the root namespace segment of the component's key, as a symbol
79
+ #
80
+ # @see Identifier#root_key
81
+ #
82
+ # @return [Symbol] the root key
83
+ #
84
+ # @api public
90
85
  def root_key
91
86
  identifier.root_key
92
87
  end
93
88
 
94
- # Returns true if the component has a corresponding file
89
+ # Returns a path-delimited representation of the compnent, appropriate for passing
90
+ # to `Kernel#require` to require its source file
95
91
  #
96
- # @return [Boolean]
97
- # @api private
98
- def file_exists?
99
- !!file_path
92
+ # The path takes into account the rules of the namespace used to load the component.
93
+ #
94
+ # @example Component from a root namespace
95
+ # component.key # => "articles.create"
96
+ # component.require_path # => "articles/create"
97
+ #
98
+ # @example Component from an "admin/" path namespace (with `key: nil`)
99
+ # component.key # => "articles.create"
100
+ # component.require_path # => "admin/articles/create"
101
+ #
102
+ # @see Config::Namespaces#add
103
+ # @see Config::Namespace
104
+ #
105
+ # @return [String] the require path
106
+ #
107
+ # @api public
108
+ def require_path
109
+ if namespace.path
110
+ "#{namespace.path}#{PATH_SEPARATOR}#{path_in_namespace}"
111
+ else
112
+ path_in_namespace
113
+ end
114
+ end
115
+
116
+ # Returns an "underscored", path-delimited representation of the component,
117
+ # appropriate for passing to the inflector for constantizing
118
+ #
119
+ # The const path takes into account the rules of the namespace used to load the
120
+ # component.
121
+ #
122
+ # @example Component from a namespace with `const: nil`
123
+ # component.key # => "articles.create_article"
124
+ # component.const_path # => "articles/create_article"
125
+ # component.inflector.constantize(component.const_path) # => Articles::CreateArticle
126
+ #
127
+ # @example Component from a namespace with `const: "admin"`
128
+ # component.key # => "articles.create_article"
129
+ # component.const_path # => "admin/articles/create_article"
130
+ # component.inflector.constantize(component.const_path) # => Admin::Articles::CreateArticle
131
+ #
132
+ # @see Config::Namespaces#add
133
+ # @see Config::Namespace
134
+ #
135
+ # @return [String] the const path
136
+ #
137
+ # @api public
138
+ def const_path
139
+ namespace_const_path = namespace.const&.gsub(KEY_SEPARATOR, PATH_SEPARATOR)
140
+
141
+ if namespace_const_path
142
+ "#{namespace_const_path}#{PATH_SEPARATOR}#{path_in_namespace}"
143
+ else
144
+ path_in_namespace
145
+ end
100
146
  end
101
147
 
102
148
  # @api private
103
149
  def loader
104
- options[:loader]
150
+ options.fetch(:loader)
105
151
  end
106
152
 
107
153
  # @api private
108
154
  def inflector
109
- options[:inflector]
155
+ options.fetch(:inflector)
110
156
  end
111
157
 
112
158
  # @api private
@@ -121,6 +167,17 @@ module Dry
121
167
 
122
168
  private
123
169
 
170
+ def path_in_namespace
171
+ identifier_in_namespace =
172
+ if namespace.key
173
+ identifier.namespaced(from: namespace.key, to: nil)
174
+ else
175
+ identifier
176
+ end
177
+
178
+ identifier_in_namespace.key_with_separator(PATH_SEPARATOR)
179
+ end
180
+
124
181
  def callable_option?(value)
125
182
  if value.respond_to?(:call)
126
183
  !!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,88 +31,126 @@ 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
+ config.namespaces.each do |namespace|
46
+ identifier = Identifier.new(key)
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
50
53
  end
51
54
 
52
- identifier = identifier.with(namespace: nil)
53
- if (file_path = find_component_file(identifier.path))
54
- build_component(identifier, file_path)
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)
55
63
  end
56
64
  end
57
65
 
58
- # Returns a component for a full path to a Ruby source file within the component dir
59
- #
60
- # @param path [String] the full path to the file
61
- # @return [Dry::System::Component] the component
62
- #
63
- # @api private
64
- def component_for_path(path)
65
- separator = container.config.namespace_separator
66
+ private
66
67
 
67
- key = Pathname(path).relative_path_from(full_path).to_s
68
- .sub(RB_EXT, EMPTY_STRING)
69
- .scan(WORD_REGEX)
70
- .join(separator)
68
+ def each_file
69
+ return enum_for(:each_file) unless block_given?
71
70
 
72
- identifier = Identifier.new(key, separator: separator)
71
+ raise ComponentDirNotFoundError, full_path unless Dir.exist?(full_path)
73
72
 
74
- if identifier.start_with?(default_namespace)
75
- identifier = identifier.dequalified(default_namespace, namespace: default_namespace)
73
+ config.namespaces.each do |namespace|
74
+ files(namespace).each do |file|
75
+ yield file, namespace
76
+ end
76
77
  end
78
+ end
77
79
 
78
- build_component(identifier, path)
80
+ def files(namespace)
81
+ if namespace.path?
82
+ Dir[File.join(full_path, namespace.path, "**", RB_GLOB)].sort
83
+ else
84
+ non_root_paths = config.namespaces.to_a.reject(&:root?).map(&:path)
85
+
86
+ Dir[File.join(full_path, "**", RB_GLOB)].reject { |file_path|
87
+ Pathname(file_path).relative_path_from(full_path).to_s.start_with?(*non_root_paths)
88
+ }.sort
89
+ end
79
90
  end
80
91
 
81
92
  # Returns the full path of the component directory
82
93
  #
83
94
  # @return [Pathname]
84
- # @api private
85
95
  def full_path
86
96
  container.root.join(path)
87
97
  end
88
98
 
89
- # @api private
90
- def component_options
91
- {
92
- auto_register: auto_register,
93
- loader: loader,
94
- memoize: memoize
95
- }
99
+ # Returns a component for a full path to a Ruby source file within the component dir
100
+ #
101
+ # @param path [String] the full path to the file
102
+ # @return [Dry::System::Component] the component
103
+ def component_for_path(path, namespace)
104
+ key = Pathname(path).relative_path_from(full_path).to_s
105
+ .sub(RB_EXT, EMPTY_STRING)
106
+ .scan(WORD_REGEX)
107
+ .join(KEY_SEPARATOR)
108
+
109
+ identifier = Identifier.new(key)
110
+ .namespaced(
111
+ from: namespace.path&.gsub(PATH_SEPARATOR, KEY_SEPARATOR),
112
+ to: namespace.key
113
+ )
114
+
115
+ build_component(identifier, namespace, path)
96
116
  end
97
117
 
98
- private
118
+ def find_component_file(identifier, namespace)
119
+ # To properly find the file within a namespace with a key, we should strip the key
120
+ # from beginning of our given identifier
121
+ if namespace.key
122
+ identifier = identifier.namespaced(from: namespace.key, to: nil)
123
+ end
99
124
 
100
- def build_component(identifier, file_path)
125
+ file_name = "#{identifier.key_with_separator(PATH_SEPARATOR)}#{RB_EXT}"
126
+
127
+ component_file =
128
+ if namespace.path?
129
+ full_path.join(namespace.path, file_name)
130
+ else
131
+ full_path.join(file_name)
132
+ end
133
+
134
+ component_file if component_file.exist?
135
+ end
136
+
137
+ def build_component(identifier, namespace, file_path)
101
138
  options = {
102
139
  inflector: container.config.inflector,
103
140
  **component_options,
104
141
  **MagicCommentsParser.(file_path)
105
142
  }
106
143
 
107
- Component.new(identifier, file_path: file_path, **options)
144
+ Component.new(identifier, namespace: namespace, **options)
108
145
  end
109
146
 
110
- def find_component_file(component_path)
111
- component_file = full_path.join("#{component_path}#{RB_EXT}")
112
- component_file if component_file.exist?
147
+ def component_options
148
+ {
149
+ auto_register: auto_register,
150
+ loader: loader,
151
+ instance: instance,
152
+ memoize: memoize
153
+ }
113
154
  end
114
155
 
115
156
  def method_missing(name, *args, &block)
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/system"
3
+ require "dry/core/deprecations"
4
4
 
5
- Dry::System.register_provider(
6
- :system,
7
- boot_path: Pathname(__dir__).join("system_components").realpath
5
+ Dry::Core::Deprecations.announce(
6
+ "require \"dry/system/components\"",
7
+ "Use `require \"dry/system/provider_sources\"` instead",
8
+ tag: "dry-system",
9
+ uplevel: 1
8
10
  )
11
+
12
+ require_relative "provider_sources"