anyway_config 2.0.0.pre2 → 2.0.0.rc1

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +350 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +444 -119
  5. data/lib/.rbnext/2.7/anyway/auto_cast.rb +33 -0
  6. data/lib/.rbnext/2.7/anyway/config.rb +404 -0
  7. data/lib/.rbnext/2.7/anyway/option_parser_builder.rb +31 -0
  8. data/lib/.rbnext/2.7/anyway/tracing.rb +187 -0
  9. data/lib/anyway/auto_cast.rb +33 -0
  10. data/lib/anyway/config.rb +235 -58
  11. data/lib/anyway/dynamic_config.rb +1 -1
  12. data/lib/anyway/env.rb +29 -19
  13. data/lib/anyway/ext/hash.rb +14 -6
  14. data/lib/anyway/loaders.rb +79 -0
  15. data/lib/anyway/loaders/base.rb +23 -0
  16. data/lib/anyway/loaders/env.rb +16 -0
  17. data/lib/anyway/loaders/yaml.rb +46 -0
  18. data/lib/anyway/option_parser_builder.rb +6 -3
  19. data/lib/anyway/optparse_config.rb +10 -6
  20. data/lib/anyway/rails.rb +16 -0
  21. data/lib/anyway/rails/config.rb +2 -69
  22. data/lib/anyway/rails/loaders.rb +5 -0
  23. data/lib/anyway/rails/loaders/credentials.rb +64 -0
  24. data/lib/anyway/rails/loaders/secrets.rb +39 -0
  25. data/lib/anyway/rails/loaders/yaml.rb +19 -0
  26. data/lib/anyway/rails/settings.rb +58 -0
  27. data/lib/anyway/railtie.rb +14 -2
  28. data/lib/anyway/settings.rb +29 -0
  29. data/lib/anyway/tracing.rb +187 -0
  30. data/lib/anyway/version.rb +1 -1
  31. data/lib/anyway_config.rb +27 -21
  32. data/lib/generators/anyway/app_config/USAGE +9 -0
  33. data/lib/generators/anyway/app_config/app_config_generator.rb +17 -0
  34. data/lib/generators/anyway/config/USAGE +13 -0
  35. data/lib/generators/anyway/config/config_generator.rb +46 -0
  36. data/lib/generators/anyway/config/templates/config.rb.tt +9 -0
  37. data/lib/generators/anyway/config/templates/config.yml.tt +13 -0
  38. data/lib/generators/anyway/install/USAGE +4 -0
  39. data/lib/generators/anyway/install/install_generator.rb +43 -0
  40. data/lib/generators/anyway/install/templates/application_config.rb.tt +17 -0
  41. metadata +75 -10
  42. data/lib/anyway/ext/string_serialize.rb +0 -38
  43. data/lib/anyway/loaders/env_loader.rb +0 -0
  44. data/lib/anyway/loaders/secrets_loader.rb +0 -0
  45. data/lib/anyway/loaders/yaml_loader.rb +0 -0
@@ -0,0 +1,187 @@
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
17
+ "#{path}:#{lineno}"
18
+ end
19
+ end
20
+ end)
21
+
22
+ class Trace
23
+ UNDEF = Object.new
24
+
25
+ attr_reader :type, :value, :source
26
+
27
+ def initialize(type = :trace, value = UNDEF, **source)
28
+ @type = type
29
+ @source = source
30
+ @value = value == UNDEF ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
31
+ end
32
+
33
+ def dig(*__rest__, &__block__)
34
+ value.dig(*__rest__, &__block__)
35
+ end
36
+
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
44
+ target_trace = path.empty? ? self : value.dig(*path)
45
+ target_trace.value[key.to_s] = trace
46
+
47
+ val
48
+ end
49
+
50
+ def merge_values(hash, **opts)
51
+ return hash unless hash
52
+
53
+ hash.each do |key, val|
54
+ if val.is_a?(Hash)
55
+ value[key.to_s].merge_values(val, **opts)
56
+ else
57
+ value[key.to_s] = Trace.new(:value, val, **opts)
58
+ end
59
+ end
60
+
61
+ hash
62
+ end
63
+
64
+ def merge!(another_trace)
65
+ raise ArgumentError, "You can only merge into a :trace type, and this is :#{type}" unless trace?
66
+ raise ArgumentError, "You can only merge a :trace type, but trying :#{type}" unless another_trace.trace?
67
+
68
+ another_trace.value.each do |key, sub_trace|
69
+ if sub_trace.trace?
70
+ value[key].merge! sub_trace
71
+ else
72
+ value[key] = sub_trace
73
+ end
74
+ end
75
+ end
76
+
77
+ def keep_if(*__rest__, &__block__)
78
+ raise ArgumentError, "You can only filter :trace type, and this is :#{type}" unless trace?
79
+ value.keep_if(*__rest__, &__block__)
80
+ end
81
+
82
+ def clear
83
+ value.clear
84
+ end
85
+
86
+ def trace?
87
+ type == :trace
88
+ end
89
+
90
+ def to_h
91
+ if trace?
92
+ value.transform_values(&:to_h).tap { |_1| _1.default_proc = nil }
93
+ else
94
+ {value: value, source: source}
95
+ end
96
+ end
97
+
98
+ def pretty_print(q)
99
+ if trace?
100
+ q.nest(2) do
101
+ q.breakable ""
102
+ q.seplist(value, nil, :each) do |k, v|
103
+ q.group do
104
+ q.text k
105
+ q.text " =>"
106
+ q.breakable " " unless v.trace?
107
+ q.pp v
108
+ end
109
+ end
110
+ end
111
+ else
112
+ q.pp value
113
+ q.group(0, " (", ")") do
114
+ q.seplist(source, lambda { q.breakable " " }, :each) do |k, v|
115
+ q.group do
116
+ q.text k.to_s
117
+ q.text "="
118
+ q.text v.to_s
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ class << self
127
+ def capture
128
+ unless Settings.tracing_enabled
129
+ yield
130
+ return
131
+ end
132
+
133
+ trace = Trace.new
134
+ trace_stack.push trace
135
+ yield
136
+ trace_stack.last
137
+ ensure
138
+ trace_stack.pop
139
+ end
140
+
141
+ def trace_stack
142
+ (Thread.current[:__anyway__trace_stack__] ||= [])
143
+ end
144
+
145
+ def current_trace
146
+ trace_stack.last
147
+ end
148
+
149
+ alias tracing? current_trace
150
+
151
+ def source_stack
152
+ (Thread.current[:__anyway__trace_source_stack__] ||= [])
153
+ end
154
+
155
+ def current_trace_source
156
+ source_stack.last || accessor_source(caller_locations(2, 1).first)
157
+ end
158
+
159
+ def with_trace_source(src)
160
+ source_stack << src
161
+ yield
162
+ ensure
163
+ source_stack.pop
164
+ end
165
+
166
+ private
167
+
168
+ def accessor_source(location)
169
+ {type: :accessor, called_from: location.path_lineno}
170
+ end
171
+ end
172
+
173
+ module_function
174
+
175
+ def trace!(type, *path, **opts)
176
+ return yield unless Tracing.tracing?
177
+ source = {type: type}.merge(opts)
178
+ val = yield
179
+ if val.is_a?(Hash)
180
+ Tracing.current_trace.merge_values(val, **source)
181
+ else
182
+ Tracing.current_trace.record_value(yield, *path, **source)
183
+ end
184
+ val
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,33 @@
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
+ def self.call(val)
11
+ return val unless String === val
12
+
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
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/anyway/config.rb CHANGED
@@ -3,24 +3,79 @@
3
3
  require "anyway/optparse_config"
4
4
  require "anyway/dynamic_config"
5
5
 
6
- require "anyway/ext/deep_dup"
7
- require "anyway/ext/deep_freeze"
8
- require "anyway/ext/hash"
9
- require "anyway/ext/string_serialize"
10
-
11
6
  module Anyway # :nodoc:
7
+ using RubyNext
12
8
  using Anyway::Ext::DeepDup
13
9
  using Anyway::Ext::DeepFreeze
14
10
  using Anyway::Ext::Hash
15
- using Anyway::Ext::StringSerialize
11
+
12
+ using(Module.new do
13
+ refine Object do
14
+ def vm_object_id
15
+ (object_id << 1).to_s(16)
16
+ end
17
+ end
18
+ end)
16
19
 
17
20
  # Base config class
18
21
  # Provides `attr_config` method to describe
19
22
  # configuration parameters and set defaults
20
23
  class Config
24
+ PARAM_NAME = /^[a-z_]([\w]+)?$/
25
+
26
+ # List of names that couldn't be used as config names
27
+ # (the class instance methods we use)
28
+ RESERVED_NAMES = %i[
29
+ config_name
30
+ env_prefix
31
+ values
32
+ class
33
+ clear
34
+ deconstruct_keys
35
+ dig
36
+ initialize
37
+ load
38
+ load_from_sources
39
+ option_parser
40
+ pretty_print
41
+ raise_validation_error
42
+ reload
43
+ resolve_config_path
44
+ to_h
45
+ to_source_trace
46
+ write_config_attr
47
+ ].freeze
48
+
49
+ class Error < StandardError; end
50
+ class ValidationError < Error; end
51
+
21
52
  include OptparseConfig
22
53
  include DynamicConfig
23
54
 
55
+ class BlockCallback
56
+ attr_reader :block
57
+
58
+ def initialize(block)
59
+ @block = block
60
+ end
61
+
62
+ def apply_to(config)
63
+ config.instance_exec(&block)
64
+ end
65
+ end
66
+
67
+ class NamedCallback
68
+ attr_reader :name
69
+
70
+ def initialize(name)
71
+ @name = name
72
+ end
73
+
74
+ def apply_to(config)
75
+ config.send(name)
76
+ end
77
+ end
78
+
24
79
  class << self
25
80
  def attr_config(*args, **hargs)
26
81
  new_defaults = hargs.deep_dup
@@ -28,9 +83,27 @@ module Anyway # :nodoc:
28
83
 
29
84
  defaults.merge! new_defaults
30
85
 
31
- new_keys = (args + new_defaults.keys) - config_attributes
86
+ new_keys = ((args + new_defaults.keys) - config_attributes)
87
+
88
+ validate_param_names! new_keys.map(&:to_s)
89
+
90
+ new_keys.map!(&:to_sym)
91
+
92
+ unless (reserved_names = (new_keys & RESERVED_NAMES)).empty?
93
+ raise ArgumentError, "Can not use the following reserved names as config attrubutes: " \
94
+ "#{reserved_names.sort.map(&:to_s).join(", ")}"
95
+ end
96
+
32
97
  config_attributes.push(*new_keys)
33
- attr_accessor(*new_keys)
98
+
99
+ define_config_accessor(*new_keys)
100
+
101
+ # Define predicate methods ("param?") for attributes
102
+ # having `true` or `false` as default values
103
+ new_defaults.each do |key, val|
104
+ next unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
105
+ alias_method :"#{key}?", :"#{key}"
106
+ end
34
107
  end
35
108
 
36
109
  def defaults
@@ -40,7 +113,7 @@ module Anyway # :nodoc:
40
113
  if superclass < Anyway::Config
41
114
  superclass.defaults.deep_dup
42
115
  else
43
- {}
116
+ new_empty_config
44
117
  end
45
118
  end
46
119
 
@@ -55,6 +128,46 @@ module Anyway # :nodoc:
55
128
  end
56
129
  end
57
130
 
131
+ def required(*names)
132
+ unless (unknown_names = (names - config_attributes)).empty?
133
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}"
134
+ end
135
+
136
+ required_attributes.push(*names)
137
+ end
138
+
139
+ def required_attributes
140
+ return @required_attributes if instance_variable_defined?(:@required_attributes)
141
+
142
+ @required_attributes =
143
+ if superclass < Anyway::Config
144
+ superclass.required_attributes.dup
145
+ else
146
+ []
147
+ end
148
+ end
149
+
150
+ def on_load(*names, &block)
151
+ raise ArgumentError, "Either methods or block should be specified, not both" if block_given? && !names.empty?
152
+
153
+ if block_given?
154
+ load_callbacks << BlockCallback.new(block)
155
+ else
156
+ load_callbacks.push(*names.map { NamedCallback.new(_1) })
157
+ end
158
+ end
159
+
160
+ def load_callbacks
161
+ return @load_callbacks if instance_variable_defined?(:@load_callbacks)
162
+
163
+ @load_callbacks =
164
+ if superclass <= Anyway::Config
165
+ superclass.load_callbacks.dup
166
+ else
167
+ []
168
+ end
169
+ end
170
+
58
171
  def config_name(val = nil)
59
172
  return (@explicit_config_name = val.to_s) unless val.nil?
60
173
 
@@ -89,8 +202,36 @@ module Anyway # :nodoc:
89
202
  end
90
203
  end
91
204
 
205
+ def new_empty_config
206
+ {}
207
+ end
208
+
92
209
  private
93
210
 
211
+ def define_config_accessor(*names)
212
+ names.each do |name|
213
+ accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
214
+ def #{name}=(val)
215
+ __trace__&.record_value(val, \"#{name}\", Tracing.current_trace_source)
216
+ # DEPRECATED: instance variable set will be removed in 2.1
217
+ @#{name} = values[:#{name}] = val
218
+ end
219
+
220
+ def #{name}
221
+ values[:#{name}]
222
+ end
223
+ RUBY
224
+ end
225
+ end
226
+
227
+ def accessors_module
228
+ return @accessors_module if instance_variable_defined?(:@accessors_module)
229
+
230
+ @accessors_module = Module.new.tap do |mod|
231
+ include mod
232
+ end
233
+ end
234
+
94
235
  def build_config_name
95
236
  unless name
96
237
  raise "Please, specify config name explicitly for anonymous class " \
@@ -107,8 +248,18 @@ module Anyway # :nodoc:
107
248
 
108
249
  Regexp.last_match[1].tap(&:downcase!)
109
250
  end
251
+
252
+ def validate_param_names!(names)
253
+ invalid_names = names.reject { |name| name =~ PARAM_NAME }
254
+ return if invalid_names.empty?
255
+
256
+ raise ArgumentError, "Invalid attr_config name: #{invalid_names.join(", ")}.\n" \
257
+ "Valid names must satisfy /#{PARAM_NAME.source}/."
258
+ end
110
259
  end
111
260
 
261
+ on_load :validate_required_attributes!
262
+
112
263
  attr_reader :config_name, :env_prefix
113
264
 
114
265
  # Instantiate config instance.
@@ -120,108 +271,134 @@ module Anyway # :nodoc:
120
271
  # # provide some values explicitly
121
272
  # my_config = Anyway::Config.new({some: :value})
122
273
  #
123
- def initialize(overrides = {})
274
+ def initialize(overrides = nil)
124
275
  @config_name = self.class.config_name
125
276
 
126
277
  raise ArgumentError, "Config name is missing" unless @config_name
127
278
 
128
279
  @env_prefix = self.class.env_prefix
280
+ @values = {}
129
281
 
130
282
  load(overrides)
131
283
  end
132
284
 
133
- def reload(overrides = {})
285
+ def reload(overrides = nil)
134
286
  clear
135
287
  load(overrides)
136
288
  self
137
289
  end
138
290
 
139
291
  def clear
140
- self.class.config_attributes.each do |attr|
141
- send("#{attr}=", nil)
142
- end
292
+ values.clear
293
+ @__trace__ = nil
143
294
  self
144
295
  end
145
296
 
146
- def load(overrides = {})
147
- base_config = (self.class.defaults || {}).deep_dup
297
+ def load(overrides = nil)
298
+ base_config = self.class.defaults.deep_dup
299
+
300
+ trace = Tracing.capture do
301
+ Tracing.trace!(:defaults) { base_config }
148
302
 
149
- base_config.deep_merge!(
150
303
  load_from_sources(
304
+ base_config,
151
305
  name: config_name,
152
306
  env_prefix: env_prefix,
153
307
  config_path: resolve_config_path(config_name, env_prefix)
154
308
  )
155
- )
156
309
 
157
- base_config.merge!(overrides) unless overrides.nil?
310
+ if overrides
311
+ Tracing.trace!(:load) { overrides }
312
+
313
+ base_config.deep_merge!(overrides)
314
+ end
315
+ end
158
316
 
159
317
  base_config.each do |key, val|
160
- set_value(key, val)
318
+ write_config_attr(key.to_sym, val)
161
319
  end
320
+
321
+ # Trace may contain unknown attributes
322
+ trace&.keep_if { |key| self.class.config_attributes.include?(key.to_sym) }
323
+
324
+ # Run on_load callbacks
325
+ self.class.load_callbacks.each { _1.apply_to(self) }
326
+
327
+ # Set trace after we write all the values to
328
+ # avoid changing the source to accessor
329
+ @__trace__ = trace
330
+
331
+ self
162
332
  end
163
333
 
164
- def load_from_sources(**options)
165
- base_config = {}
166
- each_source(options) do |config|
167
- base_config.deep_merge!(config) if config
334
+ def load_from_sources(base_config, **options)
335
+ Anyway.loaders.each do |(_id, loader)|
336
+ base_config.deep_merge!(loader.call(**options))
168
337
  end
169
338
  base_config
170
339
  end
171
340
 
172
- def each_source(options)
173
- yield load_from_file(options)
174
- yield load_from_env(options)
341
+ def dig(*keys)
342
+ values.dig(*keys)
175
343
  end
176
344
 
177
- def load_from_file(name:, env_prefix:, config_path:, **_options)
178
- file_config = load_from_yml(config_path)
345
+ def to_h
346
+ values.deep_dup.deep_freeze
347
+ end
179
348
 
180
- if Anyway::Settings.use_local_files
181
- local_config_path = config_path.sub(/\.yml/, ".local.yml")
182
- file_config.deep_merge!(load_from_yml(local_config_path))
183
- end
349
+ def resolve_config_path(name, env_prefix)
350
+ Anyway.env.fetch(env_prefix).delete("conf") || Settings.default_config_path.call(name)
351
+ end
184
352
 
185
- file_config
353
+ def deconstruct_keys(keys)
354
+ values.deconstruct_keys(keys)
186
355
  end
187
356
 
188
- def load_from_env(name:, env_prefix:, **_options)
189
- Anyway.env.fetch(env_prefix)
357
+ def to_source_trace
358
+ __trace__&.to_h
190
359
  end
191
360
 
192
- def to_h
193
- self.class.config_attributes.each_with_object({}) do |key, obj|
194
- obj[key.to_sym] = send(key)
195
- end.deep_dup.deep_freeze
361
+ def inspect
362
+ "#<#{self.class}:0x#{vm_object_id.rjust(16, "0")} config_name=\"#{config_name}\" env_prefix=\"#{env_prefix}\" " \
363
+ "values=#{values.inspect}>"
196
364
  end
197
365
 
198
- def resolve_config_path(name, env_prefix)
199
- Anyway.env.fetch(env_prefix).delete("conf") || default_config_path(name)
366
+ def pretty_print(q)
367
+ q.object_group self do
368
+ q.nest(1) do
369
+ q.breakable
370
+ q.text "config_name=#{config_name.inspect}"
371
+ q.breakable
372
+ q.text "env_prefix=#{env_prefix.inspect}"
373
+ q.breakable
374
+ q.text "values:"
375
+ q.pp __trace__
376
+ end
377
+ end
200
378
  end
201
379
 
202
380
  private
203
381
 
204
- def set_value(key, val)
205
- send("#{key}=", val) if respond_to?(key)
206
- end
207
-
208
- def load_from_yml(path)
209
- return {} unless File.file?(path)
382
+ attr_reader :values, :__trace__
210
383
 
211
- parse_yml(path)
384
+ def validate_required_attributes!
385
+ self.class.required_attributes.select do |name|
386
+ values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
387
+ end.then do |missing|
388
+ next if missing.empty?
389
+ raise_validation_error "The following config parameters are missing or empty: #{missing.join(", ")}"
390
+ end
212
391
  end
213
392
 
214
- def default_config_path(name)
215
- "./config/#{name}.yml"
393
+ def write_config_attr(key, val)
394
+ key = key.to_sym
395
+ return unless self.class.config_attributes.include?(key)
396
+
397
+ public_send(:"#{key}=", val)
216
398
  end
217
399
 
218
- def parse_yml(path)
219
- require "yaml"
220
- if defined?(ERB)
221
- YAML.safe_load(ERB.new(File.read(path)).result, [], [], true)
222
- else
223
- YAML.load_file(path)
224
- end
400
+ def raise_validation_error(msg)
401
+ raise ValidationError, msg
225
402
  end
226
403
  end
227
404
  end