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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +241 -181
- data/README.md +238 -13
- data/lib/.rbnext/1995.next/anyway/config.rb +438 -0
- data/lib/.rbnext/1995.next/anyway/dynamic_config.rb +31 -0
- data/lib/.rbnext/1995.next/anyway/env.rb +56 -0
- data/lib/.rbnext/1995.next/anyway/loaders/base.rb +21 -0
- data/lib/.rbnext/1995.next/anyway/tracing.rb +181 -0
- data/lib/.rbnext/2.7/anyway/auto_cast.rb +39 -19
- data/lib/.rbnext/2.7/anyway/config.rb +61 -16
- data/lib/.rbnext/2.7/anyway/rails/loaders/yaml.rb +30 -0
- data/lib/.rbnext/2.7/anyway/rbs.rb +92 -0
- data/lib/.rbnext/2.7/anyway/settings.rb +79 -0
- data/lib/.rbnext/2.7/anyway/tracing.rb +6 -6
- data/lib/.rbnext/2.7/anyway/type_casting.rb +143 -0
- data/lib/.rbnext/3.0/anyway/auto_cast.rb +53 -0
- data/lib/.rbnext/{2.8 → 3.0}/anyway/config.rb +61 -16
- data/lib/.rbnext/{2.8 → 3.0}/anyway/loaders/base.rb +0 -0
- data/lib/.rbnext/{2.8 → 3.0}/anyway/loaders.rb +0 -0
- data/lib/.rbnext/{2.8 → 3.0}/anyway/tracing.rb +6 -6
- data/lib/anyway/auto_cast.rb +39 -19
- data/lib/anyway/config.rb +75 -30
- data/lib/anyway/dynamic_config.rb +6 -2
- data/lib/anyway/env.rb +1 -1
- data/lib/anyway/ext/deep_dup.rb +12 -0
- data/lib/anyway/ext/hash.rb +10 -12
- data/lib/anyway/loaders/base.rb +1 -1
- data/lib/anyway/loaders/env.rb +3 -1
- data/lib/anyway/loaders/yaml.rb +9 -5
- data/lib/anyway/option_parser_builder.rb +1 -3
- data/lib/anyway/optparse_config.rb +5 -7
- data/lib/anyway/rails/loaders/credentials.rb +4 -4
- data/lib/anyway/rails/loaders/secrets.rb +6 -8
- data/lib/anyway/rails/loaders/yaml.rb +11 -0
- data/lib/anyway/rails/settings.rb +9 -2
- data/lib/anyway/rbs.rb +92 -0
- data/lib/anyway/settings.rb +52 -2
- data/lib/anyway/tracing.rb +9 -9
- data/lib/anyway/type_casting.rb +134 -0
- data/lib/anyway/utils/deep_merge.rb +21 -0
- data/lib/anyway/version.rb +1 -1
- data/lib/anyway_config.rb +4 -0
- data/sig/anyway_config.rbs +129 -0
- metadata +42 -15
- 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_](
|
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
|
148
|
+
raise ArgumentError, "Either methods or block should be specified, not both" if block && !names.empty?
|
146
149
|
|
147
|
-
if
|
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
|
-
|
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+)(
|
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
|
-
|
292
|
-
|
293
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
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, **
|
174
|
+
Tracing.current_trace.merge_values(val, type: type, **opts)
|
175
175
|
else
|
176
|
-
Tracing.current_trace.record_value(val, *path, **
|
176
|
+
Tracing.current_trace.record_value(val, *path, type: type, **opts)
|
177
177
|
end
|
178
178
|
val
|
179
179
|
end
|
data/lib/anyway/auto_cast.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
10
|
+
class << self
|
11
|
+
def call(val)
|
12
|
+
return val unless val.is_a?(::Hash) || val.is_a?(::String)
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|