dry-system 0.18.1 → 0.19.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.
@@ -0,0 +1,128 @@
1
+ require "pathname"
2
+ require_relative "constants"
3
+ require_relative "identifier"
4
+ require_relative "magic_comments_parser"
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 a given identifier if a matching component file could be
32
+ # found within the component dir
33
+ #
34
+ # This will search within the component dir's configured default_namespace first,
35
+ # then fall back to searching for a non-namespaced file
36
+ #
37
+ # @param identifier [String] the identifier string
38
+ # @return [Dry::System::Component, nil] the component, if found
39
+ #
40
+ # @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)
50
+ end
51
+
52
+ identifier = identifier.with(namespace: nil)
53
+ if (file_path = find_component_file(identifier.path))
54
+ build_component(identifier, file_path)
55
+ end
56
+ end
57
+
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
+
67
+ key = Pathname(path).relative_path_from(full_path).to_s
68
+ .sub(RB_EXT, EMPTY_STRING)
69
+ .scan(WORD_REGEX)
70
+ .join(separator)
71
+
72
+ identifier = Identifier.new(key, separator: separator)
73
+
74
+ if identifier.start_with?(default_namespace)
75
+ identifier = identifier.dequalified(default_namespace, namespace: default_namespace)
76
+ end
77
+
78
+ build_component(identifier, path)
79
+ end
80
+
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
88
+
89
+ # @api private
90
+ def component_options
91
+ {
92
+ auto_register: auto_register,
93
+ loader: loader,
94
+ memoize: memoize
95
+ }
96
+ end
97
+
98
+ private
99
+
100
+ def build_component(identifier, file_path)
101
+ options = {
102
+ inflector: container.config.inflector,
103
+ **component_options,
104
+ **MagicCommentsParser.(file_path)
105
+ }
106
+
107
+ Component.new(identifier, file_path: file_path, **options)
108
+ end
109
+
110
+ def find_component_file(component_path)
111
+ component_file = full_path.join("#{component_path}#{RB_EXT}")
112
+ component_file if component_file.exist?
113
+ end
114
+
115
+ def method_missing(name, *args, &block)
116
+ if config.respond_to?(name)
117
+ config.public_send(name, *args, &block)
118
+ else
119
+ super
120
+ end
121
+ end
122
+
123
+ def respond_to_missing?(name, include_all = false)
124
+ config.respond_to?(name) || super
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,202 @@
1
+ require "dry/configurable"
2
+ require "dry/system/loader"
3
+
4
+ module Dry
5
+ module System
6
+ module Config
7
+ class ComponentDir
8
+ include Dry::Configurable
9
+
10
+ # @!group Settings
11
+
12
+ # @!method auto_register=(policy)
13
+ #
14
+ # Sets the auto-registration policy for the component dir.
15
+ #
16
+ # This may be a simple boolean to enable or disable auto-registration for all
17
+ # components, or a proc accepting a `Dry::Sytem::Component` and returning a
18
+ # boolean to configure auto-registration on a per-component basis
19
+ #
20
+ # Defaults to `true`.
21
+ #
22
+ # @param policy [Boolean, Proc]
23
+ # @return [Boolean, Proc]
24
+ #
25
+ # @example
26
+ # dir.auto_register = false
27
+ #
28
+ # @example
29
+ # dir.auto_register = proc do |component|
30
+ # !component.start_with?("entities")
31
+ # end
32
+ #
33
+ # @see auto_register
34
+ # @see Component
35
+ #
36
+ # @!method auto_register
37
+ #
38
+ # Returns the configured auto-registration policy.
39
+ #
40
+ # @return [Boolean, Proc] the configured policy
41
+ #
42
+ # @see auto_register=
43
+ setting :auto_register, true
44
+
45
+ # @!method add_to_load_path=(policy)
46
+ #
47
+ # Sets whether the dir should be added to the `$LOAD_PATH` after the container
48
+ # is configured.
49
+ #
50
+ # Defaults to `true`. This may need to be set to `false` when using a class
51
+ # autoloading system.
52
+ #
53
+ # @param policy [Boolean]
54
+ # @return [Boolean]
55
+ #
56
+ # @see add_to_load_path
57
+ # @see Container.configure
58
+ #
59
+ # @!method add_to_load_path
60
+ #
61
+ # Returns the configured value.
62
+ #
63
+ # @return [Boolean]
64
+ #
65
+ # @see add_to_load_path=
66
+ setting :add_to_load_path, true
67
+
68
+ # @!method default_namespace=(leading_namespace)
69
+ #
70
+ # Sets the leading namespace segments to be stripped when registering components
71
+ # from the dir in the container.
72
+ #
73
+ # This is useful to configure when the dir contains components in a module
74
+ # namespace that you don't want repeated in their identifiers.
75
+ #
76
+ # Defaults to `nil`.
77
+ #
78
+ # @param leading_namespace [String, nil]
79
+ # @return [String, nil]
80
+ #
81
+ # @example
82
+ # dir.default_namespace = "my_app"
83
+ #
84
+ # @example
85
+ # dir.default_namespace = "my_app.admin"
86
+ #
87
+ # @see default_namespace
88
+ #
89
+ # @!method default_namespace
90
+ #
91
+ # Returns the configured value.
92
+ #
93
+ # @return [String, nil]
94
+ #
95
+ # @see default_namespace=
96
+ setting :default_namespace
97
+
98
+ # @!method loader=(loader)
99
+ #
100
+ # Sets the loader to use when registering coponents from the dir in the container.
101
+ #
102
+ # Defaults to `Dry::System::Loader`.
103
+ #
104
+ # When using a class autoloader, consider using `Dry::System::Loader::Autoloading`
105
+ #
106
+ # @param loader [#call] the loader
107
+ # @return [#call] the configured loader
108
+ #
109
+ # @see loader
110
+ # @see Loader
111
+ # @see Loader::Autoloading
112
+ #
113
+ # @!method loader
114
+ #
115
+ # Returns the configured loader.
116
+ #
117
+ # @return [#call]
118
+ #
119
+ # @see loader=
120
+ setting :loader, Dry::System::Loader
121
+
122
+ # @!method memoize=(policy)
123
+ #
124
+ # Sets whether to memoize components from the dir when registered in the
125
+ # container.
126
+ #
127
+ # This may be a simple boolean to enable or disable memoization for all
128
+ # components, or a proc accepting a `Dry::Sytem::Component` and returning a
129
+ # boolean to configure memoization on a per-component basis
130
+ #
131
+ # Defaults to `false`.
132
+ #
133
+ # @param policy [Boolean, Proc]
134
+ # @return [Boolean, Proc] the configured memoization policy
135
+ #
136
+ # @example
137
+ # dir.memoize = true
138
+ #
139
+ # @example
140
+ # dir.memoize = proc do |component|
141
+ # !component.start_with?("providers")
142
+ # end
143
+ #
144
+ # @see memoize
145
+ # @see Component
146
+ #
147
+ # @!method memoize
148
+ #
149
+ # Returns the configured memoization policy.
150
+ #
151
+ # @return [Boolean, Proc] the configured memoization policy
152
+ #
153
+ # @see memoize=
154
+ setting :memoize, false
155
+
156
+ # @!endgroup
157
+
158
+ # Returns the component dir path, relative to the configured container root
159
+ #
160
+ # @return [String] the path
161
+ attr_reader :path
162
+
163
+ # @api private
164
+ def initialize(path)
165
+ super()
166
+ @path = path
167
+ yield self if block_given?
168
+ end
169
+
170
+ # @api private
171
+ def auto_register?
172
+ !!config.auto_register
173
+ end
174
+
175
+ # Returns true if a setting has been explicitly configured and is not returning
176
+ # just a default value.
177
+ #
178
+ # This is used to determine which settings from `ComponentDirs` should be applied
179
+ # as additional defaults.
180
+ #
181
+ # @api private
182
+ def configured?(key)
183
+ config._settings[key].input_defined?
184
+ end
185
+
186
+ private
187
+
188
+ def method_missing(name, *args, &block)
189
+ if config.respond_to?(name)
190
+ config.public_send(name, *args, &block)
191
+ else
192
+ super
193
+ end
194
+ end
195
+
196
+ def respond_to_missing?(name, include_all = false)
197
+ config.respond_to?(name) || super
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,184 @@
1
+ require "concurrent/map"
2
+ require "dry/configurable"
3
+ require "dry/system/constants"
4
+ require "dry/system/errors"
5
+ require_relative "component_dir"
6
+
7
+ module Dry
8
+ module System
9
+ module Config
10
+ class ComponentDirs
11
+ include Dry::Configurable
12
+
13
+ # Settings from ComponentDir are configured here as defaults for all added dirs
14
+ ComponentDir._settings.each do |setting|
15
+ _settings << setting.dup
16
+ end
17
+
18
+ # @!group Settings
19
+
20
+ # @!method auto_register=(value)
21
+ #
22
+ # Sets a default `auto_register` for all added component dirs
23
+ #
24
+ # @see ComponentDir.auto_register
25
+ # @see auto_register
26
+ #
27
+ # @!method auto_register
28
+ #
29
+ # Returns the configured default `auto_register`
30
+ #
31
+ # @see auto_register=
32
+
33
+ # @!method add_to_load_path=(value)
34
+ #
35
+ # Sets a default `add_to_load_path` value for all added component dirs
36
+ #
37
+ # @see ComponentDir.add_to_load_path
38
+ # @see add_to_load_path
39
+ #
40
+ # @!method add_to_load_path
41
+ #
42
+ # Returns the configured default `add_to_load_path`
43
+ #
44
+ # @see add_to_load_path=
45
+
46
+ # @!method default_namespace=(value)
47
+ #
48
+ # Sets a default `default_namespace` value for all added component dirs
49
+ #
50
+ # @see ComponentDir.default_namespace
51
+ # @see default_namespace
52
+ #
53
+ # @!method default_namespace
54
+ #
55
+ # Returns the configured default `default_namespace`
56
+ #
57
+ # @see default_namespace=
58
+
59
+ # @!method loader=(value)
60
+ #
61
+ # Sets a default `loader` value for all added component dirs
62
+ #
63
+ # @see ComponentDir.loader
64
+ # @see loader
65
+ #
66
+ # @!method loader
67
+ #
68
+ # Returns the configured default `loader`
69
+ #
70
+ # @see loader=
71
+
72
+ # @!method memoize=(value)
73
+ #
74
+ # Sets a default `memoize` value for all added component dirs
75
+ #
76
+ # @see ComponentDir.memoize
77
+ # @see memoize
78
+ #
79
+ # @!method memoize
80
+ #
81
+ # Returns the configured default `memoize`
82
+ #
83
+ # @see memoize=
84
+
85
+ # @!endgroup
86
+
87
+ # @api private
88
+ def initialize
89
+ @dirs = Concurrent::Map.new
90
+ end
91
+
92
+ # @api private
93
+ def initialize_copy(source)
94
+ super
95
+ @dirs = source.dirs.dup
96
+ end
97
+
98
+ # Adds and configures a component dir
99
+ #
100
+ # @param path [String] the path for the component dir, relative to the configured
101
+ # container root
102
+ #
103
+ # @yieldparam dir [ComponentDir] the component dir to configure
104
+ #
105
+ # @return [ComponentDir] the added component dir
106
+ #
107
+ # @example
108
+ # component_dirs.add "lib" do |dir|
109
+ # dir.default_namespace = "my_app"
110
+ # end
111
+ #
112
+ # @see ComponentDir
113
+ def add(path)
114
+ raise ComponentDirAlreadyAddedError, path if dirs.key?(path)
115
+
116
+ dirs[path] = ComponentDir.new(path).tap do |dir|
117
+ apply_defaults_to_dir(dir)
118
+ yield dir if block_given?
119
+ end
120
+ end
121
+
122
+ # Returns the added component dirs, with default settings applied
123
+ #
124
+ # @return [Hash<String, ComponentDir>] the component dirs as a hash, keyed by path
125
+ def dirs
126
+ @dirs.each { |_, dir| apply_defaults_to_dir(dir) }
127
+ end
128
+
129
+ # Returns the added component dirs, with default settings applied
130
+ #
131
+ # @return [Array<ComponentDir>]
132
+ def to_a
133
+ dirs.values
134
+ end
135
+
136
+ # Calls the given block once for each added component dir, passing the dir as an
137
+ # argument.
138
+ #
139
+ # @yieldparam dir [ComponentDir] the yielded component dir
140
+ def each(&block)
141
+ to_a.each(&block)
142
+ end
143
+
144
+ private
145
+
146
+ # Apply default settings to a component dir. This is run every time the dirs are
147
+ # accessed to ensure defaults are applied regardless of when new component dirs
148
+ # are added. This method must be idempotent.
149
+ #
150
+ # @return [void]
151
+ def apply_defaults_to_dir(dir)
152
+ dir.config.values.each do |key, _value|
153
+ if configured?(key) && !dir.configured?(key)
154
+ dir.public_send(:"#{key}=", public_send(key))
155
+ end
156
+ end
157
+ end
158
+
159
+ # Returns true if a setting has been explicitly configured and is not returning
160
+ # just a default value.
161
+ #
162
+ # This is used to determine which settings should be applied to added component
163
+ # dirs as additional defaults.
164
+ #
165
+ # @api private
166
+ def configured?(key)
167
+ config._settings[key].input_defined?
168
+ end
169
+
170
+ def method_missing(name, *args, &block)
171
+ if config.respond_to?(name)
172
+ config.public_send(name, *args, &block)
173
+ else
174
+ super
175
+ end
176
+ end
177
+
178
+ def respond_to_missing?(name, include_all = false)
179
+ config.respond_to?(name) || super
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end