dry-system 0.18.1 → 1.0.1

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +678 -0
  3. data/LICENSE +1 -1
  4. data/README.md +5 -4
  5. data/dry-system.gemspec +18 -21
  6. data/lib/dry/system/auto_registrar.rb +9 -64
  7. data/lib/dry/system/component.rb +124 -104
  8. data/lib/dry/system/component_dir.rb +171 -0
  9. data/lib/dry/system/config/component_dir.rb +228 -0
  10. data/lib/dry/system/config/component_dirs.rb +289 -0
  11. data/lib/dry/system/config/namespace.rb +75 -0
  12. data/lib/dry/system/config/namespaces.rb +196 -0
  13. data/lib/dry/system/constants.rb +2 -4
  14. data/lib/dry/system/container.rb +305 -345
  15. data/lib/dry/system/errors.rb +73 -56
  16. data/lib/dry/system/identifier.rb +176 -0
  17. data/lib/dry/system/importer.rb +89 -12
  18. data/lib/dry/system/indirect_component.rb +63 -0
  19. data/lib/dry/system/loader/autoloading.rb +24 -0
  20. data/lib/dry/system/loader.rb +49 -41
  21. data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +13 -14
  22. data/lib/dry/system/plugins/bootsnap.rb +3 -2
  23. data/lib/dry/system/plugins/dependency_graph/strategies.rb +38 -2
  24. data/lib/dry/system/plugins/dependency_graph.rb +25 -21
  25. data/lib/dry/system/plugins/env.rb +3 -2
  26. data/lib/dry/system/plugins/logging.rb +9 -8
  27. data/lib/dry/system/plugins/monitoring.rb +1 -2
  28. data/lib/dry/system/plugins/notifications.rb +1 -1
  29. data/lib/dry/system/plugins/plugin.rb +61 -0
  30. data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
  31. data/lib/dry/system/plugins/zeitwerk.rb +109 -0
  32. data/lib/dry/system/plugins.rb +5 -73
  33. data/lib/dry/system/provider/source.rb +276 -0
  34. data/lib/dry/system/provider/source_dsl.rb +55 -0
  35. data/lib/dry/system/provider.rb +261 -23
  36. data/lib/dry/system/provider_registrar.rb +251 -0
  37. data/lib/dry/system/provider_source_registry.rb +56 -0
  38. data/lib/dry/system/provider_sources/settings/config.rb +73 -0
  39. data/lib/dry/system/provider_sources/settings/loader.rb +44 -0
  40. data/lib/dry/system/provider_sources/settings.rb +40 -0
  41. data/lib/dry/system/provider_sources.rb +5 -0
  42. data/lib/dry/system/stubs.rb +6 -2
  43. data/lib/dry/system/version.rb +1 -1
  44. data/lib/dry/system.rb +35 -13
  45. metadata +48 -97
  46. data/lib/dry/system/auto_registrar/configuration.rb +0 -43
  47. data/lib/dry/system/booter/component_registry.rb +0 -35
  48. data/lib/dry/system/booter.rb +0 -181
  49. data/lib/dry/system/components/bootable.rb +0 -289
  50. data/lib/dry/system/components/config.rb +0 -35
  51. data/lib/dry/system/components.rb +0 -8
  52. data/lib/dry/system/lifecycle.rb +0 -135
  53. data/lib/dry/system/provider_registry.rb +0 -27
  54. data/lib/dry/system/settings/file_loader.rb +0 -30
  55. data/lib/dry/system/settings/file_parser.rb +0 -51
  56. data/lib/dry/system/settings.rb +0 -67
  57. data/lib/dry/system/system_components/settings.rb +0 -11
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ <!--- this file is synced from dry-rb/template-gem project -->
1
2
  [gem]: https://rubygems.org/gems/dry-system
2
3
  [actions]: https://github.com/dry-rb/dry-system/actions
3
4
  [codacy]: https://www.codacy.com/gh/dry-rb/dry-system
@@ -10,19 +11,19 @@
10
11
  [![CI Status](https://github.com/dry-rb/dry-system/workflows/ci/badge.svg)][actions]
11
12
  [![Codacy Badge](https://api.codacy.com/project/badge/Grade/3a0e30d0ae2542c7ba047ba5f923c0bb)][codacy]
12
13
  [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/3a0e30d0ae2542c7ba047ba5f923c0bb)][codacy]
13
- [![Inline docs](http://inch-ci.org/github/dry-rb/dry-system.svg?branch=master)][inchpages]
14
+ [![Inline docs](http://inch-ci.org/github/dry-rb/dry-system.svg?branch=main)][inchpages]
14
15
 
15
16
  ## Links
16
17
 
17
- * [User documentation](http://dry-rb.org/gems/dry-system)
18
+ * [User documentation](https://dry-rb.org/gems/dry-system)
18
19
  * [API documentation](http://rubydoc.info/gems/dry-system)
19
20
 
20
21
  ## Supported Ruby versions
21
22
 
22
23
  This library officially supports the following Ruby versions:
23
24
 
24
- * MRI >= `2.4`
25
- * jruby >= `9.2`
25
+ * MRI `>= 2.7.0`
26
+ * jruby `>= 9.3` (postponed until 2.7 is supported)
26
27
 
27
28
  ## License
28
29
 
data/dry-system.gemspec CHANGED
@@ -1,41 +1,38 @@
1
1
  # frozen_string_literal: true
2
- # this file is managed by dry-rb/devtools project
3
2
 
4
- lib = File.expand_path('lib', __dir__)
3
+ # this file is synced from dry-rb/template-gem project
4
+
5
+ lib = File.expand_path("lib", __dir__)
5
6
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
- require 'dry/system/version'
7
+ require "dry/system/version"
7
8
 
8
9
  Gem::Specification.new do |spec|
9
- spec.name = 'dry-system'
10
+ spec.name = "dry-system"
10
11
  spec.authors = ["Piotr Solnica"]
11
12
  spec.email = ["piotr.solnica@gmail.com"]
12
- spec.license = 'MIT'
13
+ spec.license = "MIT"
13
14
  spec.version = Dry::System::VERSION.dup
14
15
 
15
16
  spec.summary = "Organize your code into reusable components"
16
17
  spec.description = spec.summary
17
- spec.homepage = 'https://dry-rb.org/gems/dry-system'
18
+ spec.homepage = "https://dry-rb.org/gems/dry-system"
18
19
  spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-system.gemspec", "lib/**/*"]
19
- spec.bindir = 'bin'
20
+ spec.bindir = "bin"
20
21
  spec.executables = []
21
- spec.require_paths = ['lib']
22
+ spec.require_paths = ["lib"]
22
23
 
23
- spec.metadata['allowed_push_host'] = 'https://rubygems.org'
24
- spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-system/blob/master/CHANGELOG.md'
25
- spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-system'
26
- spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-system/issues'
24
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
25
+ spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-system/blob/main/CHANGELOG.md"
26
+ spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-system"
27
+ spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-system/issues"
27
28
 
28
- spec.required_ruby_version = ">= 2.4.0"
29
+ spec.required_ruby_version = ">= 2.7.0"
29
30
 
30
31
  # to update dependencies edit project.yml
31
- spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
32
- spec.add_runtime_dependency "dry-auto_inject", ">= 0.4.0"
33
- spec.add_runtime_dependency "dry-configurable", "~> 0.11", ">= 0.11.1"
34
- spec.add_runtime_dependency "dry-container", "~> 0.7", ">= 0.7.2"
35
- spec.add_runtime_dependency "dry-core", "~> 0.3", ">= 0.3.1"
36
- spec.add_runtime_dependency "dry-equalizer", "~> 0.2"
37
- spec.add_runtime_dependency "dry-inflector", "~> 0.1", ">= 0.1.2"
38
- spec.add_runtime_dependency "dry-struct", "~> 1.0"
32
+ spec.add_runtime_dependency "dry-auto_inject", "~> 1.0", "< 2"
33
+ spec.add_runtime_dependency "dry-configurable", "~> 1.0", "< 2"
34
+ spec.add_runtime_dependency "dry-core", "~> 1.0", "< 2"
35
+ spec.add_runtime_dependency "dry-inflector", "~> 1.0", "< 2"
39
36
 
40
37
  spec.add_development_dependency "bundler"
41
38
  spec.add_development_dependency "rake"
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/system/constants"
4
- require "dry/system/magic_comments_parser"
5
- require "dry/system/auto_registrar/configuration"
6
4
 
7
5
  module Dry
8
6
  module System
@@ -17,83 +15,30 @@ module Dry
17
15
  class AutoRegistrar
18
16
  attr_reader :container
19
17
 
20
- attr_reader :config
21
-
22
18
  def initialize(container)
23
19
  @container = container
24
- @config = container.config
25
20
  end
26
21
 
27
22
  # @api private
28
23
  def finalize!
29
- Array(config.auto_register).each { |dir| call(dir) }
30
- end
31
-
32
- # @api private
33
- def call(dir)
34
- registration_config = Configuration.new
35
- yield(registration_config) if block_given?
36
- components(dir).each do |component|
37
- next if !component.auto_register? || registration_config.exclude.(component)
38
-
39
- container.require_component(component) do
40
- register(component.identifier, memoize: registration_config.memoize) {
41
- registration_config.instance.(component)
42
- }
43
- end
24
+ container.component_dirs.each do |component_dir|
25
+ call(component_dir) if component_dir.auto_register?
44
26
  end
45
27
  end
46
28
 
47
- private
48
-
49
- # @api private
50
- def components(dir)
51
- files(dir)
52
- .map { |file_name| [file_name, file_options(file_name)] }
53
- .map { |file_name, options| component(relative_path(dir, file_name), **options) }
54
- .reject { |component| registered?(component.identifier) }
55
- end
56
-
57
29
  # @api private
58
- def files(dir)
59
- components_dir = File.join(root, dir)
30
+ def call(component_dir)
31
+ component_dir.each_component do |component|
32
+ next unless register_component?(component)
60
33
 
61
- unless ::Dir.exist?(components_dir)
62
- raise ComponentsDirMissing, "Components dir '#{components_dir}' not found"
34
+ container.register(component.key, memoize: component.memoize?) { component.instance }
63
35
  end
64
-
65
- ::Dir["#{components_dir}/**/#{RB_GLOB}"].sort
66
- end
67
-
68
- # @api private
69
- def relative_path(dir, file_path)
70
- dir_root = root.join(dir.to_s.split("/")[0])
71
- file_path.to_s.sub("#{dir_root}/", "").sub(RB_EXT, EMPTY_STRING)
72
36
  end
73
37
 
74
- # @api private
75
- def file_options(file_name)
76
- MagicCommentsParser.(file_name)
77
- end
78
-
79
- # @api private
80
- def component(path, **options)
81
- container.component(path, **options)
82
- end
83
-
84
- # @api private
85
- def root
86
- container.root
87
- end
88
-
89
- # @api private
90
- def registered?(name)
91
- container.registered?(name)
92
- end
38
+ private
93
39
 
94
- # @api private
95
- def register(*args, &block)
96
- container.register(*args, &block)
40
+ def register_component?(component)
41
+ !container.registered?(component.key) && component.auto_register?
97
42
  end
98
43
  end
99
44
  end
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent/map"
4
-
5
- require "dry-equalizer"
3
+ require "pathname"
6
4
  require "dry/inflector"
7
- require "dry/system/loader"
8
5
  require "dry/system/errors"
9
6
  require "dry/system/constants"
10
7
 
@@ -14,157 +11,180 @@ module Dry
14
11
  # They expose an API to query this information and use a configurable
15
12
  # loader object to initialize class instances.
16
13
  #
17
- # Components are created automatically through auto-registration and can be
18
- # accessed through `Container.auto_register!` which yields them.
19
- #
20
14
  # @api public
21
15
  class Component
22
- include Dry::Equalizer(:identifier, :path)
16
+ include Dry::Equalizer(:identifier, :file_path, :namespace, :options)
23
17
 
24
18
  DEFAULT_OPTIONS = {
25
- separator: DEFAULT_SEPARATOR,
26
- namespace: nil,
27
- inflector: Dry::Inflector.new
19
+ inflector: Dry::Inflector.new,
20
+ loader: Loader
28
21
  }.freeze
29
22
 
30
23
  # @!attribute [r] identifier
31
- # @return [String] component's unique identifier
24
+ # @return [String] the component's unique identifier
32
25
  attr_reader :identifier
33
26
 
34
- # @!attribute [r] path
35
- # @return [String] component's relative path
36
- attr_reader :path
27
+ # @!attribute [r] file_path
28
+ # @return [Pathname] the component's source file path
29
+ attr_reader :file_path
37
30
 
38
- # @!attribute [r] file
39
- # @return [String] component's file name
40
- attr_reader :file
31
+ # @!attribute [r] namespace
32
+ # @return [Dry::System::Config::Namespace] the component's namespace
33
+ attr_reader :namespace
41
34
 
42
35
  # @!attribute [r] options
43
- # @return [Hash] component's options
36
+ # @return [Hash] the component's options
44
37
  attr_reader :options
45
38
 
46
- # @!attribute [r] loader
47
- # @return [Object#call] component's loader object
48
- attr_reader :loader
49
-
50
39
  # @api private
51
- def self.new(*args, &block)
52
- cache.fetch_or_store([*args, block].hash) do
53
- name, options = args
54
- options = DEFAULT_OPTIONS.merge(options || EMPTY_HASH)
55
-
56
- ns, sep, inflector = options.values_at(:namespace, :separator, :inflector)
57
- identifier = extract_identifier(name, ns, sep)
58
-
59
- path = name.to_s.gsub(sep, PATH_SEPARATOR)
60
- loader = options.fetch(:loader, Loader).new(path, inflector)
61
-
62
- super(identifier, path, options.merge(loader: loader))
63
- end
40
+ def initialize(identifier, file_path:, namespace:, **options)
41
+ @identifier = identifier
42
+ @file_path = Pathname(file_path)
43
+ @namespace = namespace
44
+ @options = DEFAULT_OPTIONS.merge(options)
64
45
  end
65
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
+ #
66
54
  # @api private
67
- def self.extract_identifier(name, ns, sep)
68
- name_s = name.to_s
69
- identifier = ns ? remove_namespace_from_name(name_s, ns) : name_s
70
-
71
- identifier.scan(WORD_REGEX).join(sep)
55
+ def loadable?
56
+ true
72
57
  end
73
58
 
74
- # @api private
75
- def self.remove_namespace_from_name(name, ns)
76
- match_value = name.match(/^(?<remove_namespace>#{ns})(?<separator>\W)(?<identifier>.*)/)
77
-
78
- match_value ? match_value[:identifier] : name
59
+ # Returns the component's instance
60
+ #
61
+ # @return [Object] component's class instance
62
+ # @api public
63
+ def instance(*args)
64
+ options[:instance]&.call(self, *args) || loader.call(self, *args)
79
65
  end
66
+ ruby2_keywords(:instance) if respond_to?(:ruby2_keywords, true)
80
67
 
81
- # @api private
82
- def self.cache
83
- @cache ||= Concurrent::Map.new
68
+ # Returns the component's unique key
69
+ #
70
+ # @return [String] the key
71
+ #
72
+ # @see Identifier#key
73
+ #
74
+ # @api public
75
+ def key
76
+ identifier.key
84
77
  end
85
78
 
86
- # @api private
87
- def initialize(identifier, path, options)
88
- @identifier = identifier
89
- @path = path
90
- @options = options
91
- @file = "#{path}#{RB_EXT}"
92
- @loader = options.fetch(:loader)
93
- freeze
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
86
+ def root_key
87
+ identifier.root_key
94
88
  end
95
89
 
96
- # Returns components instance
90
+ # Returns a path-delimited representation of the compnent, appropriate for passing
91
+ # to `Kernel#require` to require its source file
97
92
  #
98
- # @example
99
- # class MyApp < Dry::System::Container
100
- # configure do |config|
101
- # config.name = :my_app
102
- # config.root = Pathname('/my/app')
103
- # end
93
+ # The path takes into account the rules of the namespace used to load the component.
104
94
  #
105
- # auto_register!('lib/clients') do |component|
106
- # # some custom initialization logic, ie:
107
- # constant = component.loader.constant
108
- # constant.create
109
- # end
110
- # end
95
+ # @example Component from a root namespace
96
+ # component.key # => "articles.create"
97
+ # component.require_path # => "articles/create"
111
98
  #
112
- # @return [Object] component's class instance
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
113
107
  #
114
108
  # @api public
115
- def instance(*args)
116
- loader.call(*args)
117
- end
118
- ruby2_keywords(:instance) if respond_to?(:ruby2_keywords, true)
119
-
120
- # @api private
121
- def bootable?
122
- false
123
- end
124
-
125
- # @api private
126
- def file_exists?(paths)
127
- paths.any? { |path| path.join(file).exist? }
109
+ def require_path
110
+ if namespace.path
111
+ "#{namespace.path}#{PATH_SEPARATOR}#{path_in_namespace}"
112
+ else
113
+ path_in_namespace
114
+ end
128
115
  end
129
116
 
130
- # @api private
131
- def prepend(name)
132
- self.class.new(
133
- [name, identifier].join(separator), options.merge(loader: loader.class)
134
- )
135
- end
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(KEY_SEPARATOR, PATH_SEPARATOR)
136
141
 
137
- # @api private
138
- def namespaced(namespace)
139
- self.class.new(
140
- path, options.merge(loader: loader.class, namespace: namespace)
141
- )
142
+ if namespace_const_path
143
+ "#{namespace_const_path}#{PATH_SEPARATOR}#{path_in_namespace}"
144
+ else
145
+ path_in_namespace
146
+ end
142
147
  end
143
148
 
144
149
  # @api private
145
- def separator
146
- options[:separator]
150
+ def loader
151
+ options.fetch(:loader)
147
152
  end
148
153
 
149
154
  # @api private
150
- def namespace
151
- options[:namespace]
155
+ def inflector
156
+ options.fetch(:inflector)
152
157
  end
153
158
 
154
159
  # @api private
155
160
  def auto_register?
156
- !!options.fetch(:auto_register) { true }
161
+ callable_option?(options[:auto_register])
157
162
  end
158
163
 
159
164
  # @api private
160
- def root_key
161
- namespaces.first
165
+ def memoize?
166
+ callable_option?(options[:memoize])
162
167
  end
163
168
 
164
169
  private
165
170
 
166
- def namespaces
167
- identifier.split(separator).map(&:to_sym)
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
+
182
+ def callable_option?(value)
183
+ if value.respond_to?(:call)
184
+ !!value.call(self)
185
+ else
186
+ !!value
187
+ end
168
188
  end
169
189
  end
170
190
  end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "dry/system/constants"
5
+
6
+ module Dry
7
+ module System
8
+ # A configured component directory within the container's root. Provides access to the
9
+ # component directory's configuration, as well as methods for locating component files
10
+ # within the directory
11
+ #
12
+ # @see Dry::System::Config::ComponentDir
13
+ # @api private
14
+ class ComponentDir
15
+ # @!attribute [r] config
16
+ # @return [Dry::System::Config::ComponentDir] the component directory configuration
17
+ # @api private
18
+ attr_reader :config
19
+
20
+ # @!attribute [r] container
21
+ # @return [Dry::System::Container] the container managing the component directory
22
+ # @api private
23
+ attr_reader :container
24
+
25
+ # @api private
26
+ def initialize(config:, container:)
27
+ @config = config
28
+ @container = container
29
+ end
30
+
31
+ # Returns a component for the given key if a matching source file is found within
32
+ # the component dir
33
+ #
34
+ # This searches according to the component dir's configured namespaces, in order of
35
+ # definition, with the first match returned as the component.
36
+ #
37
+ # @param key [String] the component's key
38
+ # @return [Dry::System::Component, nil] the component, if found
39
+ #
40
+ # @api private
41
+ def component_for_key(key)
42
+ config.namespaces.each do |namespace|
43
+ identifier = Identifier.new(key)
44
+
45
+ next unless identifier.start_with?(namespace.key)
46
+
47
+ if (file_path = find_component_file(identifier, namespace))
48
+ return build_component(identifier, namespace, file_path)
49
+ end
50
+ end
51
+
52
+ nil
53
+ end
54
+
55
+ def each_component
56
+ return enum_for(:each_component) unless block_given?
57
+
58
+ each_file do |file_path, namespace|
59
+ yield component_for_path(file_path, namespace)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def each_file
66
+ return enum_for(:each_file) unless block_given?
67
+
68
+ raise ComponentDirNotFoundError, full_path unless Dir.exist?(full_path)
69
+
70
+ config.namespaces.each do |namespace|
71
+ files(namespace).each do |file|
72
+ yield file, namespace
73
+ end
74
+ end
75
+ end
76
+
77
+ def files(namespace)
78
+ if namespace.path?
79
+ Dir[File.join(full_path, namespace.path, "**", RB_GLOB)].sort
80
+ else
81
+ non_root_paths = config.namespaces.to_a.reject(&:root?).map(&:path)
82
+
83
+ Dir[File.join(full_path, "**", RB_GLOB)].reject { |file_path|
84
+ Pathname(file_path).relative_path_from(full_path).to_s.start_with?(*non_root_paths)
85
+ }.sort
86
+ end
87
+ end
88
+
89
+ # Returns the full path of the component directory
90
+ #
91
+ # @return [Pathname]
92
+ def full_path
93
+ container.root.join(path)
94
+ end
95
+
96
+ # Returns a component for a full path to a Ruby source file within the component dir
97
+ #
98
+ # @param path [String] the full path to the file
99
+ # @return [Dry::System::Component] the component
100
+ def component_for_path(path, namespace)
101
+ key = Pathname(path).relative_path_from(full_path).to_s
102
+ .sub(RB_EXT, EMPTY_STRING)
103
+ .scan(WORD_REGEX)
104
+ .join(KEY_SEPARATOR)
105
+
106
+ identifier = Identifier.new(key)
107
+ .namespaced(
108
+ from: namespace.path&.gsub(PATH_SEPARATOR, KEY_SEPARATOR),
109
+ to: namespace.key
110
+ )
111
+
112
+ build_component(identifier, namespace, path)
113
+ end
114
+
115
+ def find_component_file(identifier, namespace)
116
+ # To properly find the file within a namespace with a key, we should strip the key
117
+ # from beginning of our given identifier
118
+ if namespace.key
119
+ identifier = identifier.namespaced(from: namespace.key, to: nil)
120
+ end
121
+
122
+ file_name = "#{identifier.key_with_separator(PATH_SEPARATOR)}#{RB_EXT}"
123
+
124
+ component_file =
125
+ if namespace.path?
126
+ full_path.join(namespace.path, file_name)
127
+ else
128
+ full_path.join(file_name)
129
+ end
130
+
131
+ component_file if component_file.exist?
132
+ end
133
+
134
+ def build_component(identifier, namespace, file_path)
135
+ options = {
136
+ inflector: container.config.inflector,
137
+ **component_options,
138
+ **MagicCommentsParser.(file_path)
139
+ }
140
+
141
+ Component.new(
142
+ identifier,
143
+ namespace: namespace,
144
+ file_path: file_path,
145
+ **options
146
+ )
147
+ end
148
+
149
+ def component_options
150
+ {
151
+ auto_register: auto_register,
152
+ loader: loader,
153
+ instance: instance,
154
+ memoize: memoize
155
+ }
156
+ end
157
+
158
+ def method_missing(name, *args, &block)
159
+ if config.respond_to?(name)
160
+ config.public_send(name, *args, &block)
161
+ else
162
+ super
163
+ end
164
+ end
165
+
166
+ def respond_to_missing?(name, include_all = false)
167
+ config.respond_to?(name) || super
168
+ end
169
+ end
170
+ end
171
+ end