anyway_config 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -13,9 +13,7 @@ module Anyway
13
13
  end
14
14
 
15
15
  refine Thread::Backtrace::Location do
16
- def path_lineno
17
- "#{path}:#{lineno}"
18
- end
16
+ def path_lineno() ; "#{path}:#{lineno}"; end
19
17
  end
20
18
  end)
21
19
 
@@ -34,13 +32,16 @@ module Anyway
34
32
  value.dig(*__rest__, &__block__)
35
33
  end
36
34
 
37
- def record_value(val, *path, key, **opts)
38
- trace =
39
- if val.is_a?(Hash)
40
- Trace.new.tap { |_1| _1.merge_values(val, **opts) }
41
- else
42
- Trace.new(:value, val, **opts)
43
- end
35
+ def record_value(val, *path, **opts)
36
+ key = path.pop
37
+ (__m__ = if val.is_a?(Hash)
38
+ Trace.new.tap { |_1|
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))
44
+
44
45
  target_trace = path.empty? ? self : value.dig(*path)
45
46
  target_trace.value[key.to_s] = trace
46
47
 
@@ -79,13 +80,9 @@ module Anyway
79
80
  value.keep_if(*__rest__, &__block__)
80
81
  end
81
82
 
82
- def clear
83
- value.clear
84
- end
83
+ def clear() ; value.clear; end
85
84
 
86
- def trace?
87
- type == :trace
88
- end
85
+ def trace?() ; type == :trace; end
89
86
 
90
87
  def to_h
91
88
  if trace?
@@ -95,9 +92,7 @@ module Anyway
95
92
  end
96
93
  end
97
94
 
98
- def dup
99
- self.class.new(type, value.dup, source)
100
- end
95
+ def dup() ; self.class.new(type, value.dup, **source); end
101
96
 
102
97
  def pretty_print(q)
103
98
  if trace?
@@ -146,11 +141,9 @@ module Anyway
146
141
  (Thread.current[:__anyway__trace_stack__] ||= [])
147
142
  end
148
143
 
149
- def current_trace
150
- trace_stack.last
151
- end
144
+ def current_trace() ; trace_stack.last; end
152
145
 
153
- alias tracing? current_trace
146
+ alias_method :tracing?, :current_trace
154
147
 
155
148
  def source_stack
156
149
  (Thread.current[:__anyway__trace_source_stack__] ||= [])
@@ -178,12 +171,11 @@ module Anyway
178
171
 
179
172
  def trace!(type, *path, **opts)
180
173
  return yield unless Tracing.tracing?
181
- source = {type: type}.merge(opts)
182
174
  val = yield
183
175
  if val.is_a?(Hash)
184
- Tracing.current_trace.merge_values(val, **source)
176
+ Tracing.current_trace.merge_values(val, type: type, **opts)
185
177
  else
186
- Tracing.current_trace.record_value(val, *path, **source)
178
+ Tracing.current_trace.record_value(val, *path, type: type, **opts)
187
179
  end
188
180
  val
189
181
  end
@@ -0,0 +1,391 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway/optparse_config"
4
+ require "anyway/dynamic_config"
5
+
6
+ module Anyway # :nodoc:
7
+ using RubyNext
8
+ using Anyway::Ext::DeepDup
9
+ using Anyway::Ext::DeepFreeze
10
+ using Anyway::Ext::Hash
11
+
12
+ using(Module.new do
13
+ refine Object do
14
+ def vm_object_id() ; (object_id << 1).to_s(16); end
15
+ end
16
+ end)
17
+
18
+ # Base config class
19
+ # Provides `attr_config` method to describe
20
+ # configuration parameters and set defaults
21
+ class Config
22
+ PARAM_NAME = /^[a-z_](\w+)?$/
23
+
24
+ # List of names that couldn't be used as config names
25
+ # (the class instance methods we use)
26
+ RESERVED_NAMES = %i[
27
+ config_name
28
+ env_prefix
29
+ values
30
+ class
31
+ clear
32
+ deconstruct_keys
33
+ dig
34
+ dup
35
+ initialize
36
+ load
37
+ load_from_sources
38
+ option_parser
39
+ pretty_print
40
+ raise_validation_error
41
+ reload
42
+ resolve_config_path
43
+ tap
44
+ to_h
45
+ to_source_trace
46
+ write_config_attr
47
+ ].freeze
48
+
49
+ class Error < StandardError; end
50
+
51
+ class ValidationError < Error; end
52
+
53
+ include OptparseConfig
54
+ include DynamicConfig
55
+
56
+ class BlockCallback
57
+ attr_reader :block
58
+
59
+ def initialize(block)
60
+ @block = block
61
+ end
62
+
63
+ def apply_to(config)
64
+ config.instance_exec(&block)
65
+ end
66
+ end
67
+
68
+ class NamedCallback
69
+ attr_reader :name
70
+
71
+ def initialize(name)
72
+ @name = name
73
+ end
74
+
75
+ def apply_to(config) ; config.send(name); end
76
+ end
77
+
78
+ class << self
79
+ def attr_config(*args, **hargs)
80
+ new_defaults = hargs.deep_dup
81
+ new_defaults.stringify_keys!
82
+
83
+ defaults.merge! new_defaults
84
+
85
+ new_keys = ((args + new_defaults.keys) - config_attributes)
86
+
87
+ validate_param_names! new_keys.map(&:to_s)
88
+
89
+ new_keys.map!(&:to_sym)
90
+
91
+ unless (reserved_names = (new_keys & RESERVED_NAMES)).empty?
92
+ raise ArgumentError, "Can not use the following reserved names as config attrubutes: " \
93
+ "#{reserved_names.sort.map(&:to_s).join(", ")}"
94
+ end
95
+
96
+ config_attributes.push(*new_keys)
97
+
98
+ define_config_accessor(*new_keys)
99
+
100
+ # Define predicate methods ("param?") for attributes
101
+ # having `true` or `false` as default values
102
+ new_defaults.each do |key, val|
103
+ next unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
104
+ alias_method :"#{key}?", :"#{key}"
105
+ end
106
+ end
107
+
108
+ def defaults
109
+ return @defaults if instance_variable_defined?(:@defaults)
110
+
111
+ @defaults = if superclass < Anyway::Config
112
+ superclass.defaults.deep_dup
113
+ else
114
+ new_empty_config
115
+ end
116
+ end
117
+
118
+ def config_attributes
119
+ return @config_attributes if instance_variable_defined?(:@config_attributes)
120
+
121
+ @config_attributes = if superclass < Anyway::Config
122
+ superclass.config_attributes.dup
123
+ else
124
+ []
125
+ end
126
+ end
127
+
128
+ def required(*names)
129
+ unless (unknown_names = (names - config_attributes)).empty?
130
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}"
131
+ end
132
+
133
+ required_attributes.push(*names)
134
+ end
135
+
136
+ def required_attributes
137
+ return @required_attributes if instance_variable_defined?(:@required_attributes)
138
+
139
+ @required_attributes = if superclass < Anyway::Config
140
+ superclass.required_attributes.dup
141
+ else
142
+ []
143
+ end
144
+ end
145
+
146
+ def on_load(*names, &block)
147
+ raise ArgumentError, "Either methods or block should be specified, not both" if block && !names.empty?
148
+
149
+ if block
150
+ load_callbacks << BlockCallback.new(block)
151
+ else
152
+ load_callbacks.push(*names.map { NamedCallback.new(_1) })
153
+ end
154
+ end
155
+
156
+ def load_callbacks
157
+ return @load_callbacks if instance_variable_defined?(:@load_callbacks)
158
+
159
+ @load_callbacks = if superclass <= Anyway::Config
160
+ superclass.load_callbacks.dup
161
+ else
162
+ []
163
+ end
164
+ end
165
+
166
+ def config_name(val = nil)
167
+ return (@explicit_config_name = val.to_s) unless val.nil?
168
+
169
+ return @config_name if instance_variable_defined?(:@config_name)
170
+
171
+ @config_name = explicit_config_name || build_config_name
172
+ end
173
+
174
+ def explicit_config_name
175
+ return @explicit_config_name if instance_variable_defined?(:@explicit_config_name)
176
+
177
+ @explicit_config_name =
178
+ if superclass.respond_to?(:explicit_config_name)
179
+ superclass.explicit_config_name
180
+ end
181
+ end
182
+
183
+ def explicit_config_name?() ; !explicit_config_name.nil?; end
184
+
185
+ def env_prefix(val = nil)
186
+ return (@env_prefix = val.to_s.upcase) unless val.nil?
187
+
188
+ return @env_prefix if instance_variable_defined?(:@env_prefix)
189
+
190
+ @env_prefix = if superclass < Anyway::Config && superclass.explicit_config_name?
191
+ superclass.env_prefix
192
+ else
193
+ config_name.upcase
194
+ end
195
+ end
196
+
197
+ def new_empty_config() ; {}; end
198
+
199
+ private
200
+
201
+ def define_config_accessor(*names)
202
+ names.each do |name|
203
+ accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
204
+ def #{name}=(val)
205
+ __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
206
+ values[:#{name}] = val
207
+ end
208
+
209
+ def #{name}
210
+ values[:#{name}]
211
+ end
212
+ RUBY
213
+ end
214
+ end
215
+
216
+ def accessors_module
217
+ return @accessors_module if instance_variable_defined?(:@accessors_module)
218
+
219
+ @accessors_module = Module.new.tap do |mod|
220
+ include mod
221
+ end
222
+ end
223
+
224
+ def build_config_name
225
+ unless name
226
+ raise "Please, specify config name explicitly for anonymous class " \
227
+ "via `config_name :my_config`"
228
+ end
229
+
230
+ # handle two cases:
231
+ # - SomeModule::Config => "some_module"
232
+ # - SomeConfig => "some"
233
+ unless name =~ /^(\w+)(::)?Config$/
234
+ raise "Couldn't infer config name, please, specify it explicitly" \
235
+ "via `config_name :my_config`"
236
+ end
237
+
238
+ Regexp.last_match[1].tap(&:downcase!)
239
+ end
240
+
241
+ def validate_param_names!(names)
242
+ invalid_names = names.reject { |name| name =~ PARAM_NAME }
243
+ return if invalid_names.empty?
244
+
245
+ raise ArgumentError, "Invalid attr_config name: #{invalid_names.join(", ")}.\n" \
246
+ "Valid names must satisfy /#{PARAM_NAME.source}/."
247
+ end
248
+ end
249
+
250
+ on_load :validate_required_attributes!
251
+
252
+ attr_reader :config_name, :env_prefix
253
+
254
+ # Instantiate config instance.
255
+ #
256
+ # Example:
257
+ #
258
+ # my_config = Anyway::Config.new()
259
+ #
260
+ # # provide some values explicitly
261
+ # my_config = Anyway::Config.new({some: :value})
262
+ #
263
+ def initialize(overrides = nil)
264
+ @config_name = self.class.config_name
265
+
266
+ raise ArgumentError, "Config name is missing" unless @config_name
267
+
268
+ @env_prefix = self.class.env_prefix
269
+ @values = {}
270
+
271
+ load(overrides)
272
+ end
273
+
274
+ def reload(overrides = nil)
275
+ clear
276
+ load(overrides)
277
+ self
278
+ end
279
+
280
+ def clear
281
+ values.clear
282
+ @__trace__ = nil
283
+ self
284
+ end
285
+
286
+ def load(overrides = nil)
287
+ base_config = self.class.defaults.deep_dup
288
+
289
+ trace = Tracing.capture do
290
+ Tracing.trace!(:defaults) { base_config }
291
+
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)
295
+
296
+ if overrides
297
+ Tracing.trace!(:load) { overrides }
298
+
299
+ Utils.deep_merge!(base_config, overrides)
300
+ end
301
+ end
302
+
303
+ base_config.each do |key, val|
304
+ write_config_attr(key.to_sym, val)
305
+ end
306
+
307
+ # Trace may contain unknown attributes
308
+ trace&.keep_if { |key| self.class.config_attributes.include?(key.to_sym) }
309
+
310
+ # Run on_load callbacks
311
+ self.class.load_callbacks.each { _1.apply_to(self) }
312
+
313
+ # Set trace after we write all the values to
314
+ # avoid changing the source to accessor
315
+ @__trace__ = trace
316
+
317
+ self
318
+ end
319
+
320
+ def load_from_sources(base_config, **options)
321
+ Anyway.loaders.each do |(_id, loader)|
322
+ Utils.deep_merge!(base_config, loader.call(**options))
323
+ end
324
+ base_config
325
+ end
326
+
327
+ def dig(*keys) ; values.dig(*keys); end
328
+
329
+ def to_h() ; values.deep_dup.deep_freeze; end
330
+
331
+ def dup
332
+ self.class.allocate.tap do |new_config|
333
+ %i[config_name env_prefix __trace__].each do |ivar|
334
+ new_config.instance_variable_set(:"@#{ivar}", send(ivar).dup)
335
+ end
336
+ new_config.instance_variable_set(:@values, values.deep_dup)
337
+ end
338
+ end
339
+
340
+ def resolve_config_path(name, env_prefix)
341
+ Anyway.env.fetch(env_prefix).delete("conf") || Settings.default_config_path.call(name)
342
+ end
343
+
344
+ def deconstruct_keys(keys) ; values.deconstruct_keys(keys); end
345
+
346
+ def to_source_trace() ; __trace__&.to_h; end
347
+
348
+ def inspect
349
+ "#<#{self.class}:0x#{vm_object_id.rjust(16, "0")} config_name=\"#{config_name}\" env_prefix=\"#{env_prefix}\" " \
350
+ "values=#{values.inspect}>"
351
+ end
352
+
353
+ def pretty_print(q)
354
+ q.object_group self do
355
+ q.nest(1) do
356
+ q.breakable
357
+ q.text "config_name=#{config_name.inspect}"
358
+ q.breakable
359
+ q.text "env_prefix=#{env_prefix.inspect}"
360
+ q.breakable
361
+ q.text "values:"
362
+ q.pp __trace__
363
+ end
364
+ end
365
+ end
366
+
367
+ private
368
+
369
+ attr_reader :values, :__trace__
370
+
371
+ def validate_required_attributes!
372
+ self.class.required_attributes.select do |name|
373
+ values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
374
+ end.then do |missing|
375
+ next if missing.empty?
376
+ raise_validation_error "The following config parameters are missing or empty: #{missing.join(", ")}"
377
+ end
378
+ end
379
+
380
+ def write_config_attr(key, val)
381
+ key = key.to_sym
382
+ return unless self.class.config_attributes.include?(key)
383
+
384
+ public_send(:"#{key}=", val)
385
+ end
386
+
387
+ def raise_validation_error(msg)
388
+ raise ValidationError, msg
389
+ end
390
+ end
391
+ end