anyway_config 2.0.4 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -181
  3. data/README.md +231 -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 +130 -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 +121 -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 +123 -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