anyway_config 2.0.5 → 2.2.1

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -181
  3. data/README.md +238 -13
  4. data/lib/.rbnext/1995.next/anyway/config.rb +438 -0
  5. data/lib/.rbnext/1995.next/anyway/dynamic_config.rb +31 -0
  6. data/lib/.rbnext/1995.next/anyway/env.rb +56 -0
  7. data/lib/.rbnext/1995.next/anyway/loaders/base.rb +21 -0
  8. data/lib/.rbnext/1995.next/anyway/tracing.rb +181 -0
  9. data/lib/.rbnext/2.7/anyway/auto_cast.rb +39 -19
  10. data/lib/.rbnext/2.7/anyway/config.rb +61 -16
  11. data/lib/.rbnext/2.7/anyway/rails/loaders/yaml.rb +30 -0
  12. data/lib/.rbnext/2.7/anyway/rbs.rb +92 -0
  13. data/lib/.rbnext/2.7/anyway/settings.rb +79 -0
  14. data/lib/.rbnext/2.7/anyway/tracing.rb +6 -6
  15. data/lib/.rbnext/2.7/anyway/type_casting.rb +143 -0
  16. data/lib/.rbnext/3.0/anyway/auto_cast.rb +53 -0
  17. data/lib/.rbnext/{2.8 → 3.0}/anyway/config.rb +61 -16
  18. data/lib/.rbnext/{2.8 → 3.0}/anyway/loaders/base.rb +0 -0
  19. data/lib/.rbnext/{2.8 → 3.0}/anyway/loaders.rb +0 -0
  20. data/lib/.rbnext/{2.8 → 3.0}/anyway/tracing.rb +6 -6
  21. data/lib/anyway/auto_cast.rb +39 -19
  22. data/lib/anyway/config.rb +75 -30
  23. data/lib/anyway/dynamic_config.rb +6 -2
  24. data/lib/anyway/env.rb +1 -1
  25. data/lib/anyway/ext/deep_dup.rb +12 -0
  26. data/lib/anyway/ext/hash.rb +10 -12
  27. data/lib/anyway/loaders/base.rb +1 -1
  28. data/lib/anyway/loaders/env.rb +3 -1
  29. data/lib/anyway/loaders/yaml.rb +9 -5
  30. data/lib/anyway/option_parser_builder.rb +1 -3
  31. data/lib/anyway/optparse_config.rb +5 -7
  32. data/lib/anyway/rails/loaders/credentials.rb +4 -4
  33. data/lib/anyway/rails/loaders/secrets.rb +6 -8
  34. data/lib/anyway/rails/loaders/yaml.rb +11 -0
  35. data/lib/anyway/rails/settings.rb +9 -2
  36. data/lib/anyway/rbs.rb +92 -0
  37. data/lib/anyway/settings.rb +52 -2
  38. data/lib/anyway/tracing.rb +9 -9
  39. data/lib/anyway/type_casting.rb +134 -0
  40. data/lib/anyway/utils/deep_merge.rb +21 -0
  41. data/lib/anyway/version.rb +1 -1
  42. data/lib/anyway_config.rb +4 -0
  43. data/sig/anyway_config.rbs +129 -0
  44. metadata +42 -15
  45. data/lib/.rbnext/2.7/anyway/option_parser_builder.rb +0 -31
@@ -0,0 +1,438 @@
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
+
12
+ using(Module.new do
13
+ refine Object do
14
+ def vm_object_id() = (object_id << 1).to_s(16)
15
+ end
16
+ end)
17
+
18
+ # Base config class
19
+ # Provides `attr_config` method to describe
20
+ # configuration parameters and set defaults
21
+ class Config
22
+ PARAM_NAME = /^[a-z_](\w+)?$/
23
+
24
+ # List of names that couldn't be used as config names
25
+ # (the class instance methods we use)
26
+ RESERVED_NAMES = %i[
27
+ config_name
28
+ env_prefix
29
+ values
30
+ class
31
+ clear
32
+ deconstruct_keys
33
+ dig
34
+ dup
35
+ initialize
36
+ load
37
+ load_from_sources
38
+ option_parser
39
+ pretty_print
40
+ raise_validation_error
41
+ reload
42
+ resolve_config_path
43
+ tap
44
+ to_h
45
+ to_source_trace
46
+ write_config_attr
47
+ __type_caster__
48
+ ].freeze
49
+
50
+ class Error < StandardError; end
51
+
52
+ class ValidationError < Error; end
53
+
54
+ include OptparseConfig
55
+ include DynamicConfig
56
+
57
+ class BlockCallback
58
+ attr_reader :block
59
+
60
+ def initialize(block)
61
+ @block = block
62
+ end
63
+
64
+ def apply_to(config)
65
+ config.instance_exec(&block)
66
+ end
67
+ end
68
+
69
+ class NamedCallback
70
+ attr_reader :name
71
+
72
+ def initialize(name)
73
+ @name = name
74
+ end
75
+
76
+ def apply_to(config) = config.send(name)
77
+ end
78
+
79
+ class << self
80
+ def attr_config(*args, **hargs)
81
+ new_defaults = hargs.deep_dup
82
+ new_defaults.stringify_keys!
83
+
84
+ defaults.merge! new_defaults
85
+
86
+ new_keys = ((args + new_defaults.keys) - config_attributes)
87
+
88
+ validate_param_names! new_keys.map(&:to_s)
89
+
90
+ new_keys.map!(&:to_sym)
91
+
92
+ unless (reserved_names = (new_keys & RESERVED_NAMES)).empty?
93
+ raise ArgumentError, "Can not use the following reserved names as config attrubutes: " \
94
+ "#{reserved_names.sort.map(&:to_s).join(", ")}"
95
+ end
96
+
97
+ config_attributes.push(*new_keys)
98
+
99
+ define_config_accessor(*new_keys)
100
+
101
+ # Define predicate methods ("param?") for attributes
102
+ # having `true` or `false` as default values
103
+ new_defaults.each do |key, val|
104
+ next unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
105
+ alias_method :"#{key}?", :"#{key}"
106
+ end
107
+ end
108
+
109
+ def defaults
110
+ return @defaults if instance_variable_defined?(:@defaults)
111
+
112
+ @defaults = if superclass < Anyway::Config
113
+ superclass.defaults.deep_dup
114
+ else
115
+ new_empty_config
116
+ end
117
+ end
118
+
119
+ def config_attributes
120
+ return @config_attributes if instance_variable_defined?(:@config_attributes)
121
+
122
+ @config_attributes = if superclass < Anyway::Config
123
+ superclass.config_attributes.dup
124
+ else
125
+ []
126
+ end
127
+ end
128
+
129
+ def required(*names)
130
+ unless (unknown_names = (names - config_attributes)).empty?
131
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}"
132
+ end
133
+
134
+ required_attributes.push(*names)
135
+ end
136
+
137
+ def required_attributes
138
+ return @required_attributes if instance_variable_defined?(:@required_attributes)
139
+
140
+ @required_attributes = if superclass < Anyway::Config
141
+ superclass.required_attributes.dup
142
+ else
143
+ []
144
+ end
145
+ end
146
+
147
+ def on_load(*names, &block)
148
+ raise ArgumentError, "Either methods or block should be specified, not both" if block && !names.empty?
149
+
150
+ if block
151
+ load_callbacks << BlockCallback.new(block)
152
+ else
153
+ load_callbacks.push(*names.map { NamedCallback.new(_1) })
154
+ end
155
+ end
156
+
157
+ def load_callbacks
158
+ return @load_callbacks if instance_variable_defined?(:@load_callbacks)
159
+
160
+ @load_callbacks = if superclass <= Anyway::Config
161
+ superclass.load_callbacks.dup
162
+ else
163
+ []
164
+ end
165
+ end
166
+
167
+ def config_name(val = nil)
168
+ return (@explicit_config_name = val.to_s) unless val.nil?
169
+
170
+ return @config_name if instance_variable_defined?(:@config_name)
171
+
172
+ @config_name = explicit_config_name || build_config_name
173
+ end
174
+
175
+ def explicit_config_name
176
+ return @explicit_config_name if instance_variable_defined?(:@explicit_config_name)
177
+
178
+ @explicit_config_name =
179
+ if superclass.respond_to?(:explicit_config_name)
180
+ superclass.explicit_config_name
181
+ end
182
+ end
183
+
184
+ def explicit_config_name?() = !explicit_config_name.nil?
185
+
186
+ def env_prefix(val = nil)
187
+ return (@env_prefix = val.to_s.upcase) unless val.nil?
188
+
189
+ return @env_prefix if instance_variable_defined?(:@env_prefix)
190
+
191
+ @env_prefix = if superclass < Anyway::Config && superclass.explicit_config_name?
192
+ superclass.env_prefix
193
+ else
194
+ config_name.upcase
195
+ end
196
+ end
197
+
198
+ def new_empty_config() = {}
199
+
200
+ def coerce_types(mapping)
201
+ coercion_mapping.deep_merge!(mapping)
202
+ end
203
+
204
+ def coercion_mapping
205
+ return @coercion_mapping if instance_variable_defined?(:@coercion_mapping)
206
+
207
+ @coercion_mapping = if superclass < Anyway::Config
208
+ superclass.coercion_mapping.deep_dup
209
+ else
210
+ {}
211
+ end
212
+ end
213
+
214
+ def type_caster(val = nil)
215
+ return @type_caster unless val.nil?
216
+
217
+ @type_caster ||=
218
+ if coercion_mapping.empty?
219
+ fallback_type_caster
220
+ else
221
+ ::Anyway::TypeCaster.new(coercion_mapping, fallback: fallback_type_caster)
222
+ end
223
+ end
224
+
225
+ def fallback_type_caster(val = nil)
226
+ return (@fallback_type_caster = val) unless val.nil?
227
+
228
+ return @fallback_type_caster if instance_variable_defined?(:@fallback_type_caster)
229
+
230
+ @fallback_type_caster = if superclass < Anyway::Config
231
+ superclass.fallback_type_caster.deep_dup
232
+ else
233
+ ::Anyway::AutoCast
234
+ end
235
+ end
236
+
237
+ def disable_auto_cast!
238
+ @fallback_type_caster = ::Anyway::NoCast
239
+ end
240
+
241
+ private
242
+
243
+ def define_config_accessor(*names)
244
+ names.each do |name|
245
+ accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
246
+ def #{name}=(val)
247
+ __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
248
+ values[:#{name}] = val
249
+ end
250
+
251
+ def #{name}
252
+ values[:#{name}]
253
+ end
254
+ RUBY
255
+ end
256
+ end
257
+
258
+ def accessors_module
259
+ return @accessors_module if instance_variable_defined?(:@accessors_module)
260
+
261
+ @accessors_module = Module.new.tap do |mod|
262
+ include mod
263
+ end
264
+ end
265
+
266
+ def build_config_name
267
+ unless name
268
+ raise "Please, specify config name explicitly for anonymous class " \
269
+ "via `config_name :my_config`"
270
+ end
271
+
272
+ # handle two cases:
273
+ # - SomeModule::Config => "some_module"
274
+ # - SomeConfig => "some"
275
+ unless name =~ /^(\w+)(::)?Config$/
276
+ raise "Couldn't infer config name, please, specify it explicitly" \
277
+ "via `config_name :my_config`"
278
+ end
279
+
280
+ Regexp.last_match[1].tap(&:downcase!)
281
+ end
282
+
283
+ def validate_param_names!(names)
284
+ invalid_names = names.reject { |name| name =~ PARAM_NAME }
285
+ return if invalid_names.empty?
286
+
287
+ raise ArgumentError, "Invalid attr_config name: #{invalid_names.join(", ")}.\n" \
288
+ "Valid names must satisfy /#{PARAM_NAME.source}/."
289
+ end
290
+ end
291
+
292
+ on_load :validate_required_attributes!
293
+
294
+ attr_reader :config_name, :env_prefix
295
+
296
+ # Instantiate config instance.
297
+ #
298
+ # Example:
299
+ #
300
+ # my_config = Anyway::Config.new()
301
+ #
302
+ # # provide some values explicitly
303
+ # my_config = Anyway::Config.new({some: :value})
304
+ #
305
+ def initialize(overrides = nil)
306
+ @config_name = self.class.config_name
307
+
308
+ raise ArgumentError, "Config name is missing" unless @config_name
309
+
310
+ @env_prefix = self.class.env_prefix
311
+ @values = {}
312
+
313
+ load(overrides)
314
+ end
315
+
316
+ def reload(overrides = nil)
317
+ clear
318
+ load(overrides)
319
+ self
320
+ end
321
+
322
+ def clear
323
+ values.clear
324
+ @__trace__ = nil
325
+ self
326
+ end
327
+
328
+ def load(overrides = nil)
329
+ base_config = self.class.defaults.deep_dup
330
+
331
+ trace = Tracing.capture do
332
+ Tracing.trace!(:defaults) { base_config }
333
+
334
+ config_path = resolve_config_path(config_name, env_prefix)
335
+
336
+ load_from_sources(base_config, name: config_name, env_prefix: env_prefix, config_path: config_path)
337
+
338
+ if overrides
339
+ Tracing.trace!(:load) { overrides }
340
+
341
+ Utils.deep_merge!(base_config, overrides)
342
+ end
343
+ end
344
+
345
+ base_config.each do |key, val|
346
+ write_config_attr(key.to_sym, val)
347
+ end
348
+
349
+ # Trace may contain unknown attributes
350
+ trace&.keep_if { |key| self.class.config_attributes.include?(key.to_sym) }
351
+
352
+ # Run on_load callbacks
353
+ self.class.load_callbacks.each { _1.apply_to(self) }
354
+
355
+ # Set trace after we write all the values to
356
+ # avoid changing the source to accessor
357
+ @__trace__ = trace
358
+
359
+ self
360
+ end
361
+
362
+ def load_from_sources(base_config, **options)
363
+ Anyway.loaders.each do |(_id, loader)|
364
+ Utils.deep_merge!(base_config, loader.call(**options))
365
+ end
366
+ base_config
367
+ end
368
+
369
+ def dig(*keys) = values.dig(*keys)
370
+
371
+ def to_h() = values.deep_dup.deep_freeze
372
+
373
+ def dup
374
+ self.class.allocate.tap do |new_config|
375
+ %i[config_name env_prefix __trace__].each do |ivar|
376
+ new_config.instance_variable_set(:"@#{ivar}", send(ivar).dup)
377
+ end
378
+ new_config.instance_variable_set(:@values, values.deep_dup)
379
+ end
380
+ end
381
+
382
+ def resolve_config_path(name, env_prefix)
383
+ Anyway.env.fetch(env_prefix).delete("conf") || Settings.default_config_path.call(name)
384
+ end
385
+
386
+ def deconstruct_keys(keys) = values.deconstruct_keys(keys)
387
+
388
+ def to_source_trace() = __trace__&.to_h
389
+
390
+ def inspect
391
+ "#<#{self.class}:0x#{vm_object_id.rjust(16, "0")} config_name=\"#{config_name}\" env_prefix=\"#{env_prefix}\" " \
392
+ "values=#{values.inspect}>"
393
+ end
394
+
395
+ def pretty_print(q)
396
+ q.object_group self do
397
+ q.nest(1) do
398
+ q.breakable
399
+ q.text "config_name=#{config_name.inspect}"
400
+ q.breakable
401
+ q.text "env_prefix=#{env_prefix.inspect}"
402
+ q.breakable
403
+ q.text "values:"
404
+ q.pp __trace__
405
+ end
406
+ end
407
+ end
408
+
409
+ private
410
+
411
+ attr_reader :values, :__trace__
412
+
413
+ def validate_required_attributes!
414
+ self.class.required_attributes.select do |name|
415
+ values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
416
+ end.then do |missing|
417
+ next if missing.empty?
418
+ raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
419
+ end
420
+ end
421
+
422
+ def write_config_attr(key, val)
423
+ key = key.to_sym
424
+ return unless self.class.config_attributes.include?(key)
425
+
426
+ val = __type_caster__.coerce(key, val)
427
+ public_send(:"#{key}=", val)
428
+ end
429
+
430
+ def raise_validation_error(msg)
431
+ raise ValidationError, msg
432
+ end
433
+
434
+ def __type_caster__
435
+ self.class.type_caster
436
+ end
437
+ end
438
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Adds ability to generate anonymous (class-less) config dynamicly
5
+ # (like Rails.application.config_for but using more data sources).
6
+ module DynamicConfig
7
+ module ClassMethods
8
+ # Load config as Hash by any name
9
+ #
10
+ # Example:
11
+ #
12
+ # my_config = Anyway::Config.for(:my_app)
13
+ # # will load data from config/my_app.yml, secrets.my_app, ENV["MY_APP_*"]
14
+ #
15
+ def for(name, auto_cast: true, **options)
16
+ config = allocate
17
+ options[:env_prefix] ||= name.to_s.upcase
18
+ options[:config_path] ||= config.resolve_config_path(name, options[:env_prefix])
19
+
20
+ raw_config = config.load_from_sources(new_empty_config, name: name, **options)
21
+ return raw_config unless auto_cast
22
+
23
+ AutoCast.call(raw_config)
24
+ end
25
+ end
26
+
27
+ def self.included(base)
28
+ base.extend ClassMethods
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Parses environment variables and provides
5
+ # method-like access
6
+ class Env
7
+ using RubyNext
8
+ using Anyway::Ext::DeepDup
9
+ using Anyway::Ext::Hash
10
+
11
+ include Tracing
12
+
13
+ attr_reader :data, :traces, :type_cast
14
+
15
+ def initialize(type_cast: AutoCast)
16
+ @type_cast = type_cast
17
+ @data = {}
18
+ @traces = {}
19
+ end
20
+
21
+ def clear
22
+ data.clear
23
+ traces.clear
24
+ end
25
+
26
+ def fetch(prefix)
27
+ return data[prefix].deep_dup if data.key?(prefix)
28
+
29
+ Tracing.capture do
30
+ data[prefix] = parse_env(prefix)
31
+ end.then do |trace|
32
+ traces[prefix] = trace
33
+ end
34
+
35
+ data[prefix].deep_dup
36
+ end
37
+
38
+ def fetch_with_trace(prefix)
39
+ [fetch(prefix), traces[prefix]]
40
+ end
41
+
42
+ private
43
+
44
+ def parse_env(prefix)
45
+ match_prefix = "#{prefix}_"
46
+ ENV.each_pair.with_object({}) do |(key, val), data|
47
+ next unless key.start_with?(match_prefix)
48
+
49
+ path = key.sub(/^#{prefix}_/, "").downcase
50
+
51
+ paths = path.split("__")
52
+ trace!(:env, *paths, key: key) { data.bury(type_cast.call(val), *paths) }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module Loaders
5
+ class Base
6
+ include Tracing
7
+
8
+ class << self
9
+ def call(local: Anyway::Settings.use_local_files, **opts)
10
+ new(local: local).call(**opts)
11
+ end
12
+ end
13
+
14
+ def initialize(local:)
15
+ @local = local
16
+ end
17
+
18
+ def use_local?() = @local == true
19
+ end
20
+ end
21
+ end