dry-system 0.18.1 → 1.0.1

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