anyway_config 2.0.6 → 2.2.2

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 +237 -12
  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 +60 -15
  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 +1 -1
  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 +60 -15
  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 +1 -1
  21. data/lib/anyway/auto_cast.rb +39 -19
  22. data/lib/anyway/config.rb +74 -29
  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 +0 -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 +6 -6
  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,6 +197,47 @@ module Anyway # :nodoc:
194
197
 
195
198
  def new_empty_config() ; {}; end
196
199
 
200
+ def coerce_types(mapping)
201
+ Utils.deep_merge!(coercion_mapping, 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)
@@ -201,8 +245,7 @@ module Anyway # :nodoc:
201
245
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
202
246
  def #{name}=(val)
203
247
  __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
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
@@ -141,7 +141,7 @@ module Anyway
141
141
 
142
142
  def current_trace() ; trace_stack.last; end
143
143
 
144
- alias tracing? current_trace
144
+ alias_method :tracing?, :current_trace
145
145
 
146
146
  def source_stack
147
147
  (Thread.current[:__anyway__trace_source_stack__] ||= [])
@@ -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
data/lib/anyway/config.rb CHANGED
@@ -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
@@ -106,21 +109,21 @@ module Anyway # :nodoc:
106
109
  def defaults
107
110
  return @defaults if instance_variable_defined?(:@defaults)
108
111
 
109
- if superclass < Anyway::Config
112
+ @defaults = if superclass < Anyway::Config
110
113
  superclass.defaults.deep_dup
111
114
  else
112
115
  new_empty_config
113
- end => @defaults
116
+ end
114
117
  end
115
118
 
116
119
  def config_attributes
117
120
  return @config_attributes if instance_variable_defined?(:@config_attributes)
118
121
 
119
- if superclass < Anyway::Config
122
+ @config_attributes = if superclass < Anyway::Config
120
123
  superclass.config_attributes.dup
121
124
  else
122
125
  []
123
- end => @config_attributes
126
+ end
124
127
  end
125
128
 
126
129
  def required(*names)
@@ -134,17 +137,17 @@ module Anyway # :nodoc:
134
137
  def required_attributes
135
138
  return @required_attributes if instance_variable_defined?(:@required_attributes)
136
139
 
137
- if superclass < Anyway::Config
140
+ @required_attributes = if superclass < Anyway::Config
138
141
  superclass.required_attributes.dup
139
142
  else
140
143
  []
141
- end => @required_attributes
144
+ end
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) })
@@ -154,11 +157,11 @@ module Anyway # :nodoc:
154
157
  def load_callbacks
155
158
  return @load_callbacks if instance_variable_defined?(:@load_callbacks)
156
159
 
157
- if superclass <= Anyway::Config
160
+ @load_callbacks = if superclass <= Anyway::Config
158
161
  superclass.load_callbacks.dup
159
162
  else
160
163
  []
161
- end => @load_callbacks
164
+ end
162
165
  end
163
166
 
164
167
  def config_name(val = nil)
@@ -185,15 +188,56 @@ module Anyway # :nodoc:
185
188
 
186
189
  return @env_prefix if instance_variable_defined?(:@env_prefix)
187
190
 
188
- if superclass < Anyway::Config && superclass.explicit_config_name?
191
+ @env_prefix = if superclass < Anyway::Config && superclass.explicit_config_name?
189
192
  superclass.env_prefix
190
193
  else
191
194
  config_name.upcase
192
- end => @env_prefix
195
+ end
193
196
  end
194
197
 
195
198
  def new_empty_config() = {}
196
199
 
200
+ def coerce_types(mapping)
201
+ Utils.deep_merge!(coercion_mapping, 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)
@@ -201,8 +245,7 @@ module Anyway # :nodoc:
201
245
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
202
246
  def #{name}=(val)
203
247
  __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
248
+ values[:#{name}] = val
206
249
  end
207
250
 
208
251
  def #{name}
@@ -215,9 +258,9 @@ module Anyway # :nodoc:
215
258
  def accessors_module
216
259
  return @accessors_module if instance_variable_defined?(:@accessors_module)
217
260
 
218
- Module.new.tap do |mod|
261
+ @accessors_module = Module.new.tap do |mod|
219
262
  include mod
220
- end => @accessors_module
263
+ end
221
264
  end
222
265
 
223
266
  def build_config_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
@@ -285,22 +328,19 @@ module Anyway # :nodoc:
285
328
  def load(overrides = nil)
286
329
  base_config = self.class.defaults.deep_dup
287
330
 
288
- Tracing.capture do
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:, 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
- end => trace
343
+ end
304
344
 
305
345
  base_config.each do |key, val|
306
346
  write_config_attr(key.to_sym, val)
@@ -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
@@ -12,11 +12,15 @@ module Anyway
12
12
  # my_config = Anyway::Config.for(:my_app)
13
13
  # # will load data from config/my_app.yml, secrets.my_app, ENV["MY_APP_*"]
14
14
  #
15
- def for(name, **options)
15
+ def for(name, auto_cast: true, **options)
16
16
  config = allocate
17
17
  options[:env_prefix] ||= name.to_s.upcase
18
18
  options[:config_path] ||= config.resolve_config_path(name, options[:env_prefix])
19
- config.load_from_sources(new_empty_config, name: name, **options)
19
+
20
+ raw_config = config.load_from_sources(new_empty_config, name:, **options)
21
+ return raw_config unless auto_cast
22
+
23
+ AutoCast.call(raw_config)
20
24
  end
21
25
  end
22
26