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,143 @@
1
+ # frozen_string_literal: true
2
+ using RubyNext;
3
+ module Anyway
4
+ # Contains a mapping between type IDs/names and deserializers
5
+ class TypeRegistry
6
+ class << self
7
+ def default
8
+ @default ||= TypeRegistry.new
9
+ end
10
+ end
11
+
12
+ def initialize
13
+ @registry = {}
14
+ end
15
+
16
+ def accept(name_or_object, &block)
17
+ if !block && !name_or_object.respond_to?(:call)
18
+ raise ArgumentError, "Please, provide a type casting block or an object implementing #call(val) method"
19
+ end
20
+
21
+ registry[name_or_object] = block || name_or_object
22
+ end
23
+
24
+ def deserialize(raw, type_id, array: false)
25
+ return if raw.nil?
26
+
27
+ caster =
28
+ if type_id.is_a?(Symbol) || type_id.nil?
29
+ registry.fetch(type_id) { raise ArgumentError, "Unknown type: #{type_id}" }
30
+ else
31
+ raise ArgumentError, "Type must implement #call(val): #{type_id}" unless type_id.respond_to?(:call)
32
+ type_id
33
+ end
34
+
35
+ if array
36
+ raw_arr = raw.is_a?(String) ? raw.split(/\s*,\s*/) : Array(raw)
37
+ raw_arr.map { |_1| caster.call(_1) }
38
+ else
39
+ caster.call(raw)
40
+ end
41
+ end
42
+
43
+ def dup
44
+ new_obj = self.class.allocate
45
+ new_obj.instance_variable_set(:@registry, registry.dup)
46
+ new_obj
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :registry
52
+ end
53
+
54
+ TypeRegistry.default.tap do |obj|
55
+ obj.accept(nil, &:itself)
56
+ obj.accept(:string, &:to_s)
57
+ obj.accept(:integer, &:to_i)
58
+ obj.accept(:float, &:to_f)
59
+
60
+ obj.accept(:date) do |_1|
61
+ require "date" unless defined?(::Date)
62
+
63
+ next _1 if _1.is_a?(::Date)
64
+
65
+ next _1.to_date if _1.respond_to?(:to_date)
66
+
67
+ ::Date.parse(_1)
68
+ end
69
+
70
+ obj.accept(:datetime) do |_1|
71
+ require "date" unless defined?(::Date)
72
+
73
+ next _1 if _1.is_a?(::DateTime)
74
+
75
+ next _1.to_datetime if _1.respond_to?(:to_datetime)
76
+
77
+ ::DateTime.parse(_1)
78
+ end
79
+
80
+ obj.accept(:uri) do |_1|
81
+ require "uri" unless defined?(::URI)
82
+
83
+ next _1 if _1.is_a?(::URI)
84
+
85
+ ::URI.parse(_1)
86
+ end
87
+
88
+ obj.accept(:boolean) do |_1|
89
+ _1.to_s.match?(/\A(true|t|yes|y|1)\z/i)
90
+ end
91
+ end
92
+
93
+ # TypeCaster is an object responsible for type-casting.
94
+ # It uses a provided types registry and mapping, and also
95
+ # accepts a fallback typecaster.
96
+ class TypeCaster
97
+ using Ext::DeepDup
98
+ using Ext::Hash
99
+
100
+ def initialize(mapping, registry: TypeRegistry.default, fallback: ::Anyway::AutoCast)
101
+ @mapping = mapping.deep_dup
102
+ @registry = registry
103
+ @fallback = fallback
104
+ end
105
+
106
+ def coerce(key, val, config: mapping)
107
+ caster_config = config[key.to_sym]
108
+
109
+ return fallback.coerce(key, val) unless caster_config
110
+
111
+ case; when ((__m__ = caster_config)) && false
112
+ when ((__m__.respond_to?(:deconstruct_keys) && (((__m_hash__src__ = __m__.deconstruct_keys(nil)) || true) && (Hash === __m_hash__src__ || Kernel.raise(TypeError, "#deconstruct_keys must return Hash"))) && (__m_hash__ = __m_hash__src__.dup)) && ((__m_hash__.key?(:array) && __m_hash__.key?(:type)) && (((array = __m_hash__.delete(:array)) || true) && (((type = __m_hash__.delete(:type)) || true) && __m_hash__.empty?))))
113
+ registry.deserialize(val, type, array: array)
114
+ when (Hash === __m__)
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
+
123
+
124
+ return val unless val.is_a?(Hash)
125
+
126
+ caster_config.each do |k, v|
127
+ ks = k.to_s
128
+ next unless val.key?(ks)
129
+
130
+ val[ks] = coerce(k, val[ks], config: caster_config)
131
+ end
132
+
133
+ val
134
+ else
135
+ registry.deserialize(val, caster_config)
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ attr_reader :mapping, :registry, :fallback
142
+ end
143
+ end
@@ -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
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 { call(_1) }
17
+ when ARRAY_RXP
18
+ val.split(/\s*,\s*/).map { call(_1) }
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; end
50
+
51
+ def self.coerce(_key, val) ; val; end
52
+ end
53
+ end
@@ -19,7 +19,7 @@ module Anyway # :nodoc:
19
19
  # Provides `attr_config` method to describe
20
20
  # configuration parameters and set defaults
21
21
  class Config
22
- PARAM_NAME = /^[a-z_]([\w]+)?$/
22
+ PARAM_NAME = /^[a-z_](\w+)?$/
23
23
 
24
24
  # List of names that couldn't be used as config names
25
25
  # (the class instance methods we use)
@@ -40,12 +40,15 @@ module Anyway # :nodoc:
40
40
  raise_validation_error
41
41
  reload
42
42
  resolve_config_path
43
+ tap
43
44
  to_h
44
45
  to_source_trace
45
46
  write_config_attr
47
+ __type_caster__
46
48
  ].freeze
47
49
 
48
50
  class Error < StandardError; end
51
+
49
52
  class ValidationError < Error; end
50
53
 
51
54
  include OptparseConfig
@@ -142,9 +145,9 @@ module Anyway # :nodoc:
142
145
  end
143
146
 
144
147
  def on_load(*names, &block)
145
- raise ArgumentError, "Either methods or block should be specified, not both" if block_given? && !names.empty?
148
+ raise ArgumentError, "Either methods or block should be specified, not both" if block && !names.empty?
146
149
 
147
- if block_given?
150
+ if block
148
151
  load_callbacks << BlockCallback.new(block)
149
152
  else
150
153
  load_callbacks.push(*names.map { NamedCallback.new(_1) })
@@ -194,15 +197,55 @@ module Anyway # :nodoc:
194
197
 
195
198
  def new_empty_config() ; {}; end
196
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
+
197
241
  private
198
242
 
199
243
  def define_config_accessor(*names)
200
244
  names.each do |name|
201
245
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
202
246
  def #{name}=(val)
203
- __trace__&.record_value(val, \"#{name}\", Tracing.current_trace_source)
204
- # DEPRECATED: instance variable set will be removed in 2.1
205
- @#{name} = values[:#{name}] = val
247
+ __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
248
+ values[:#{name}] = val
206
249
  end
207
250
 
208
251
  def #{name}
@@ -229,7 +272,7 @@ module Anyway # :nodoc:
229
272
  # handle two cases:
230
273
  # - SomeModule::Config => "some_module"
231
274
  # - SomeConfig => "some"
232
- unless name =~ /^(\w+)(\:\:)?Config$/
275
+ unless name =~ /^(\w+)(::)?Config$/
233
276
  raise "Couldn't infer config name, please, specify it explicitly" \
234
277
  "via `config_name :my_config`"
235
278
  end
@@ -288,17 +331,14 @@ module Anyway # :nodoc:
288
331
  trace = Tracing.capture do
289
332
  Tracing.trace!(:defaults) { base_config }
290
333
 
291
- load_from_sources(
292
- base_config,
293
- name: config_name,
294
- env_prefix: env_prefix,
295
- config_path: resolve_config_path(config_name, env_prefix)
296
- )
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)
297
337
 
298
338
  if overrides
299
339
  Tracing.trace!(:load) { overrides }
300
340
 
301
- base_config.deep_merge!(overrides)
341
+ Utils.deep_merge!(base_config, overrides)
302
342
  end
303
343
  end
304
344
 
@@ -321,7 +361,7 @@ module Anyway # :nodoc:
321
361
 
322
362
  def load_from_sources(base_config, **options)
323
363
  Anyway.loaders.each do |(_id, loader)|
324
- base_config.deep_merge!(loader.call(**options))
364
+ Utils.deep_merge!(base_config, loader.call(**options))
325
365
  end
326
366
  base_config
327
367
  end
@@ -375,7 +415,7 @@ module Anyway # :nodoc:
375
415
  values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
376
416
  end.then do |missing|
377
417
  next if missing.empty?
378
- raise_validation_error "The following config parameters are missing or empty: #{missing.join(", ")}"
418
+ raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
379
419
  end
380
420
  end
381
421
 
@@ -383,11 +423,16 @@ module Anyway # :nodoc:
383
423
  key = key.to_sym
384
424
  return unless self.class.config_attributes.include?(key)
385
425
 
426
+ val = __type_caster__.coerce(key, val)
386
427
  public_send(:"#{key}=", val)
387
428
  end
388
429
 
389
430
  def raise_validation_error(msg)
390
431
  raise ValidationError, msg
391
432
  end
433
+
434
+ def __type_caster__
435
+ self.class.type_caster
436
+ end
392
437
  end
393
438
  end
File without changes
File without changes
@@ -32,7 +32,8 @@ module Anyway
32
32
  value.dig(...)
33
33
  end
34
34
 
35
- def record_value(val, *path, key, **opts)
35
+ def record_value(val, *path, **opts)
36
+ key = path.pop
36
37
  trace = if val.is_a?(Hash)
37
38
  Trace.new.tap { _1.merge_values(val, **opts) }
38
39
  else
@@ -89,7 +90,7 @@ module Anyway
89
90
  end
90
91
  end
91
92
 
92
- def dup() ; self.class.new(type, value.dup, source); end
93
+ def dup() ; self.class.new(type, value.dup, **source); end
93
94
 
94
95
  def pretty_print(q)
95
96
  if trace?
@@ -140,7 +141,7 @@ module Anyway
140
141
 
141
142
  def current_trace() ; trace_stack.last; end
142
143
 
143
- alias tracing? current_trace
144
+ alias_method :tracing?, :current_trace
144
145
 
145
146
  def source_stack
146
147
  (Thread.current[:__anyway__trace_source_stack__] ||= [])
@@ -168,12 +169,11 @@ module Anyway
168
169
 
169
170
  def trace!(type, *path, **opts)
170
171
  return yield unless Tracing.tracing?
171
- source = {type: type}.merge(opts)
172
172
  val = yield
173
173
  if val.is_a?(Hash)
174
- Tracing.current_trace.merge_values(val, **source)
174
+ Tracing.current_trace.merge_values(val, type: type, **opts)
175
175
  else
176
- Tracing.current_trace.record_value(val, *path, **source)
176
+ Tracing.current_trace.record_value(val, *path, type: type, **opts)
177
177
  end
178
178
  val
179
179
  end
@@ -7,27 +7,47 @@ module Anyway
7
7
  # and doesn't start/end with quote
8
8
  ARRAY_RXP = /\A[^'"].*\s*,\s*.*[^'"]\z/
9
9
 
10
- def self.call(val)
11
- return val unless String === val
10
+ class << self
11
+ def call(val)
12
+ return val unless val.is_a?(::Hash) || val.is_a?(::String)
12
13
 
13
- case val
14
- when ARRAY_RXP
15
- val.split(/\s*,\s*/).map { call(_1) }
16
- when /\A(true|t|yes|y)\z/i
17
- true
18
- when /\A(false|f|no|n)\z/i
19
- false
20
- when /\A(nil|null)\z/i
21
- nil
22
- when /\A\d+\z/
23
- val.to_i
24
- when /\A\d*\.\d+\z/
25
- val.to_f
26
- when /\A['"].*['"]\z/
27
- val.gsub(/(\A['"]|['"]\z)/, "")
28
- else
29
- val
14
+ case val
15
+ when Hash
16
+ val.transform_values { call(_1) }
17
+ when ARRAY_RXP
18
+ val.split(/\s*,\s*/).map { call(_1) }
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)
30
44
  end
31
45
  end
32
46
  end
47
+
48
+ module NoCast
49
+ def self.call(val) = val
50
+
51
+ def self.coerce(_key, val) = val
52
+ end
33
53
  end