plumbum 0.1.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,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools'
4
+
5
+ require 'plumbum/consumers'
6
+
7
+ module Plumbum::Consumers
8
+ # Class methods for defining the Consumer interface.
9
+ module ClassMethods # rubocop:disable Metrics/ModuleLength
10
+ PROVIDER_METHODS = %i[get has?].freeze
11
+ private_constant :PROVIDER_METHODS
12
+
13
+ UNDEFINED = SleepingKingStudios::Tools::UNDEFINED
14
+ private_constant :UNDEFINED
15
+
16
+ class << self # rubocop:disable Metrics/ClassLength
17
+ INVALID_OPTIONS_FOR_METHOD_DEPENDENCY = {
18
+ memoize: true,
19
+ predicate: false
20
+ }.freeze
21
+ private_constant :INVALID_OPTIONS_FOR_METHOD_DEPENDENCY
22
+
23
+ # @private
24
+ def define_delegated_method( # rubocop:disable Metrics/MethodLength
25
+ receiver,
26
+ key:,
27
+ method_name:,
28
+ path:,
29
+ **options
30
+ )
31
+ validate_delegated_method_options(path:, **options)
32
+
33
+ *path, inner_name = path
34
+ method_name = method_name[1..] if method_name.start_with?('#')
35
+ inner_name = inner_name[1..]
36
+
37
+ dependency_methods_for(receiver)
38
+ .define_method(method_name) do |*args, **keywords, &block|
39
+ inner = get_scoped_plumbum_dependency(key, path:)
40
+
41
+ inner.public_send(inner_name, *args, **keywords, &block)
42
+ end # rubocop:disable Style/MultilineBlockChain
43
+ .tap do |method_name|
44
+ receiver.send(:private, method_name) if options[:private]
45
+ end
46
+ end
47
+
48
+ # @private
49
+ def define_memoized_reader( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
50
+ receiver,
51
+ default:,
52
+ key:,
53
+ method_name:,
54
+ optional:,
55
+ path:,
56
+ **options
57
+ )
58
+ dependency_methods_for(receiver)
59
+ .define_method(method_name) do
60
+ if (@plumbum_dependencies ||= {}).key?(key)
61
+ return @plumbum_dependencies[key]
62
+ end
63
+
64
+ get_scoped_plumbum_dependency(key, default:, optional:, path:)
65
+ .tap do |value|
66
+ @plumbum_dependencies[key] = value unless value.nil?
67
+ end
68
+ end # rubocop:disable Style/MultilineBlockChain
69
+ .tap do |method_name|
70
+ receiver.send(:private, method_name) if options[:private]
71
+ end
72
+ end
73
+
74
+ # @private
75
+ def define_methods(receiver, key:, method_name:, memoize:, predicate:, **) # rubocop:disable Metrics/ParameterLists
76
+ define_predicate(receiver, key:, method_name:, **) if predicate
77
+
78
+ if memoize
79
+ define_memoized_reader(receiver, key:, method_name:, **)
80
+ else
81
+ define_reader(receiver, key:, method_name:, **)
82
+ end
83
+
84
+ method_name.to_sym
85
+ end
86
+
87
+ # @private
88
+ def define_predicate(receiver, key:, method_name:, **options)
89
+ method_name = :"#{method_name}?"
90
+
91
+ dependency_methods_for(receiver)
92
+ .define_method(method_name) do
93
+ has_plumbum_dependency?(key)
94
+ end # rubocop:disable Style/MultilineBlockChain
95
+ .tap do |method_name|
96
+ receiver.send(:private, method_name) if options[:private]
97
+ end
98
+ end
99
+
100
+ # @private
101
+ def define_reader( # rubocop:disable Metrics/ParameterLists
102
+ receiver,
103
+ default:,
104
+ key:,
105
+ method_name:,
106
+ optional:,
107
+ path:,
108
+ **options
109
+ )
110
+ dependency_methods_for(receiver)
111
+ .define_method(method_name) do
112
+ get_scoped_plumbum_dependency(key, default:, optional:, path:)
113
+ end # rubocop:disable Style/MultilineBlockChain
114
+ .tap do |method_name|
115
+ receiver.send(:private, method_name) if options[:private]
116
+ end
117
+ end
118
+
119
+ # @private
120
+ def dependency_methods_for(receiver)
121
+ if receiver.const_defined?(:PlumbumDependencyMethods, false)
122
+ return receiver.const_get(:PlumbumDependencyMethods)
123
+ end
124
+
125
+ Module
126
+ .new
127
+ .tap { |mod| receiver.include mod }
128
+ .then { |mod| receiver.const_set(:PlumbumDependencyMethods, mod) }
129
+ end
130
+
131
+ # @private
132
+ def split_key(key, as:, scope:)
133
+ ClassMethods.validate_name(key, as: :key)
134
+ ClassMethods.validate_name(scope, as: :scope) if scope
135
+
136
+ key = "#{scope}.#{key}" if scope
137
+
138
+ segments = key.to_s.split('.')
139
+
140
+ return [key, as || key, nil] if segments.size == 1
141
+
142
+ [segments.first, as || segments.last, segments[1..]]
143
+ end
144
+
145
+ # @private
146
+ def validate_name(value, as: nil)
147
+ SleepingKingStudios::Tools::Toolbelt
148
+ .instance
149
+ .assertions
150
+ .validate_name(value, as:)
151
+ end
152
+
153
+ private
154
+
155
+ def validate_delegated_method_options(path: nil, **options) # rubocop:disable Metrics/MethodLength
156
+ if path.nil?
157
+ message =
158
+ 'delegated methods must have a scope - use a scoped key or pass ' \
159
+ 'the :scope option to #dependency'
160
+
161
+ raise ArgumentError, message
162
+ end
163
+
164
+ INVALID_OPTIONS_FOR_METHOD_DEPENDENCY.each \
165
+ do |option_name, default_value|
166
+ next if options[option_name] == default_value
167
+
168
+ raise ArgumentError,
169
+ "invalid option #{option_name.inspect} for method dependency"
170
+ end
171
+ end
172
+ end
173
+
174
+ # @overload plumbum_dependency(*keys, as: nil, memoize: true, optional: false, predicate: false, scope: nil)
175
+ # Defines injected dependencies for instances of the class.
176
+ #
177
+ # @param keys [Array<String, Symbol>] the keys for the dependency. A new
178
+ # dependency will be defined for each key using the same options.
179
+ # @param as [String, Symbol] the method name used to define dependency
180
+ # methods. Defaults to the key. Cannot be used with multiple keys.
181
+ # @param default [Object, Proc] if given, the default value will be
182
+ # returned when the consumer does not have a provider for the
183
+ # dependency. If the default value is a Proc, the default will be lazily
184
+ # evaluated in the context of the consumer. A default value *will not*
185
+ # be returned if a matching provider is defined but does not support the
186
+ # given scope.
187
+ # @param memoize [true, false] if true, memoizes the value of the
188
+ # dependency the first time it is successfully called. Defaults to true.
189
+ # @param optional [true, false] if true, calling the dependency returns
190
+ # nil if the dependency is not defined. Defaults to false.
191
+ # @param predicate [true, false] if true, also defines a predicate method
192
+ # that returns true if the dependency has a defined value. Defaults to
193
+ # false.
194
+ # @param private [true, false] if true, the generated methods will be
195
+ # generated with private visibility. Defaults to false.
196
+ # @param scope [String, Symbol] if given, combined with the key or keys to
197
+ # determine the dependency name and the path from the dependency to the
198
+ # returned value.
199
+ #
200
+ # @return [Symbol, Array<Symbol>] the name of the generated method, or the
201
+ # method names if given more than one key.
202
+ #
203
+ # @raise [ArgumentError] if any key is not a String or Symbol, or is
204
+ # empty.
205
+ def plumbum_dependency( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
206
+ *keys,
207
+ as: nil,
208
+ default: UNDEFINED,
209
+ memoize: true,
210
+ optional: false,
211
+ predicate: false,
212
+ private: false,
213
+ scope: nil
214
+ )
215
+ if keys.size > 1 && as
216
+ raise ArgumentError, 'invalid option :as when providing multiple keys'
217
+ end
218
+
219
+ scoped_keys = keys.map do |key|
220
+ define_plumbum_dependency(
221
+ key,
222
+ as:,
223
+ default:,
224
+ memoize:,
225
+ optional:,
226
+ predicate:,
227
+ private:,
228
+ scope:
229
+ )
230
+ end
231
+
232
+ scoped_keys.size == 1 ? scoped_keys.first : scoped_keys
233
+ end
234
+
235
+ # @param cache [true, false] if false,.clears the memoized value and
236
+ # recalculates the keys.
237
+ #
238
+ # @return [Set<String>] the keys of the dependencies declared by the class
239
+ # and its ancestors.
240
+ def plumbum_dependency_keys(cache: true)
241
+ @plumbum_dependency_keys = nil if cache == false
242
+
243
+ return @plumbum_dependency_keys if @plumbum_dependency_keys
244
+
245
+ @plumbum_dependency_keys = ancestors.reduce(Set.new) do |set, ancestor|
246
+ next set unless ancestor.respond_to?(:own_plumbum_dependency_keys, true)
247
+
248
+ set.union(ancestor.own_plumbum_dependency_keys)
249
+ end
250
+ end
251
+
252
+ # Registers a provider for the class.
253
+ #
254
+ # @param provider [#get, #has?] the provider to register.
255
+ #
256
+ # @return void
257
+ def plumbum_provider(provider)
258
+ PROVIDER_METHODS.each do |method_name|
259
+ next if provider.respond_to?(method_name)
260
+
261
+ # @todo [0.2] use tools error message for assertions.respond_to
262
+ message = "provider does not respond to ##{method_name}"
263
+
264
+ raise ArgumentError, message
265
+ end
266
+
267
+ own_plumbum_providers.prepend(provider)
268
+
269
+ nil
270
+ end
271
+
272
+ # @param cache [true, false] if false,.clears the memoized value and
273
+ # reaggregates the providers.
274
+ #
275
+ # @return [Array<Plumbum::Provider>] the providers defined for the class.
276
+ def plumbum_providers(cache: true)
277
+ @plumbum_providers = nil if cache == false
278
+
279
+ @plumbum_providers ||= each_plumbum_provider.to_a
280
+ end
281
+
282
+ protected
283
+
284
+ def own_plumbum_dependency_keys
285
+ @own_plumbum_dependency_keys ||= Set.new
286
+ end
287
+
288
+ def own_plumbum_providers
289
+ @own_plumbum_providers ||= []
290
+ end
291
+
292
+ private
293
+
294
+ def define_plumbum_dependency(key, as: nil, scope: nil, **)
295
+ ClassMethods.validate_name(as, as: :as) unless as.nil?
296
+
297
+ key, method_name, path = ClassMethods.split_key(key, as:, scope:)
298
+
299
+ own_plumbum_dependency_keys << key.to_s
300
+
301
+ if method_name.start_with?('#') || path&.last&.start_with?('#')
302
+ ClassMethods
303
+ .define_delegated_method(self, key:, method_name:, path:, **)
304
+ else
305
+ ClassMethods.define_methods(self, key:, method_name:, path:, **)
306
+ end
307
+ end
308
+
309
+ def each_plumbum_provider(&)
310
+ return enum_for(:each_plumbum_provider) unless block_given?
311
+
312
+ ancestors.reverse_each do |ancestor|
313
+ next unless ancestor.respond_to?(:own_plumbum_providers, true)
314
+
315
+ ancestor.own_plumbum_providers.each(&)
316
+ end
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools'
4
+
5
+ require 'plumbum/consumers'
6
+
7
+ module Plumbum::Consumers
8
+ # Instance methods for defining the Consumer interface.
9
+ module InstanceMethods
10
+ UNDEFINED = SleepingKingStudios::Tools::UNDEFINED
11
+ private_constant :UNDEFINED
12
+
13
+ # Retrieves the dependency with the specified key.
14
+ #
15
+ # @param key [String, Symbol] the key for the requested dependency.
16
+ # @param optional [true, false] if true, returns nil if the dependency is
17
+ # not defined. Defaults to false.
18
+ #
19
+ # @return [Object] the dependency value.
20
+ #
21
+ # @raise [ArgumentError] if the key is not a String or Symbol, or is empty.
22
+ # @raise [Plumbum::Errors::MissingDependencyError] if no matching dependency
23
+ # is found.
24
+ def get_plumbum_dependency(key, optional: false)
25
+ SleepingKingStudios::Tools::Toolbelt
26
+ .instance
27
+ .assertions
28
+ .validate_name(key, as: :key)
29
+
30
+ find_plumbum_dependency(key) do
31
+ handle_missing_plumbum_dependency(key, optional:)
32
+ end
33
+ end
34
+
35
+ # Checks if the dependency with the given key is defined.
36
+ #
37
+ # @param key [String, Symbol] the key for the requested dependency.
38
+ #
39
+ # @return [true, false] true if the dependency is defined, otherwise false.
40
+ #
41
+ # @raise [ArgumentError] if the key is not a String or Symbol, or is empty.
42
+ def has_plumbum_dependency?(key) # rubocop:disable Naming/PredicatePrefix
43
+ SleepingKingStudios::Tools::Toolbelt
44
+ .instance
45
+ .assertions
46
+ .validate_name(key, as: :key)
47
+
48
+ find_plumbum_dependency(key) { return false }
49
+
50
+ true
51
+ end
52
+
53
+ private
54
+
55
+ def find_plumbum_dependency(key)
56
+ plumbum_providers.each do |provider|
57
+ return provider.get(key) if provider.has?(key)
58
+ end
59
+
60
+ yield
61
+ end
62
+
63
+ def get_scoped_plumbum_dependency( # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
64
+ key,
65
+ path:,
66
+ default: UNDEFINED,
67
+ optional: false
68
+ )
69
+ optional ||= default != UNDEFINED
70
+ dependency = get_plumbum_dependency(key, optional:)
71
+
72
+ if dependency.nil?
73
+ return if default == UNDEFINED
74
+
75
+ return default unless default.is_a?(Proc)
76
+
77
+ return instance_exec(&default)
78
+ end
79
+
80
+ return dependency if path.nil? || path.empty?
81
+
82
+ path.reduce(dependency) do |memo, method_name|
83
+ SleepingKingStudios::Tools::Toolbelt
84
+ .instance
85
+ .object_tools
86
+ .fetch(memo, method_name, indifferent_key: true)
87
+ end
88
+ rescue KeyError, IndexError, NoMethodError => exception # rubocop:disable Lint/ShadowedException
89
+ raise Plumbum::Errors::MissingDependencyError,
90
+ exception.message,
91
+ cause: exception
92
+ end
93
+
94
+ def handle_missing_plumbum_dependency(key, optional: false)
95
+ return nil if optional
96
+
97
+ raise Plumbum::Errors::MissingDependencyError,
98
+ "dependency not found with key #{key.inspect}"
99
+ end
100
+
101
+ def plumbum_providers
102
+ self.class.plumbum_providers
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools'
4
+
5
+ require 'plumbum/consumers'
6
+ require 'plumbum/consumers/class_methods'
7
+ require 'plumbum/consumers/instance_methods'
8
+
9
+ module Plumbum::Consumers
10
+ # Consumer implementation with fully-scoped method names for compatibility.
11
+ #
12
+ # Use a scoped consumer when the standard Consumer DSL class methods (such as
13
+ # .dependency and .provider) might conflict with existing methods.
14
+ #
15
+ # @see Plumbum::Consumer
16
+ module ScopedConsumer
17
+ extend SleepingKingStudios::Tools::Toolbox::Mixin
18
+ include Plumbum::Consumers::InstanceMethods
19
+
20
+ # Class methods to extend when including Plumbum::Consumer.
21
+ module ClassMethods
22
+ include Plumbum::Consumers::ClassMethods
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumbum'
4
+
5
+ module Plumbum
6
+ # Functionality for defining consumers.
7
+ module Consumers
8
+ autoload :ClassMethods, 'plumbum/consumers/class_methods'
9
+ autoload :InstanceMethods, 'plumbum/consumers/instance_methods'
10
+ autoload :ScopedConsumer, 'plumbum/consumers/scoped_consumer'
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumbum/errors'
4
+
5
+ module Plumbum::Errors
6
+ # Exception raised when attempting to change an immutable value.
7
+ class ImmutableError < StandardError; end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumbum/errors'
4
+
5
+ module Plumbum::Errors
6
+ # Exception raised when a dependency exists but cannot be resolved.
7
+ class InvalidDependencyError < StandardError; end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumbum/errors'
4
+
5
+ module Plumbum::Errors
6
+ # Exception raised when attempting to set an invalid key for a provider.
7
+ class InvalidKeyError < StandardError; end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumbum/errors'
4
+
5
+ module Plumbum::Errors
6
+ # Exception raised when attempting to retrieve a missing dependency.
7
+ class MissingDependencyError < StandardError; end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumbum'
4
+
5
+ module Plumbum
6
+ # Namespace for exceptions raised when handling Plumbum errors.
7
+ module Errors
8
+ autoload :ImmutableError, 'plumbum/errors/immutable_error'
9
+ autoload :InvalidDependencyError, 'plumbum/errors/invalid_dependency_error'
10
+ autoload :InvalidKeyError, 'plumbum/errors/invalid_key_error'
11
+ autoload :MissingDependencyError, 'plumbum/errors/missing_dependency_error'
12
+ end
13
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumbum'
4
+ require 'plumbum/providers/plural'
5
+
6
+ module Plumbum
7
+ # Provider that provides a mapping of keys to values.
8
+ class ManyProvider
9
+ include Plumbum::Providers::Plural
10
+
11
+ # @param values [Hash{String, Symbol => Object}] the provided values.
12
+ # @param options [Hash] additional options for the provider.
13
+ def initialize(values: Plumbum::UNDEFINED, **options)
14
+ super()
15
+
16
+ @values = normalize_values(values)
17
+ @options = validate_options(options)
18
+ end
19
+
20
+ # (see Plumbum::Providers::Plural#values)
21
+ def values
22
+ @values == Plumbum::UNDEFINED ? {} : super.dup
23
+ end
24
+
25
+ # @param values [Hash{String, Symbol => Object}] the updated values.
26
+ def values=(values)
27
+ validate_values(values)
28
+
29
+ values = values.transform_keys(&:to_s)
30
+
31
+ changed_values = find_changed_values(values)
32
+
33
+ changed_values.each_key { |key| require_mutable(key) }
34
+
35
+ @values = self.values.merge(changed_values)
36
+ end
37
+
38
+ private
39
+
40
+ def find_changed_values(updated_values)
41
+ missing_values = values.dup
42
+ changed_values = updated_values.each.with_object({}) \
43
+ do |(key, value), hsh|
44
+ missing_values.delete(key)
45
+
46
+ next if value == values[key]
47
+
48
+ hsh[key] = value
49
+ end
50
+
51
+ missing_values.each_key { |key| changed_values[key] = Plumbum::UNDEFINED }
52
+
53
+ changed_values
54
+ end
55
+
56
+ def get_value(key)
57
+ value = super
58
+
59
+ value == Plumbum::UNDEFINED ? nil : value
60
+ end
61
+
62
+ def has_value?(key, allow_undefined: false) # rubocop:disable Naming/PredicatePrefix
63
+ super && (allow_undefined || @values[key] != Plumbum::UNDEFINED)
64
+ end
65
+
66
+ def normalize_values(values)
67
+ return values if values == Plumbum::UNDEFINED
68
+
69
+ if values.is_a?(Array)
70
+ values = values.to_h { |key| [key, Plumbum::UNDEFINED] }
71
+ end
72
+
73
+ validate_values(values)
74
+
75
+ @values = values.transform_keys(&:to_s)
76
+ end
77
+
78
+ def validate_values(values)
79
+ tools.assertions.validate_instance_of(values, as: :values, expected: Hash)
80
+
81
+ values.each_key.with_index do |key, index|
82
+ tools.assertions.validate_name(key, as: :"values.keys[#{index}]")
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumbum'
4
+ require 'plumbum/providers/singular'
5
+
6
+ module Plumbum
7
+ # Provider that provides a single value for a specified key.
8
+ class OneProvider
9
+ include Plumbum::Providers::Singular
10
+
11
+ # @param key [String, Symbol] the key used to identify the provided value.
12
+ # @param value [Object] the provided value, if any.
13
+ # @param options [Hash] additional options for the provider.
14
+ def initialize(key, value: Plumbum::UNDEFINED, **options)
15
+ super()
16
+
17
+ tools
18
+ .assertions
19
+ .validate_name(key, as: :key)
20
+
21
+ @key = key.to_s
22
+ @value = value
23
+ @options = validate_options(options)
24
+ end
25
+
26
+ # @return [String, Symbol] the key used to identify the provided value.
27
+ attr_reader :key
28
+
29
+ # @return [Object, nil] the provided value, or nil if the value is not
30
+ # defined.
31
+ def value
32
+ @value == Plumbum::UNDEFINED ? nil : @value
33
+ end
34
+
35
+ # @param value [Object] the changed value.
36
+ def value=(value)
37
+ require_mutable(key)
38
+
39
+ set_value(key, value)
40
+ end
41
+
42
+ private
43
+
44
+ def get_value(key)
45
+ return nil if @value == Plumbum::UNDEFINED
46
+
47
+ super
48
+ end
49
+
50
+ def has_value?(key, allow_undefined: false) # rubocop:disable Naming/PredicatePrefix
51
+ return false if !allow_undefined && @value == Plumbum::UNDEFINED
52
+
53
+ super
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumbum'
4
+ require 'plumbum/many_provider'
5
+
6
+ module Plumbum
7
+ # Utility module that converts constructor parameters to a Provider.
8
+ module Parameters
9
+ # @overload initialize(*arguments, **keywords, &block)
10
+ # @param arguments [Array] the arguments passed to the constructor.
11
+ # @param keywords [Hash] the keywords passed to the constructor, including
12
+ # any injected dependencies.
13
+ # @param block [Proc] the block passed to the constructor.
14
+ def initialize(*, **keywords, &)
15
+ values, keywords = extract_plumbum_dependencies(keywords)
16
+
17
+ @plumbum_parameters_provider = Plumbum::ManyProvider.new(values:)
18
+
19
+ super
20
+ end
21
+
22
+ private
23
+
24
+ def extract_plumbum_dependencies(keywords)
25
+ dependency_keys = self.class.dependency_keys
26
+
27
+ keywords
28
+ .partition { |key, _| dependency_keys.include?(key.to_s) }
29
+ .map(&:to_h)
30
+ end
31
+
32
+ def plumbum_providers
33
+ [
34
+ @plumbum_parameters_provider,
35
+ *super
36
+ ]
37
+ end
38
+ end
39
+ end