anyway_config 2.0.0.pre2 → 2.0.0.rc1

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