anyway_config 2.5.3 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module AutoCast
5
+ # Regexp to detect array values
6
+ # Array value is a values that contains at least one comma
7
+ # and doesn't start/end with quote or curly braces
8
+ ARRAY_RXP = /\A[^'"{].*\s*,\s*.*[^'"}]\z/
9
+
10
+ class << self
11
+ def call(val)
12
+ return val unless val.is_a?(::Hash) || val.is_a?(::String)
13
+
14
+ case val
15
+ when Hash
16
+ val.transform_values { it = _1;call(it) }
17
+ when ARRAY_RXP
18
+ val.split(/\s*,\s*/).map { it = _1;call(it) }
19
+ when /\A(true|t|yes|y)\z/i
20
+ true
21
+ when /\A(false|f|no|n)\z/i
22
+ false
23
+ when /\A(nil|null)\z/i
24
+ nil
25
+ when /\A\d+\z/
26
+ val.to_i
27
+ when /\A\d*\.\d+\z/
28
+ val.to_f
29
+ when /\A['"].*['"]\z/
30
+ val.gsub(/(\A['"]|['"]\z)/, "")
31
+ else
32
+ val
33
+ end
34
+ end
35
+
36
+ def cast_hash(obj)
37
+ obj.transform_values do |val|
38
+ val.is_a?(::Hash) ? cast_hash(val) : call(val)
39
+ end
40
+ end
41
+
42
+ def coerce(_key, val)
43
+ call(val)
44
+ end
45
+ end
46
+ end
47
+
48
+ module NoCast
49
+ def self.call(val) = val
50
+
51
+ def self.coerce(_key, val) = val
52
+ end
53
+ end
@@ -0,0 +1,473 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway/optparse_config"
4
+ require "anyway/dynamic_config"
5
+
6
+ module Anyway # :nodoc:
7
+ using RubyNext
8
+ using Anyway::Ext::DeepDup
9
+ using Anyway::Ext::DeepFreeze
10
+ using Anyway::Ext::Hash
11
+ using Anyway::Ext::FlattenNames
12
+
13
+ using(Module.new do
14
+ refine Object do
15
+ def vm_object_id() = (object_id << 1).to_s(16)
16
+ end
17
+ end)
18
+
19
+ # Base config class
20
+ # Provides `attr_config` method to describe
21
+ # configuration parameters and set defaults
22
+ class Config
23
+ PARAM_NAME = /^[a-z_](\w+)?$/
24
+
25
+ # List of names that couldn't be used as config names
26
+ # (the class instance methods we use)
27
+ RESERVED_NAMES = %i[
28
+ config_name
29
+ env_prefix
30
+ as_env
31
+ values
32
+ class
33
+ clear
34
+ deconstruct_keys
35
+ dig
36
+ dup
37
+ initialize
38
+ load
39
+ load_from_sources
40
+ option_parser
41
+ pretty_print
42
+ raise_validation_error
43
+ reload
44
+ resolve_config_path
45
+ tap
46
+ to_h
47
+ to_source_trace
48
+ write_config_attr
49
+ __type_caster__
50
+ ].freeze
51
+
52
+ class Error < StandardError; end
53
+
54
+ class ValidationError < Error; end
55
+
56
+ include OptparseConfig
57
+ include DynamicConfig
58
+
59
+ class BlockCallback
60
+ attr_reader :block
61
+
62
+ def initialize(block)
63
+ @block = block
64
+ end
65
+
66
+ def apply_to(config)
67
+ config.instance_exec(&block)
68
+ end
69
+ end
70
+
71
+ class NamedCallback
72
+ attr_reader :name
73
+
74
+ def initialize(name)
75
+ @name = name
76
+ end
77
+
78
+ def apply_to(config) = config.send(name)
79
+ end
80
+
81
+ class << self
82
+ def attr_config(*args, **hargs)
83
+ new_defaults = hargs.deep_dup
84
+ new_defaults.stringify_keys!
85
+
86
+ defaults.merge! new_defaults
87
+
88
+ new_keys = ((args + new_defaults.keys) - config_attributes)
89
+
90
+ validate_param_names! new_keys.map(&:to_s)
91
+
92
+ new_keys.map!(&:to_sym)
93
+
94
+ unless (reserved_names = (new_keys & RESERVED_NAMES)).empty?
95
+ raise ArgumentError, "Can not use the following reserved names as config attrubutes: " \
96
+ "#{reserved_names.sort.map(&:to_s).join(", ")}"
97
+ end
98
+
99
+ config_attributes.push(*new_keys)
100
+
101
+ define_config_accessor(*new_keys)
102
+
103
+ # Define predicate methods ("param?") for attributes
104
+ # having `true` or `false` as default values
105
+ new_defaults.each do |key, val|
106
+ next unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
107
+ alias_method :"#{key}?", :"#{key}"
108
+ end
109
+ end
110
+
111
+ def defaults
112
+ return @defaults if instance_variable_defined?(:@defaults)
113
+
114
+ @defaults = if superclass < Anyway::Config
115
+ superclass.defaults.deep_dup
116
+ else
117
+ new_empty_config
118
+ end
119
+ end
120
+
121
+ def config_attributes
122
+ return @config_attributes if instance_variable_defined?(:@config_attributes)
123
+
124
+ @config_attributes = if superclass < Anyway::Config
125
+ superclass.config_attributes.dup
126
+ else
127
+ []
128
+ end
129
+ end
130
+
131
+ def required(*names, env: nil, **nested)
132
+ unknown_names = names + nested.keys - config_attributes
133
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
134
+
135
+ return unless Settings.matching_env?(env)
136
+
137
+ required_attributes.push(*names)
138
+ required_attributes.push(*nested.flatten_names)
139
+ end
140
+
141
+ def required_attributes
142
+ return @required_attributes if instance_variable_defined?(:@required_attributes)
143
+
144
+ @required_attributes = if superclass < Anyway::Config
145
+ superclass.required_attributes.dup
146
+ else
147
+ []
148
+ end
149
+ end
150
+
151
+ def on_load(*names, &block)
152
+ raise ArgumentError, "Either methods or block should be specified, not both" if block && !names.empty?
153
+
154
+ if block
155
+ load_callbacks << BlockCallback.new(block)
156
+ else
157
+ load_callbacks.push(*names.map { it = _1;NamedCallback.new(it) })
158
+ end
159
+ end
160
+
161
+ def load_callbacks
162
+ return @load_callbacks if instance_variable_defined?(:@load_callbacks)
163
+
164
+ @load_callbacks = if superclass <= Anyway::Config
165
+ superclass.load_callbacks.dup
166
+ else
167
+ []
168
+ end
169
+ end
170
+
171
+ def config_name(val = nil)
172
+ return (@explicit_config_name = val.to_s) unless val.nil?
173
+
174
+ return @config_name if instance_variable_defined?(:@config_name)
175
+
176
+ @config_name = explicit_config_name || build_config_name
177
+ end
178
+
179
+ def explicit_config_name
180
+ return @explicit_config_name if instance_variable_defined?(:@explicit_config_name)
181
+
182
+ @explicit_config_name =
183
+ if superclass.respond_to?(:explicit_config_name)
184
+ superclass.explicit_config_name
185
+ end
186
+ end
187
+
188
+ def explicit_config_name?() = !explicit_config_name.nil?
189
+
190
+ def env_prefix(val = nil)
191
+ return (@env_prefix = val.to_s.upcase) unless val.nil?
192
+
193
+ return @env_prefix if instance_variable_defined?(:@env_prefix)
194
+
195
+ @env_prefix = if superclass < Anyway::Config && superclass.explicit_config_name?
196
+ superclass.env_prefix
197
+ else
198
+ config_name.upcase
199
+ end
200
+ end
201
+
202
+ def loader_options(val = nil)
203
+ return (@loader_options = val) unless val.nil?
204
+
205
+ return @loader_options if instance_variable_defined?(:@loader_options)
206
+
207
+ @loader_options = if superclass < Anyway::Config
208
+ superclass.loader_options
209
+ else
210
+ {}
211
+ end
212
+ end
213
+
214
+ def new_empty_config() = {}
215
+
216
+ def coerce_types(mapping)
217
+ Utils.deep_merge!(coercion_mapping, mapping)
218
+
219
+ mapping.each do |key, val|
220
+ type = val.is_a?(::Hash) ? val[:type] : val
221
+ next if type != :boolean
222
+
223
+ alias_method :"#{key}?", :"#{key}"
224
+ end
225
+ end
226
+
227
+ def coercion_mapping
228
+ return @coercion_mapping if instance_variable_defined?(:@coercion_mapping)
229
+
230
+ @coercion_mapping = if superclass < Anyway::Config
231
+ superclass.coercion_mapping.deep_dup
232
+ else
233
+ {}
234
+ end
235
+ end
236
+
237
+ def type_caster(val = nil)
238
+ return @type_caster unless val.nil?
239
+
240
+ @type_caster ||=
241
+ if coercion_mapping.empty?
242
+ fallback_type_caster
243
+ else
244
+ ::Anyway::TypeCaster.new(coercion_mapping, fallback: fallback_type_caster)
245
+ end
246
+ end
247
+
248
+ def fallback_type_caster(val = nil)
249
+ return (@fallback_type_caster = val) unless val.nil?
250
+
251
+ return @fallback_type_caster if instance_variable_defined?(:@fallback_type_caster)
252
+
253
+ @fallback_type_caster = if superclass < Anyway::Config
254
+ superclass.fallback_type_caster.deep_dup
255
+ else
256
+ ::Anyway::AutoCast
257
+ end
258
+ end
259
+
260
+ def disable_auto_cast!
261
+ @fallback_type_caster = ::Anyway::NoCast
262
+ end
263
+
264
+ private
265
+
266
+ def define_config_accessor(*names)
267
+ names.each do |name|
268
+ accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
269
+ def #{name}=(val)
270
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
271
+ values[:#{name}] = val
272
+ end
273
+
274
+ def #{name}
275
+ values[:#{name}]
276
+ end
277
+ RUBY
278
+ end
279
+ end
280
+
281
+ def accessors_module
282
+ return @accessors_module if instance_variable_defined?(:@accessors_module)
283
+
284
+ @accessors_module = Module.new.tap do |mod|
285
+ include mod
286
+ end
287
+ end
288
+
289
+ def build_config_name
290
+ unless name
291
+ raise "Please, specify config name explicitly for anonymous class " \
292
+ "via `config_name :my_config`"
293
+ end
294
+
295
+ # handle two cases:
296
+ # - SomeModule::Config => "some_module"
297
+ # - SomeConfig => "some"
298
+ unless name =~ /^(\w+)(::)?Config$/
299
+ raise "Couldn't infer config name, please, specify it explicitly " \
300
+ "via `config_name :my_config`"
301
+ end
302
+
303
+ # TODO(v3.0): Replace downcase with underscore
304
+ Regexp.last_match[1].tap(&:downcase!)
305
+ end
306
+
307
+ def validate_param_names!(names)
308
+ invalid_names = names.reject { |name| name =~ PARAM_NAME }
309
+ return if invalid_names.empty?
310
+
311
+ raise ArgumentError, "Invalid attr_config name: #{invalid_names.join(", ")}.\n" \
312
+ "Valid names must satisfy /#{PARAM_NAME.source}/."
313
+ end
314
+ end
315
+
316
+ on_load :validate_required_attributes!
317
+
318
+ attr_reader :config_name, :env_prefix
319
+
320
+ # Instantiate config instance.
321
+ #
322
+ # Example:
323
+ #
324
+ # my_config = Anyway::Config.new()
325
+ #
326
+ # # provide some values explicitly
327
+ # my_config = Anyway::Config.new({some: :value})
328
+ #
329
+ def initialize(overrides = nil)
330
+ @config_name = self.class.config_name
331
+
332
+ raise ArgumentError, "Config name is missing" unless @config_name
333
+
334
+ @env_prefix = self.class.env_prefix
335
+ @values = {}
336
+
337
+ load(overrides)
338
+ end
339
+
340
+ def reload(overrides = nil)
341
+ clear
342
+ load(overrides)
343
+ self
344
+ end
345
+
346
+ def clear
347
+ values.clear
348
+ @__trace__ = nil
349
+ self
350
+ end
351
+
352
+ def load(overrides = nil)
353
+ base_config = self.class.defaults.deep_dup
354
+
355
+ trace = Tracing.capture do
356
+ Tracing.trace!(:defaults) { base_config }
357
+
358
+ config_path = resolve_config_path(config_name, env_prefix)
359
+
360
+ load_from_sources(
361
+ base_config,
362
+ name: config_name,
363
+ env_prefix:,
364
+ config_path:,
365
+ **self.class.loader_options
366
+ )
367
+
368
+ if overrides
369
+ Tracing.trace!(:load) { overrides }
370
+
371
+ Utils.deep_merge!(base_config, overrides)
372
+ end
373
+ end
374
+
375
+ base_config.each do |key, val|
376
+ write_config_attr(key.to_sym, val)
377
+ end
378
+
379
+ # Trace may contain unknown attributes
380
+ trace&.keep_if { |key| self.class.config_attributes.include?(key.to_sym) }
381
+
382
+ # Run on_load callbacks
383
+ self.class.load_callbacks.each { it = _1;it.apply_to(self) }
384
+
385
+ # Set trace after we write all the values to
386
+ # avoid changing the source to accessor
387
+ @__trace__ = trace
388
+
389
+ self
390
+ end
391
+
392
+ def load_from_sources(base_config, **)
393
+ Anyway.loaders.each do |(_id, loader)|
394
+ Utils.deep_merge!(base_config, loader.call(**))
395
+ end
396
+ base_config
397
+ end
398
+
399
+ def dig(*) = values.dig(*)
400
+
401
+ def to_h() = values.deep_dup.deep_freeze
402
+
403
+ def dup
404
+ self.class.allocate.tap do |new_config|
405
+ %i[config_name env_prefix __trace__].each do |ivar|
406
+ new_config.instance_variable_set(:"@#{ivar}", send(ivar).dup)
407
+ end
408
+ new_config.instance_variable_set(:@values, values.deep_dup)
409
+ end
410
+ end
411
+
412
+ def resolve_config_path(name, env_prefix)
413
+ Anyway.env.fetch(env_prefix).delete("conf") || Settings.default_config_path.call(name)
414
+ end
415
+
416
+ def deconstruct_keys(keys) = values.deconstruct_keys(keys)
417
+
418
+ def to_source_trace() = __trace__&.to_h
419
+
420
+ def inspect
421
+ "#<#{self.class}:0x#{vm_object_id.rjust(16, "0")} config_name=\"#{config_name}\" env_prefix=\"#{env_prefix}\" " \
422
+ "values=#{values.inspect}>"
423
+ end
424
+
425
+ def pretty_print(q)
426
+ q.object_group self do
427
+ q.nest(1) do
428
+ q.breakable
429
+ q.text "config_name=#{config_name.inspect}"
430
+ q.breakable
431
+ q.text "env_prefix=#{env_prefix.inspect}"
432
+ q.breakable
433
+ q.text "values:"
434
+ q.pp __trace__
435
+ end
436
+ end
437
+ end
438
+
439
+ def as_env
440
+ Env.from_hash(to_h, prefix: env_prefix)
441
+ end
442
+
443
+ private
444
+
445
+ attr_reader :values, :__trace__
446
+
447
+ def validate_required_attributes!
448
+ self.class.required_attributes.select do |name|
449
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
450
+ val.nil? || (val.is_a?(String) && val.empty?)
451
+ end.then do |missing|
452
+ next if missing.empty?
453
+ raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
454
+ end
455
+ end
456
+
457
+ def write_config_attr(key, val)
458
+ key = key.to_sym
459
+ return unless self.class.config_attributes.include?(key)
460
+
461
+ val = __type_caster__.coerce(key, val)
462
+ public_send(:"#{key}=", val)
463
+ end
464
+
465
+ def raise_validation_error(msg)
466
+ raise ValidationError, msg
467
+ end
468
+
469
+ def __type_caster__
470
+ self.class.type_caster
471
+ end
472
+ end
473
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Provides method to trace values association
5
+ module Tracing
6
+ using Anyway::Ext::DeepDup
7
+
8
+ using(Module.new do
9
+ refine Thread::Backtrace::Location do
10
+ def path_lineno() = "#{path}:#{lineno}"
11
+ end
12
+ end)
13
+
14
+ class Trace
15
+ UNDEF = Object.new
16
+
17
+ attr_reader :type, :value, :source
18
+
19
+ def initialize(type = :trace, value = UNDEF, **source)
20
+ @type = type
21
+ @source = source
22
+ @value = (value == UNDEF) ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
23
+ end
24
+
25
+ def dig(...)
26
+ value.dig(...)
27
+ end
28
+
29
+ def record_value(val, *path, **)
30
+ key = path.pop
31
+ trace = if val.is_a?(Hash)
32
+ Trace.new.tap { it = _1;it.merge_values(val, **) }
33
+ else
34
+ Trace.new(:value, val, **)
35
+ end
36
+
37
+ target_trace = path.empty? ? self : value.dig(*path)
38
+ target_trace.record_key(key.to_s, trace)
39
+
40
+ val
41
+ end
42
+
43
+ def merge_values(hash, **)
44
+ return hash unless hash
45
+
46
+ hash.each do |key, val|
47
+ if val.is_a?(Hash)
48
+ value[key.to_s].merge_values(val, **)
49
+ else
50
+ value[key.to_s] = Trace.new(:value, val, **)
51
+ end
52
+ end
53
+
54
+ hash
55
+ end
56
+
57
+ def record_key(key, key_trace)
58
+ @value = Hash.new { |h, k| h[k] = Trace.new(:trace) } unless value.is_a?(::Hash)
59
+
60
+ value[key] = key_trace
61
+ end
62
+
63
+ def merge!(another_trace)
64
+ raise ArgumentError, "You can only merge into a :trace type, and this is :#{type}" unless trace?
65
+ raise ArgumentError, "You can only merge a :trace type, but trying :#{type}" unless another_trace.trace?
66
+
67
+ another_trace.value.each do |key, sub_trace|
68
+ if sub_trace.trace?
69
+ value[key].merge! sub_trace
70
+ else
71
+ value[key] = sub_trace
72
+ end
73
+ end
74
+ end
75
+
76
+ def keep_if(...)
77
+ raise ArgumentError, "You can only filter :trace type, and this is :#{type}" unless trace?
78
+ value.keep_if(...)
79
+ end
80
+
81
+ def clear() = value.clear
82
+
83
+ def trace?() = type == :trace
84
+
85
+ def to_h
86
+ if trace?
87
+ value.transform_values(&:to_h).tap { it = _1;it.default_proc = nil }
88
+ else
89
+ {value:, source:}
90
+ end
91
+ end
92
+
93
+ def dup() = self.class.new(type, value.dup, **source)
94
+
95
+ def pretty_print(q)
96
+ if trace?
97
+ q.nest(2) do
98
+ q.breakable ""
99
+ q.seplist(value, nil, :each) do |k, v|
100
+ q.group do
101
+ q.text k
102
+ q.text " =>"
103
+ if v.trace?
104
+ q.text " { "
105
+ q.pp v
106
+ q.breakable " "
107
+ q.text "}"
108
+ else
109
+ q.breakable " "
110
+ q.pp v
111
+ end
112
+ end
113
+ end
114
+ end
115
+ else
116
+ q.pp value
117
+ q.group(0, " (", ")") do
118
+ q.seplist(source, lambda { q.breakable " " }, :each) do |k, v|
119
+ q.group do
120
+ q.text k.to_s
121
+ q.text "="
122
+ q.text v.to_s
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ class << self
131
+ def capture
132
+ unless Settings.tracing_enabled
133
+ yield
134
+ return
135
+ end
136
+
137
+ trace = Trace.new
138
+ trace_stack.push trace
139
+ yield
140
+ trace_stack.last
141
+ ensure
142
+ trace_stack.pop
143
+ end
144
+
145
+ def trace_stack
146
+ (Thread.current[:__anyway__trace_stack__] ||= [])
147
+ end
148
+
149
+ def current_trace() = trace_stack.last
150
+
151
+ alias_method :tracing?, :current_trace
152
+
153
+ def source_stack
154
+ (Thread.current[:__anyway__trace_source_stack__] ||= [])
155
+ end
156
+
157
+ def current_trace_source
158
+ source_stack.last || accessor_source(caller_locations(2, 1).first)
159
+ end
160
+
161
+ def with_trace_source(src)
162
+ source_stack << src
163
+ yield
164
+ ensure
165
+ source_stack.pop
166
+ end
167
+
168
+ private
169
+
170
+ def accessor_source(location)
171
+ {type: :accessor, called_from: location.path_lineno}
172
+ end
173
+ end
174
+
175
+ module_function
176
+
177
+ def trace!(type, *path, **)
178
+ return yield unless Tracing.tracing?
179
+ val = yield
180
+ if val.is_a?(Hash)
181
+ Tracing.current_trace.merge_values(val, type:, **)
182
+ elsif !path.empty?
183
+ Tracing.current_trace.record_value(val, *path, type:, **)
184
+ end
185
+ val
186
+ end
187
+ end
188
+ end