dry-system 0.18.2 → 0.19.0

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