anyway_config 2.0.6 → 2.1.0

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.
@@ -0,0 +1,27 @@
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, **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
+ config.load_from_sources(new_empty_config, name: name, **options)
20
+ end
21
+ end
22
+
23
+ def self.included(base)
24
+ base.extend ClassMethods
25
+ end
26
+ end
27
+ 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
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Provides method to trace values association
5
+ module Tracing
6
+ using Anyway::Ext::DeepDup
7
+
8
+ using(Module.new do
9
+ refine Hash do
10
+ def inspect
11
+ "{#{map { |k, v| "#{k}: #{v.inspect}" }.join(", ")}}"
12
+ end
13
+ end
14
+
15
+ refine Thread::Backtrace::Location do
16
+ def path_lineno() = "#{path}:#{lineno}"
17
+ end
18
+ end)
19
+
20
+ class Trace
21
+ UNDEF = Object.new
22
+
23
+ attr_reader :type, :value, :source
24
+
25
+ def initialize(type = :trace, value = UNDEF, **source)
26
+ @type = type
27
+ @source = source
28
+ @value = value == UNDEF ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
29
+ end
30
+
31
+ def dig(...)
32
+ value.dig(...)
33
+ end
34
+
35
+ def record_value(val, *path, **opts)
36
+ key = path.pop
37
+ if val.is_a?(Hash)
38
+ Trace.new.tap { _1.merge_values(val, **opts) }
39
+ else
40
+ Trace.new(:value, val, **opts)
41
+ end => trace
42
+
43
+ target_trace = path.empty? ? self : value.dig(*path)
44
+ target_trace.value[key.to_s] = trace
45
+
46
+ val
47
+ end
48
+
49
+ def merge_values(hash, **opts)
50
+ return hash unless hash
51
+
52
+ hash.each do |key, val|
53
+ if val.is_a?(Hash)
54
+ value[key.to_s].merge_values(val, **opts)
55
+ else
56
+ value[key.to_s] = Trace.new(:value, val, **opts)
57
+ end
58
+ end
59
+
60
+ hash
61
+ end
62
+
63
+ def merge!(another_trace)
64
+ raise ArgumentError, "You can only merge into a :trace type, and this is :#{type}" unless trace?
65
+ raise ArgumentError, "You can only merge a :trace type, but trying :#{type}" unless another_trace.trace?
66
+
67
+ another_trace.value.each do |key, sub_trace|
68
+ if sub_trace.trace?
69
+ value[key].merge! sub_trace
70
+ else
71
+ value[key] = sub_trace
72
+ end
73
+ end
74
+ end
75
+
76
+ def keep_if(...)
77
+ raise ArgumentError, "You can only filter :trace type, and this is :#{type}" unless trace?
78
+ value.keep_if(...)
79
+ end
80
+
81
+ def clear() = value.clear
82
+
83
+ def trace?() = type == :trace
84
+
85
+ def to_h
86
+ if trace?
87
+ value.transform_values(&:to_h).tap { _1.default_proc = nil }
88
+ else
89
+ {value: value, source: source}
90
+ end
91
+ end
92
+
93
+ def dup() = self.class.new(type, value.dup, **source)
94
+
95
+ def pretty_print(q)
96
+ if trace?
97
+ q.nest(2) do
98
+ q.breakable ""
99
+ q.seplist(value, nil, :each) do |k, v|
100
+ q.group do
101
+ q.text k
102
+ q.text " =>"
103
+ q.breakable " " unless v.trace?
104
+ q.pp v
105
+ end
106
+ end
107
+ end
108
+ else
109
+ q.pp value
110
+ q.group(0, " (", ")") do
111
+ q.seplist(source, lambda { q.breakable " " }, :each) do |k, v|
112
+ q.group do
113
+ q.text k.to_s
114
+ q.text "="
115
+ q.text v.to_s
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ class << self
124
+ def capture
125
+ unless Settings.tracing_enabled
126
+ yield
127
+ return
128
+ end
129
+
130
+ trace = Trace.new
131
+ trace_stack.push trace
132
+ yield
133
+ trace_stack.last
134
+ ensure
135
+ trace_stack.pop
136
+ end
137
+
138
+ def trace_stack
139
+ (Thread.current[:__anyway__trace_stack__] ||= [])
140
+ end
141
+
142
+ def current_trace() = trace_stack.last
143
+
144
+ alias_method :tracing?, :current_trace
145
+
146
+ def source_stack
147
+ (Thread.current[:__anyway__trace_source_stack__] ||= [])
148
+ end
149
+
150
+ def current_trace_source
151
+ source_stack.last || accessor_source(caller_locations(2, 1).first)
152
+ end
153
+
154
+ def with_trace_source(src)
155
+ source_stack << src
156
+ yield
157
+ ensure
158
+ source_stack.pop
159
+ end
160
+
161
+ private
162
+
163
+ def accessor_source(location)
164
+ {type: :accessor, called_from: location.path_lineno}
165
+ end
166
+ end
167
+
168
+ module_function
169
+
170
+ def trace!(type, *path, **opts)
171
+ return yield unless Tracing.tracing?
172
+ val = yield
173
+ if val.is_a?(Hash)
174
+ Tracing.current_trace.merge_values(val, type: type, **opts)
175
+ else
176
+ Tracing.current_trace.record_value(val, *path, type: type, **opts)
177
+ end
178
+ val
179
+ end
180
+ end
181
+ 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,14 @@ 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
46
47
  ].freeze
47
48
 
48
49
  class Error < StandardError; end
50
+
49
51
  class ValidationError < Error; end
50
52
 
51
53
  include OptparseConfig
@@ -142,9 +144,9 @@ module Anyway # :nodoc:
142
144
  end
143
145
 
144
146
  def on_load(*names, &block)
145
- raise ArgumentError, "Either methods or block should be specified, not both" if block_given? && !names.empty?
147
+ raise ArgumentError, "Either methods or block should be specified, not both" if block && !names.empty?
146
148
 
147
- if block_given?
149
+ if block
148
150
  load_callbacks << BlockCallback.new(block)
149
151
  else
150
152
  load_callbacks.push(*names.map { |_1| NamedCallback.new(_1) })
@@ -201,8 +203,7 @@ module Anyway # :nodoc:
201
203
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
202
204
  def #{name}=(val)
203
205
  __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
206
+ values[:#{name}] = val
206
207
  end
207
208
 
208
209
  def #{name}
@@ -229,7 +230,7 @@ module Anyway # :nodoc:
229
230
  # handle two cases:
230
231
  # - SomeModule::Config => "some_module"
231
232
  # - SomeConfig => "some"
232
- unless name =~ /^(\w+)(\:\:)?Config$/
233
+ unless name =~ /^(\w+)(::)?Config$/
233
234
  raise "Couldn't infer config name, please, specify it explicitly" \
234
235
  "via `config_name :my_config`"
235
236
  end
@@ -288,17 +289,14 @@ module Anyway # :nodoc:
288
289
  trace = Tracing.capture do
289
290
  Tracing.trace!(:defaults) { base_config }
290
291
 
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
- )
292
+ config_path = resolve_config_path(config_name, env_prefix)
293
+
294
+ load_from_sources(base_config, name: config_name, env_prefix: env_prefix, config_path: config_path)
297
295
 
298
296
  if overrides
299
297
  Tracing.trace!(:load) { overrides }
300
298
 
301
- base_config.deep_merge!(overrides)
299
+ Utils.deep_merge!(base_config, overrides)
302
300
  end
303
301
  end
304
302
 
@@ -321,7 +319,7 @@ module Anyway # :nodoc:
321
319
 
322
320
  def load_from_sources(base_config, **options)
323
321
  Anyway.loaders.each do |(_id, loader)|
324
- base_config.deep_merge!(loader.call(**options))
322
+ Utils.deep_merge!(base_config, loader.call(**options))
325
323
  end
326
324
  base_config
327
325
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module Rails
5
+ module Loaders
6
+ class YAML < Anyway::Loaders::YAML
7
+ def load_base_yml(*)
8
+ parsed_yml = super
9
+ return parsed_yml unless environmental?(parsed_yml)
10
+
11
+ super[::Rails.env] || {}
12
+ end
13
+
14
+ private
15
+
16
+ def environmental?(parsed_yml)
17
+ return true unless Settings.future.unwrap_known_environments
18
+ # likely
19
+ return true if parsed_yml.key?(::Rails.env)
20
+ # less likely
21
+ ::Rails.application.config.anyway_config.known_environments.any? { |_1| parsed_yml.key?(_1) }
22
+ end
23
+
24
+ def relative_config_path(path)
25
+ Pathname.new(path).relative_path_from(::Rails.root)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Use Settings name to not confuse with Config.
5
+ #
6
+ # Settings contain the library-wide configuration.
7
+ class Settings
8
+ # Future encapsulates settings that will be introduced in the upcoming version
9
+ # with the default values, which could break compatibility
10
+ class Future
11
+ class << self
12
+ def setting(name, default_value)
13
+ settings[name] = default_value
14
+
15
+ define_method(name) do
16
+ store[name]
17
+ end
18
+
19
+ define_method(:"#{name}=") do |val|
20
+ store[name] = val
21
+ end
22
+ end
23
+
24
+ def settings
25
+ @settings ||= {}
26
+ end
27
+ end
28
+
29
+ def initialize
30
+ @store = {}
31
+ end
32
+
33
+ def use(*names)
34
+ store.clear
35
+ names.each { |_1| store[_1] = self.class.settings[_1] }
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :store
41
+ end
42
+
43
+ class << self
44
+ # Define whether to load data from
45
+ # *.yml.local (or credentials/local.yml.enc)
46
+ attr_accessor :use_local_files
47
+
48
+ # A proc returning a path to YML config file given the config name
49
+ attr_reader :default_config_path
50
+
51
+ def default_config_path=(val)
52
+ if val.is_a?(Proc)
53
+ @default_config_path = val
54
+ return
55
+ end
56
+
57
+ val = val.to_s
58
+
59
+ @default_config_path = ->(name) { File.join(val, "#{name}.yml") }
60
+ end
61
+
62
+ # Enable source tracing
63
+ attr_accessor :tracing_enabled
64
+
65
+ def future
66
+ @future ||= Future.new
67
+ end
68
+ end
69
+
70
+ # By default, use local files only in development (that's the purpose if the local files)
71
+ self.use_local_files = (ENV["RACK_ENV"] == "development" || ENV["RAILS_ENV"] == "development")
72
+
73
+ # By default, consider configs are stored in the ./config folder
74
+ self.default_config_path = ->(name) { "./config/#{name}.yml" }
75
+
76
+ # Tracing is enabled by default
77
+ self.tracing_enabled = true
78
+ end
79
+ end