anyway_config 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,130 @@
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
+ caster =
26
+ if type_id.is_a?(Symbol)
27
+ registry.fetch(type_id) { raise ArgumentError, "Unknown type: #{type_id}" }
28
+ else
29
+ raise ArgumentError, "Type must implement #call(val): #{type_id}" unless type_id.respond_to?(:call)
30
+ type_id
31
+ end
32
+
33
+ if array
34
+ raw_arr = raw.is_a?(Array) ? raw : raw.split(/\s*,\s*/)
35
+ raw_arr.map { |_1| caster.call(_1) }
36
+ else
37
+ caster.call(raw)
38
+ end
39
+ end
40
+
41
+ def dup
42
+ new_obj = self.class.allocate
43
+ new_obj.instance_variable_set(:@registry, registry.dup)
44
+ new_obj
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :registry
50
+ end
51
+
52
+ TypeRegistry.default.tap do |obj|
53
+ obj.accept(:string, &:to_s)
54
+ obj.accept(:integer, &:to_i)
55
+ obj.accept(:float, &:to_f)
56
+
57
+ obj.accept(:date) do |_1|
58
+ require "date" unless defined?(::Date)
59
+
60
+ Date.parse(_1)
61
+ end
62
+
63
+ obj.accept(:datetime) do |_1|
64
+ require "date" unless defined?(::Date)
65
+
66
+ DateTime.parse(_1)
67
+ end
68
+
69
+ obj.accept(:uri) do |_1|
70
+ require "uri" unless defined?(::URI)
71
+
72
+ URI.parse(_1)
73
+ end
74
+
75
+ obj.accept(:boolean) do |_1|
76
+ _1.match?(/\A(true|t|yes|y|1)\z/i)
77
+ end
78
+ end
79
+
80
+ # TypeCaster is an object responsible for type-casting.
81
+ # It uses a provided types registry and mapping, and also
82
+ # accepts a fallback typecaster.
83
+ class TypeCaster
84
+ using Ext::DeepDup
85
+ using Ext::Hash
86
+
87
+ def initialize(mapping, registry: TypeRegistry.default, fallback: ::Anyway::AutoCast)
88
+ @mapping = mapping.deep_dup
89
+ @registry = registry
90
+ @fallback = fallback
91
+ end
92
+
93
+ def coerce(key, val, config: mapping)
94
+ caster_config = config[key.to_sym]
95
+
96
+ return fallback.coerce(key, val) unless caster_config
97
+
98
+ case; when ((__m__ = caster_config)) && false
99
+ 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?))))
100
+ registry.deserialize(val, type, array: array)
101
+ when (Hash === __m__)
102
+
103
+
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
+ return val unless val.is_a?(Hash)
112
+
113
+ caster_config.each do |k, v|
114
+ ks = k.to_s
115
+ next unless val.key?(ks)
116
+
117
+ val[ks] = coerce(k, val[ks], config: caster_config)
118
+ end
119
+
120
+ val
121
+ else
122
+ registry.deserialize(val, caster_config)
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ attr_reader :mapping, :registry, :fallback
129
+ end
130
+ 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
@@ -44,6 +44,7 @@ module Anyway # :nodoc:
44
44
  to_h
45
45
  to_source_trace
46
46
  write_config_attr
47
+ __type_caster__
47
48
  ].freeze
48
49
 
49
50
  class Error < StandardError; end
@@ -196,6 +197,47 @@ module Anyway # :nodoc:
196
197
 
197
198
  def new_empty_config() ; {}; end
198
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
+
199
241
  private
200
242
 
201
243
  def define_config_accessor(*names)
@@ -373,7 +415,7 @@ module Anyway # :nodoc:
373
415
  values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
374
416
  end.then do |missing|
375
417
  next if missing.empty?
376
- 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(", ")}"
377
419
  end
378
420
  end
379
421
 
@@ -381,11 +423,16 @@ module Anyway # :nodoc:
381
423
  key = key.to_sym
382
424
  return unless self.class.config_attributes.include?(key)
383
425
 
426
+ val = __type_caster__.coerce(key, val)
384
427
  public_send(:"#{key}=", val)
385
428
  end
386
429
 
387
430
  def raise_validation_error(msg)
388
431
  raise ValidationError, msg
389
432
  end
433
+
434
+ def __type_caster__
435
+ self.class.type_caster
436
+ end
390
437
  end
391
438
  end
@@ -34,13 +34,11 @@ module Anyway
34
34
 
35
35
  def record_value(val, *path, **opts)
36
36
  key = path.pop
37
- (__m__ = if val.is_a?(Hash)
38
- Trace.new.tap {
39
- _1.merge_values(val, **opts)
40
- }
41
- else
42
- Trace.new(:value, val, **opts)
43
- end) && (((trace = __m__) || true) || Kernel.raise(NoMatchingPatternError, __m__.inspect))
37
+ trace = if val.is_a?(Hash)
38
+ Trace.new.tap { _1.merge_values(val, **opts) }
39
+ else
40
+ Trace.new(:value, val, **opts)
41
+ end
44
42
 
45
43
  target_trace = path.empty? ? self : value.dig(*path)
46
44
  target_trace.value[key.to_s] = trace
@@ -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
@@ -44,6 +44,7 @@ module Anyway # :nodoc:
44
44
  to_h
45
45
  to_source_trace
46
46
  write_config_attr
47
+ __type_caster__
47
48
  ].freeze
48
49
 
49
50
  class Error < StandardError; end
@@ -196,6 +197,47 @@ module Anyway # :nodoc:
196
197
 
197
198
  def new_empty_config() = {}
198
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
+
199
241
  private
200
242
 
201
243
  def define_config_accessor(*names)
@@ -373,7 +415,7 @@ module Anyway # :nodoc:
373
415
  values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
374
416
  end.then do |missing|
375
417
  next if missing.empty?
376
- 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(", ")}"
377
419
  end
378
420
  end
379
421
 
@@ -381,11 +423,16 @@ module Anyway # :nodoc:
381
423
  key = key.to_sym
382
424
  return unless self.class.config_attributes.include?(key)
383
425
 
426
+ val = __type_caster__.coerce(key, val)
384
427
  public_send(:"#{key}=", val)
385
428
  end
386
429
 
387
430
  def raise_validation_error(msg)
388
431
  raise ValidationError, msg
389
432
  end
433
+
434
+ def __type_caster__
435
+ self.class.type_caster
436
+ end
390
437
  end
391
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:, **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
 
@@ -36,6 +36,12 @@ module Anyway
36
36
  end
37
37
  end
38
38
 
39
+ refine ::Module do
40
+ def deep_dup
41
+ self
42
+ end
43
+ end
44
+
39
45
  using self
40
46
  end
41
47
  end
@@ -26,6 +26,16 @@ module Anyway
26
26
  end
27
27
  hash[last_key] = val
28
28
  end
29
+
30
+ def deep_merge!(other)
31
+ other.each do |k, v|
32
+ if key?(k) && self[k].is_a?(::Hash) && v.is_a?(::Hash)
33
+ self[k].deep_merge!(v)
34
+ else
35
+ self[k] = v
36
+ end
37
+ end
38
+ end
29
39
  end
30
40
 
31
41
  using self
@@ -6,7 +6,9 @@ module Anyway
6
6
  module Loaders
7
7
  class Env < Base
8
8
  def call(env_prefix:, **_options)
9
- Anyway.env.fetch_with_trace(env_prefix).then do |(conf, trace)|
9
+ env = ::Anyway::Env.new(type_cast: ::Anyway::NoCast)
10
+
11
+ env.fetch_with_trace(env_prefix).then do |(conf, trace)|
10
12
  Tracing.current_trace&.merge!(trace)
11
13
  conf
12
14
  end
@@ -25,10 +25,14 @@ module Anyway
25
25
  def parse_yml(path)
26
26
  return {} unless File.file?(path)
27
27
  require "yaml" unless defined?(::YAML)
28
+
29
+ # By default, YAML load will return `false` when the yaml document is
30
+ # empty. When this occurs, we return an empty hash instead, to match
31
+ # the interface when no config file is present.
28
32
  if defined?(ERB)
29
- ::YAML.load(ERB.new(File.read(path)).result) # rubocop:disable Security/YAMLLoad
33
+ ::YAML.load(ERB.new(File.read(path)).result) || {} # rubocop:disable Security/YAMLLoad
30
34
  else
31
- ::YAML.load_file(path)
35
+ ::YAML.load_file(path) || {}
32
36
  end
33
37
  end
34
38
 
@@ -8,8 +8,6 @@ module Anyway # :nodoc:
8
8
  class << self
9
9
  def call(options)
10
10
  OptionParser.new do |opts|
11
- opts.accept(AutoCast) { AutoCast.call(_1) }
12
-
13
11
  options.each do |key, descriptor|
14
12
  opts.on(*option_parser_on_args(key, **descriptor)) do |val|
15
13
  yield [key, val]
@@ -20,7 +18,7 @@ module Anyway # :nodoc:
20
18
 
21
19
  private
22
20
 
23
- def option_parser_on_args(key, flag: false, desc: nil, type: AutoCast)
21
+ def option_parser_on_args(key, flag: false, desc: nil, type: ::String)
24
22
  on_args = ["--#{key.to_s.tr("_", "-")}#{flag ? "" : " VALUE"}"]
25
23
  on_args << type unless flag
26
24
  on_args << desc unless desc.nil?
@@ -70,13 +70,11 @@ module Anyway
70
70
  end
71
71
 
72
72
  def option_parser
73
- @option_parser ||= begin
74
- OptionParserBuilder.call(self.class.option_parser_options) do |key, val|
75
- write_config_attr(key, val)
76
- end.tap do |parser|
77
- self.class.option_parser_extensions.map do |extension|
78
- extension.call(parser, self)
79
- end
73
+ @option_parser ||= OptionParserBuilder.call(self.class.option_parser_options) do |key, val|
74
+ write_config_attr(key, val)
75
+ end.tap do |parser|
76
+ self.class.option_parser_extensions.map do |extension|
77
+ extension.call(parser, self)
80
78
  end
81
79
  end
82
80
  end
@@ -22,7 +22,7 @@ module Anyway
22
22
  :credentials,
23
23
  store: credentials_path
24
24
  ) do
25
- ::Rails.application.credentials.public_send(name)
25
+ ::Rails.application.credentials.config[name.to_sym]
26
26
  end.then do |creds|
27
27
  Utils.deep_merge!(config, creds) if creds
28
28
  end
@@ -48,7 +48,7 @@ module Anyway
48
48
  key_path: ::Rails.root.join("config/credentials/local.key")
49
49
  )
50
50
 
51
- creds.public_send(name)
51
+ creds.config[name.to_sym]
52
52
  end
53
53
 
54
54
  def credentials_path
@@ -24,13 +24,11 @@ module Anyway
24
24
  private
25
25
 
26
26
  def secrets
27
- @secrets ||= begin
28
- ::Rails.application.secrets.tap do |_|
29
- # Reset secrets state if the app hasn't been initialized
30
- # See https://github.com/palkan/anyway_config/issues/14
31
- next if ::Rails.application.initialized?
32
- ::Rails.application.remove_instance_variable(:@secrets)
33
- end
27
+ @secrets ||= ::Rails.application.secrets.tap do |_|
28
+ # Reset secrets state if the app hasn't been initialized
29
+ # See https://github.com/palkan/anyway_config/issues/14
30
+ next if ::Rails.application.initialized?
31
+ ::Rails.application.remove_instance_variable(:@secrets)
34
32
  end
35
33
  end
36
34
  end
@@ -27,10 +27,11 @@ module Anyway
27
27
 
28
28
  @autoload_static_config_path = val
29
29
 
30
- # See https://github.com/rails/rails/blob/8ab4fd12f18203b83d0f252db96d10731485ff6a/railties/lib/rails/autoloaders.rb#L10
30
+ # See Rails 6 https://github.com/rails/rails/blob/8ab4fd12f18203b83d0f252db96d10731485ff6a/railties/lib/rails/autoloaders.rb#L10
31
+ # and Rails 7 https://github.com/rails/rails/blob/5462fbd5de1900c1b1ce1c9dc11c1a2d8cdcd809/railties/lib/rails/autoloaders.rb#L15
31
32
  @autoloader = Zeitwerk::Loader.new.tap do |loader|
32
33
  loader.tag = "anyway.config"
33
- loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
34
+ loader.inflector = defined?(ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector) ? ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector : ::Rails::Autoloaders::Inflector
34
35
  loader.push_dir(::Rails.root.join(val))
35
36
  loader.setup
36
37
  end