anyway_config 2.0.6 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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