dry-system 0.19.2 → 0.23.0

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