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,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 { |_1| 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
@@ -0,0 +1,404 @@
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
15
+ (object_id << 1).to_s(16)
16
+ end
17
+ end
18
+ end)
19
+
20
+ # Base config class
21
+ # Provides `attr_config` method to describe
22
+ # configuration parameters and set defaults
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
+
52
+ include OptparseConfig
53
+ include DynamicConfig
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
+
79
+ class << self
80
+ def attr_config(*args, **hargs)
81
+ new_defaults = hargs.deep_dup
82
+ new_defaults.stringify_keys!
83
+
84
+ defaults.merge! new_defaults
85
+
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
+
97
+ config_attributes.push(*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
107
+ end
108
+
109
+ def defaults
110
+ return @defaults if instance_variable_defined?(:@defaults)
111
+
112
+ @defaults =
113
+ if superclass < Anyway::Config
114
+ superclass.defaults.deep_dup
115
+ else
116
+ new_empty_config
117
+ end
118
+ end
119
+
120
+ def config_attributes
121
+ return @config_attributes if instance_variable_defined?(:@config_attributes)
122
+
123
+ @config_attributes =
124
+ if superclass < Anyway::Config
125
+ superclass.config_attributes.dup
126
+ else
127
+ []
128
+ end
129
+ end
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 { |_1| 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
+
171
+ def config_name(val = nil)
172
+ return (@explicit_config_name = val.to_s) unless val.nil?
173
+
174
+ return @config_name if instance_variable_defined?(:@config_name)
175
+
176
+ @config_name = explicit_config_name || build_config_name
177
+ end
178
+
179
+ def explicit_config_name
180
+ return @explicit_config_name if instance_variable_defined?(:@explicit_config_name)
181
+
182
+ @explicit_config_name =
183
+ if superclass.respond_to?(:explicit_config_name)
184
+ superclass.explicit_config_name
185
+ end
186
+ end
187
+
188
+ def explicit_config_name?
189
+ !explicit_config_name.nil?
190
+ end
191
+
192
+ def env_prefix(val = nil)
193
+ return (@env_prefix = val.to_s.upcase) unless val.nil?
194
+
195
+ return @env_prefix if instance_variable_defined?(:@env_prefix)
196
+
197
+ @env_prefix =
198
+ if superclass < Anyway::Config && superclass.explicit_config_name?
199
+ superclass.env_prefix
200
+ else
201
+ config_name.upcase
202
+ end
203
+ end
204
+
205
+ def new_empty_config
206
+ {}
207
+ end
208
+
209
+ private
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
+
235
+ def build_config_name
236
+ unless name
237
+ raise "Please, specify config name explicitly for anonymous class " \
238
+ "via `config_name :my_config`"
239
+ end
240
+
241
+ # handle two cases:
242
+ # - SomeModule::Config => "some_module"
243
+ # - SomeConfig => "some"
244
+ unless name =~ /^(\w+)(\:\:)?Config$/
245
+ raise "Couldn't infer config name, please, specify it explicitly" \
246
+ "via `config_name :my_config`"
247
+ end
248
+
249
+ Regexp.last_match[1].tap(&:downcase!)
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
259
+ end
260
+
261
+ on_load :validate_required_attributes!
262
+
263
+ attr_reader :config_name, :env_prefix
264
+
265
+ # Instantiate config instance.
266
+ #
267
+ # Example:
268
+ #
269
+ # my_config = Anyway::Config.new()
270
+ #
271
+ # # provide some values explicitly
272
+ # my_config = Anyway::Config.new({some: :value})
273
+ #
274
+ def initialize(overrides = nil)
275
+ @config_name = self.class.config_name
276
+
277
+ raise ArgumentError, "Config name is missing" unless @config_name
278
+
279
+ @env_prefix = self.class.env_prefix
280
+ @values = {}
281
+
282
+ load(overrides)
283
+ end
284
+
285
+ def reload(overrides = nil)
286
+ clear
287
+ load(overrides)
288
+ self
289
+ end
290
+
291
+ def clear
292
+ values.clear
293
+ @__trace__ = nil
294
+ self
295
+ end
296
+
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 }
302
+
303
+ load_from_sources(
304
+ base_config,
305
+ name: config_name,
306
+ env_prefix: env_prefix,
307
+ config_path: resolve_config_path(config_name, env_prefix)
308
+ )
309
+
310
+ if overrides
311
+ Tracing.trace!(:load) { overrides }
312
+
313
+ base_config.deep_merge!(overrides)
314
+ end
315
+ end
316
+
317
+ base_config.each do |key, val|
318
+ write_config_attr(key.to_sym, val)
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| _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
332
+ end
333
+
334
+ def load_from_sources(base_config, **options)
335
+ Anyway.loaders.each do |(_id, loader)|
336
+ base_config.deep_merge!(loader.call(**options))
337
+ end
338
+ base_config
339
+ end
340
+
341
+ def dig(*keys)
342
+ values.dig(*keys)
343
+ end
344
+
345
+ def to_h
346
+ values.deep_dup.deep_freeze
347
+ end
348
+
349
+ def resolve_config_path(name, env_prefix)
350
+ Anyway.env.fetch(env_prefix).delete("conf") || Settings.default_config_path.call(name)
351
+ end
352
+
353
+ def deconstruct_keys(keys)
354
+ values.deconstruct_keys(keys)
355
+ end
356
+
357
+ def to_source_trace
358
+ __trace__&.to_h
359
+ end
360
+
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}>"
364
+ end
365
+
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
378
+ end
379
+
380
+ private
381
+
382
+ attr_reader :values, :__trace__
383
+
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
391
+ end
392
+
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)
398
+ end
399
+
400
+ def raise_validation_error(msg)
401
+ raise ValidationError, msg
402
+ end
403
+ end
404
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Anyway # :nodoc:
6
+ # Initializes the OptionParser instance using the given configuration
7
+ class OptionParserBuilder
8
+ class << self
9
+ def call(options)
10
+ OptionParser.new do |opts|
11
+ opts.accept(AutoCast) { |_1| AutoCast.call(_1) }
12
+
13
+ options.each do |key, descriptor|
14
+ opts.on(*option_parser_on_args(key, **descriptor)) do |val|
15
+ yield [key, val]
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def option_parser_on_args(key, flag: false, desc: nil, type: AutoCast)
24
+ on_args = ["--#{key.to_s.tr("_", "-")}#{flag ? "" : " VALUE"}"]
25
+ on_args << type unless flag
26
+ on_args << desc unless desc.nil?
27
+ on_args
28
+ end
29
+ end
30
+ end
31
+ end