anyway_config 2.0.2 → 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,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