complex_config 0.22.3 → 0.23.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.
@@ -6,17 +6,51 @@ require 'mize'
6
6
  require 'tins/xt/secure_write'
7
7
  require 'tins/xt/ask_and_send'
8
8
 
9
+ # A provider class that manages configuration loading, caching, and access
10
+ #
11
+ # The Provider class serves as the central hub for accessing and managing
12
+ # configuration data within the ComplexConfig system. It handles loading
13
+ # configuration files from disk, applying environment-specific settings,
14
+ # processing plugins for dynamic attribute resolution, and providing memoized
15
+ # access to configuration data through caching mechanisms.
16
+ #
17
+ # @see ComplexConfig::Config
18
+ # @see ComplexConfig::Settings
19
+ # @see ComplexConfig::Proxy
20
+ # @see ComplexConfig::KeySource
21
+ # @see ComplexConfig::Encryption
9
22
  class ComplexConfig::Provider
10
23
  include Tins::SexySingleton
11
24
  include ComplexConfig::Provider::Shortcuts
12
25
 
26
+ # Initializes a new provider instance with default settings
27
+ #
28
+ # Sets up the provider with an empty plugins collection and enables deep
29
+ # freezing by default to ensure configuration immutability.
13
30
  def initialize
14
31
  @plugins = Set.new
15
32
  @deep_freeze = true
16
33
  end
17
34
 
35
+ # The master_key_pathname= method sets the pathname for the master encryption
36
+ # key file
37
+ #
38
+ # This setter method allows configuring the location of the master key file
39
+ # that will be used for encryption and decryption operations throughout the
40
+ # system
41
+ #
42
+ # @attr_writer [String, nil] pathname The path to the master key file, or nil to reset
18
43
  attr_writer :master_key_pathname
19
44
 
45
+ # The master_key_pathname method retrieves the configured master key file
46
+ # path
47
+ #
48
+ # This method returns the explicitly set master key pathname if one has been
49
+ # configured, otherwise it defaults to a 'master.key' file within the
50
+ # configuration directory
51
+ #
52
+ # @return [String, Pathname] the path to the master key file or a Pathname object
53
+ # representing the default location in the config directory
20
54
  def master_key_pathname
21
55
  if @master_key_pathname
22
56
  @master_key_pathname
@@ -25,18 +59,50 @@ class ComplexConfig::Provider
25
59
  end
26
60
  end
27
61
 
62
+ # The configure_with method applies the given configuration to this provider
63
+ # instance
64
+ #
65
+ # This method takes a configuration object and applies its settings to the
66
+ # current provider instance It then flushes the configuration cache to ensure
67
+ # the changes take effect immediately
68
+ #
69
+ # @param config [ComplexConfig::Config] the configuration object to apply to
70
+ # this provider
71
+ # @return [self] returns self for chaining operations
28
72
  def configure_with(config)
29
73
  config.configure(self)
30
74
  flush_cache
31
75
  end
32
76
 
77
+ # The plugins attribute reader provides access to the set of plugins
78
+ # registered with this provider
79
+ #
80
+ # @return [Set<Proc>] the set containing all registered plugin procs
33
81
  attr_reader :plugins
34
82
 
83
+ # The add_plugin method adds a new plugin to the provider's collection of
84
+ # plugins
85
+ #
86
+ # This method registers a plugin proc with the provider, allowing it to be
87
+ # executed when configuration attributes are accessed and no direct value is
88
+ # found
89
+ #
90
+ # @param plugin [Proc] The plugin proc to add to the provider's plugins set
91
+ # @return [self] Returns self to allow for method chaining
35
92
  def add_plugin(plugin)
36
93
  plugins.add plugin
37
94
  self
38
95
  end
39
96
 
97
+ # The deep_freeze= method sets the deep freezing behavior for configuration
98
+ # objects
99
+ #
100
+ # This method configures whether configuration settings should be deeply
101
+ # frozen to prevent modification after initialization. When disabling deep
102
+ # freezing, it clears the configuration cache to ensure changes take effect
103
+ # immediately.
104
+ #
105
+ # @attr_writer [Boolean] flag true to enable deep freezing, false to disable it
40
106
  def deep_freeze=(flag)
41
107
  if @deep_freeze && !flag
42
108
  mize_cache_clear
@@ -44,10 +110,31 @@ class ComplexConfig::Provider
44
110
  @deep_freeze = flag
45
111
  end
46
112
 
113
+ # The deep_freeze? method checks whether deep freezing is enabled for
114
+ # configuration objects
115
+ #
116
+ # This method returns a boolean value indicating whether the provider has
117
+ # been configured to deeply freeze configuration settings, preventing
118
+ # modification after initialization
119
+ #
120
+ # @return [TrueClass, FalseClass] true if deep freezing is enabled, false
121
+ # otherwise
47
122
  def deep_freeze?
48
123
  !!@deep_freeze
49
124
  end
50
125
 
126
+ # The apply_plugins method executes registered plugins for a given setting
127
+ # and ID
128
+ #
129
+ # This method iterates through all registered plugins and attempts to execute
130
+ # each one with the provided setting and ID. It uses catch/throw to handle
131
+ # plugin skipping behavior, returning the first non-skipped plugin result.
132
+ #
133
+ # @param setting [ComplexConfig::Settings] The settings object to apply
134
+ # plugins to
135
+ # @param id [Object] The identifier used when executing plugins
136
+ # @return [Object, nil] The result from the first applicable plugin, or nil
137
+ # if no plugin applies
51
138
  def apply_plugins(setting, id)
52
139
  plugins.find do |plugin|
53
140
  catch :skip do
@@ -57,6 +144,15 @@ class ComplexConfig::Provider
57
144
  end
58
145
  end
59
146
 
147
+ # The config_dir= method sets the configuration directory path for the
148
+ # provider
149
+ #
150
+ # This setter method assigns a new configuration directory path to the
151
+ # provider instance, allowing it to locate configuration files at the
152
+ # specified location
153
+ #
154
+ # @attr_writer [String, nil] dir The path to the configuration directory, or nil to
155
+ # reset to default
60
156
  def config_dir=(dir)
61
157
  if dir.nil?
62
158
  @config_dir = nil
@@ -65,23 +161,79 @@ class ComplexConfig::Provider
65
161
  end
66
162
  end
67
163
 
164
+ # The config_dir method retrieves the configuration directory path
165
+ #
166
+ # This method returns the configured configuration directory path, falling
167
+ # back to a default location based on Rails root or the current working
168
+ # directory when no explicit path is set
169
+ #
170
+ # @return [Pathname] the configuration directory path
68
171
  def config_dir
69
172
  @config_dir || (defined?(Rails) && Rails.respond_to?(:root) && Rails.root || Pathname.pwd) + 'config'
70
173
  end
71
174
 
175
+ # The pathname method constructs a file path for a configuration file
176
+ #
177
+ # This method takes a configuration name and returns the full path to the
178
+ # corresponding YAML configuration file by combining the configuration
179
+ # directory with the name and file extension.
180
+ #
181
+ # @param name [String] the name of the configuration file (without extension)
182
+ # @return [Pathname] the full path to the configuration file with .yml extension
72
183
  def pathname(name)
73
184
  config_dir + "#{name}.yml"
74
185
  end
75
186
 
187
+ # The decrypt_config method retrieves decrypted configuration data from an
188
+ # encrypted file
189
+ #
190
+ # @param pathname [String, Pathname] the path to the encrypted configuration
191
+ # file
192
+ #
193
+ # @return [String, nil] the decrypted configuration content if successful, or
194
+ # nil if decryption fails
195
+ #
196
+ # @see decrypt_config_case for the internal implementation that handles the
197
+ # actual decryption logic
76
198
  def decrypt_config(pathname)
77
199
  decrypt_config_case(pathname).first
78
200
  end
79
201
 
202
+ # The encrypt_config method encrypts configuration data using a key source
203
+ #
204
+ # @param pathname [String, Pathname] the path to the configuration file to be
205
+ # encrypted
206
+ # @param config [Object] the configuration object to encrypt
207
+ # @return [String] the base64-encoded encrypted string including the
208
+ # encrypted data, initialization vector, and authentication tag separated by
209
+ # '--'
80
210
  def encrypt_config(pathname, config)
81
211
  ks = key_source(pathname)
82
212
  ComplexConfig::Encryption.new(ks.key_bytes).encrypt(config)
83
213
  end
84
214
 
215
+ # The config method reads and processes configuration data from a file
216
+ #
217
+ # This method loads configuration data from the specified file path, handling
218
+ # both plain YAML files and encrypted YAML files. It processes the
219
+ # configuration data through ERB evaluation, parses it into Ruby objects, and
220
+ # builds a Settings object with appropriate environment-specific values.
221
+ #
222
+ # @param pathname [String, Pathname] The path to the configuration file to read
223
+ # @param name [String, nil] The name to use when building the Settings object,
224
+ # or nil to derive it from the filename
225
+ # @return [ComplexConfig::Settings] A Settings object containing the parsed
226
+ # configuration data with environment-specific values
227
+ # @raise [ComplexConfig::ConfigurationFileMissing] If the configuration file
228
+ # cannot be found and no decrypted data is available
229
+ # @raise [ComplexConfig::EncryptionKeyMissing] If an encrypted configuration
230
+ # file is encountered but no encryption key is available
231
+ # @raise [ComplexConfig::ConfigurationSyntaxError] If the YAML syntax in the
232
+ # configuration file is invalid
233
+ # @raise [TypeError] If the configuration data cannot be converted to a hash
234
+ # @see decrypt_config_case for decryption logic
235
+ # @see evaluate for ERB processing
236
+ # @see ComplexConfig::Settings.build for Settings object construction
85
237
  def config(pathname, name = nil)
86
238
  datas = []
87
239
  path_exist = File.exist?(pathname)
@@ -124,11 +276,40 @@ class ComplexConfig::Provider
124
276
  raise ComplexConfig::ComplexConfigError.wrap(:ConfigurationSyntaxError, e)
125
277
  end
126
278
 
279
+ # The [] method provides access to configuration data by name
280
+ #
281
+ # This method serves as a shortcut for retrieving configuration settings by
282
+ # their name, delegating to the config method with the appropriate pathname
283
+ # and name parameters
284
+ #
285
+ # @param name [String] the name of the configuration to retrieve
286
+ # @return [ComplexConfig::Settings] the configuration settings object for the
287
+ # specified name
127
288
  def [](name)
128
289
  config pathname(name), name
129
290
  end
130
291
  memoize method: :[]
131
292
 
293
+ # The write_config method writes configuration data to a file
294
+ #
295
+ # This method handles writing configuration data to either a plain YAML file
296
+ # or an encrypted file depending on the encryption settings. It supports
297
+ # storing encryption keys alongside the encrypted configuration file and
298
+ # provides options for specifying the encryption key source.
299
+ #
300
+ # @param name [String, ComplexConfig::Settings] The name of the configuration
301
+ # to write or a Settings object
302
+ # @param value [Object, nil] The configuration value to write, required when
303
+ # name is a string
304
+ # @param encrypt [Boolean, Symbol, String] Whether to encrypt the
305
+ # configuration, accepts :random, true, or a hex key string
306
+ # @param store_key [Boolean] Whether to store the encryption key in a
307
+ # separate file
308
+ #
309
+ # @return [String, Boolean] The encryption key if stored, otherwise true
310
+ # @see prepare_output for data formatting
311
+ # @see provide_key_source for key management
312
+ # @see flush_cache to clear the configuration cache after writing
132
313
  def write_config(name, value: nil, encrypt: false, store_key: false)
133
314
  name, value = interpret_name_value(name, value)
134
315
  config_pathname = pathname(name).to_s
@@ -153,29 +334,81 @@ class ComplexConfig::Provider
153
334
  flush_cache
154
335
  end
155
336
 
337
+ # The prepare_output method converts a value into YAML format
338
+ #
339
+ # This method takes a value and transforms it into a YAML string
340
+ # representation by first converting it to a hash with string keys and then
341
+ # serializing it as YAML
342
+ #
343
+ # @param value [Object] the value to convert to YAML format
344
+ # @return [String] the YAML representation of the value
156
345
  def prepare_output(value)
157
346
  value.each_with_object({}) do |(k, v), h|
158
347
  h[k.to_s] = v
159
348
  end.to_yaml
160
349
  end
161
350
 
351
+ # The exist? method checks whether a configuration file exists and is
352
+ # accessible
353
+ #
354
+ # @param name [String] the name of the configuration to check for existence
355
+ # @return [TrueClass, FalseClass] true if the configuration file exists and
356
+ # can be loaded, false otherwise
357
+ # @see config for the underlying configuration loading logic
358
+ # @see ComplexConfig::ConfigurationFileMissing when a configuration file is missing
359
+ # @see ComplexConfig::EncryptionKeyMissing when an encryption key is required but not available
162
360
  def exist?(name)
163
361
  !!config(pathname(name), name)
164
362
  rescue ComplexConfig::ConfigurationFileMissing, ComplexConfig::EncryptionKeyMissing
165
363
  false
166
364
  end
167
365
 
366
+ # The proxy method creates and returns a new proxy object for dynamic
367
+ # configuration access
368
+ #
369
+ # This method instantiates a ComplexConfig::Proxy object that defers
370
+ # configuration loading until a method is first called. The proxy supports
371
+ # environment-specific configuration lookups and can handle both direct
372
+ # configuration access and existence checks.
373
+ #
374
+ # @param env [String, nil] The environment name to use for configuration lookups,
375
+ # defaults to nil which will use the default environment
376
+ # @return [ComplexConfig::Proxy] A new proxy object for dynamic configuration
377
+ # access
168
378
  def proxy(env = nil)
169
379
  ComplexConfig::Proxy.new(env)
170
380
  end
171
381
  memoize method: :proxy
172
382
 
383
+ # The flush_cache method clears the configuration cache and returns the
384
+ # receiver
385
+ #
386
+ # This method invalidates the cached configuration data stored in the
387
+ # provider, ensuring that subsequent configuration accesses will reload the
388
+ # data from source. It is typically used during development when
389
+ # configuration files may have changed or when explicit cache invalidation is
390
+ # required.
391
+ #
392
+ # @return [ComplexConfig::Provider] the provider instance itself for chaining operations
173
393
  def flush_cache
174
394
  mize_cache_clear
175
395
  self
176
396
  end
177
397
 
178
398
  if RUBY_VERSION >= "3"
399
+ # The evaluate method processes ERB template data and returns the result
400
+ #
401
+ # This method takes raw configuration data that may contain ERB templating
402
+ # syntax and evaluates it using Ruby's built-in ERB processor. It sets up
403
+ # the ERB environment with appropriate trim mode and filename for proper
404
+ # error reporting before executing the template.
405
+ #
406
+ # @param pathname [String, Pathname] The path to the file being evaluated,
407
+ # used for error reporting and debugging purposes
408
+ # @param data [String] The raw configuration data string that may contain
409
+ # ERB templating syntax to be processed
410
+ # @return [String] The processed configuration data with all ERB templates
411
+ # evaluated and replaced with their actual values
179
412
  def evaluate(pathname, data)
180
413
  erb = ::ERB.new(data, trim_mode: '-')
181
414
  erb.filename = pathname.to_s
@@ -189,14 +422,42 @@ class ComplexConfig::Provider
189
422
  end
190
423
  end
191
424
 
425
+ # The env method retrieves the current environment name for configuration
426
+ # lookups
427
+ #
428
+ # This method determines the appropriate environment to use for configuration
429
+ # access by checking various possible sources in order: an explicitly set
430
+ # instance variable, Rails environment if available, the RAILS_ENV
431
+ # environment variable, or falling back to 'development' as the default
432
+ #
433
+ # @return [String] the name of the current environment
192
434
  def env
193
435
  @env || defined?(Rails) && Rails.respond_to?(:env) && Rails.env ||
194
436
  ENV['RAILS_ENV'] ||
195
437
  'development'
196
438
  end
197
439
 
440
+ # The env= method sets the environment name for configuration lookups
441
+ #
442
+ # This setter method assigns a new environment value to the provider
443
+ # instance, which will be used when accessing configuration data that
444
+ # supports environment-specific values.
445
+ #
446
+ # @attr_writer [String, nil] env The environment name to use for configuration
447
+ # lookups, or nil to reset to the default environment
198
448
  attr_writer :env
199
449
 
450
+ # The key_source method retrieves an encryption key from configured sources
451
+ #
452
+ # This method attempts to find a valid encryption key by checking multiple
453
+ # possible sources in a specific order until one provides a usable key. It
454
+ # prioritizes keys from instance variables, file paths, environment
455
+ # variables, and master key files.
456
+ #
457
+ # @param pathname [String, nil] The path to a configuration file that may
458
+ # contain a key
459
+ # @return [ComplexConfig::KeySource, nil] A KeySource object containing the
460
+ # first valid key found, or nil if no key is available
200
461
  def key_source(pathname = nil)
201
462
  [
202
463
  ComplexConfig::KeySource.new(var: @key),
@@ -207,16 +468,49 @@ class ComplexConfig::Provider
207
468
  ].find(&:key)
208
469
  end
209
470
 
471
+ # The key method retrieves an encryption key from configured sources
472
+ #
473
+ # This method obtains an encryption key by delegating to the key_source
474
+ # method with the provided pathname, then extracts the actual key value from
475
+ # the returned KeySource object
476
+ #
477
+ # @param pathname [String, nil] The path to a configuration file that may
478
+ # contain a key
479
+ # @return [String, nil] The encryption key as a string if found, or nil if no
480
+ # key is available
210
481
  def key(pathname = nil)
211
482
  key_source(pathname).ask_and_send(:key)
212
483
  end
213
484
 
485
+ # The key= method sets the encryption key for the provider
486
+ #
487
+ # This setter method assigns a new encryption key value to the provider
488
+ # instance, which will be used for encrypting and decrypting configuration
489
+ # data.
490
+ #
491
+ # @attr_writer [String, nil] key The encryption key to use, or nil to clear the key
214
492
  attr_writer :key
215
493
 
494
+ # The new_key method generates a random encryption key
495
+ #
496
+ # This method creates a secure random key suitable for encryption purposes by
497
+ # generating a hexadecimal string of 32 characters (16 bytes).
498
+ #
499
+ # @return [String] a randomly generated hexadecimal encryption key
216
500
  def new_key
217
501
  SecureRandom.hex(16)
218
502
  end
219
503
 
504
+ # The valid_key? method checks whether a given key is valid for encryption
505
+ # purposes
506
+ #
507
+ # This method attempts to validate an encryption key by creating a KeySource
508
+ # object with the provided key and then trying to instantiate an Encryption
509
+ # object with it to verify the key's format and validity
510
+ #
511
+ # @param key [String] the encryption key to validate
512
+ # @return [ComplexConfig::KeySource, FalseClass] returns the KeySource object
513
+ # if the key is valid, false otherwise
220
514
  def valid_key?(key)
221
515
  ks = ComplexConfig::KeySource.new(var: key)
222
516
  ComplexConfig::Encryption.new(ks.key_bytes)
@@ -227,6 +521,20 @@ class ComplexConfig::Provider
227
521
 
228
522
  private
229
523
 
524
+ # The decrypt_config_case method handles the decryption of encrypted
525
+ # configuration files
526
+ #
527
+ # This method checks for the existence of an encrypted configuration file and
528
+ # attempts to decrypt it using the available key source. It returns different
529
+ # status codes based on whether the decryption was successful, if a key was
530
+ # missing, or if the file itself was missing.
531
+ #
532
+ # @param pathname [String, Pathname] The path to the configuration file to
533
+ # check for encryption
534
+ #
535
+ # @return [Array] An array containing the decrypted data (or nil), a symbol
536
+ # indicating the result status (:ok, :key_missing, or :file_missing), and
537
+ # the encrypted file's pathname
230
538
  def decrypt_config_case(pathname)
231
539
  enc_pathname = pathname.to_s + '.enc'
232
540
  my_ks = key_source(pathname)
@@ -242,6 +550,24 @@ class ComplexConfig::Provider
242
550
  return nil, :file_missing, enc_pathname
243
551
  end
244
552
 
553
+ # The interpret_name_value method processes name and value parameters for
554
+ # configuration handling
555
+ #
556
+ # This method normalizes the input parameters for configuration operations by
557
+ # validating the name parameter type and preparing the value parameter
558
+ # accordingly. It handles different input scenarios including
559
+ # ComplexConfig::Settings objects and string/symbol names.
560
+ #
561
+ # @param name [String, Symbol, ComplexConfig::Settings] The name parameter
562
+ # which can be a string, symbol, or ComplexConfig::Settings object
563
+ # @param value [Object, nil] The value parameter to be processed, typically a
564
+ # hash or nil
565
+ #
566
+ # @return [Array<String, Object>] An array containing the normalized name and
567
+ # value parameters for further configuration processing
568
+ #
569
+ # @raise [ArgumentError] if the name parameter is not a string/symbol or
570
+ # ComplexConfig::Settings object
245
571
  def interpret_name_value(name, value)
246
572
  if ComplexConfig::Settings === name
247
573
  if value
@@ -258,6 +584,22 @@ class ComplexConfig::Provider
258
584
  return name, value
259
585
  end
260
586
 
587
+ # The provide_key_source method determines and returns an appropriate key
588
+ # source for encryption operations
589
+ #
590
+ # This method analyzes the encryption parameter and constructs a suitable key
591
+ # source object based on whether a random key should be generated, an
592
+ # existing key source should be used, or a specific hex key string was
593
+ # provided
594
+ #
595
+ # @param pathname [String, nil] The path to a configuration file that may
596
+ # contain a key
597
+ # @param encrypt [Boolean, Symbol, String] Encryption directive that
598
+ # determines key source selection
599
+ # @return [ComplexConfig::KeySource] A key source object configured with the
600
+ # appropriate encryption key
601
+ # @raise [ComplexConfig::EncryptionKeyInvalid] if the encryption key is
602
+ # missing or has invalid format
261
603
  def provide_key_source(pathname, encrypt)
262
604
  ks =
263
605
  case encrypt
@@ -1,22 +1,54 @@
1
1
  module ComplexConfig
2
+ # A proxy class that provides dynamic configuration access with lazy
3
+ # evaluation
4
+ #
5
+ # The Proxy class acts as a wrapper around configuration access, deferring
6
+ # the actual configuration loading until a method is first called. It
7
+ # supports environment-specific configuration lookups and can handle both
8
+ # direct configuration access and existence checks.
9
+ #
10
+ # @attr_reader [String, nil] env The environment name used for configuration
11
+ # lookups
2
12
  class Proxy < BasicObject
13
+ # The proxy object's initialization method sets up the environment for
14
+ # configuration access.
15
+ #
16
+ # @param env [String, nil] The environment name to use for configuration
17
+ # lookups, defaults to nil which will use the default environment
3
18
  def initialize(env = nil)
4
19
  @env = env
5
20
  end
6
21
 
22
+ # The to_s method returns a string representation of the proxy object.
23
+ #
24
+ # @return [String] the string 'ComplexConfig::Proxy'
7
25
  def to_s
8
26
  'ComplexConfig::Proxy'
9
27
  end
10
28
 
29
+ # The inspect method returns a string representation of the proxy object.
30
+ #
31
+ # @return [String] a string representation in the format "#<ComplexConfig::Proxy>"
11
32
  def inspect
12
33
  "#<#{to_s}>"
13
34
  end
14
35
 
36
+ # The reload method flushes the configuration cache and returns the
37
+ # receiver.
38
+ #
39
+ # @return [ComplexConfig::Proxy] the proxy object itself
15
40
  def reload
16
41
  ::ComplexConfig::Provider.flush_cache
17
42
  self
18
43
  end
19
44
 
45
+ # The method_missing method handles dynamic configuration access and
46
+ # validation.
47
+ #
48
+ # @param name [Symbol] The name of the method being called
49
+ # @param args [Array] Arguments passed to the method
50
+ #
51
+ # @return [Object] The result of the dynamic configuration lookup or method call
20
52
  def method_missing(name, *args)
21
53
  if name =~ /\?\z/
22
54
  method_name, name = name, $`
@@ -1,4 +1,9 @@
1
1
  module ComplexConfig
2
+ # Rails integration for ComplexConfig
3
+ #
4
+ # Provides integration with Rails application lifecycle by flushing the
5
+ # configuration cache during the to_prepare callback, ensuring that
6
+ # configuration changes are picked up correctly in development mode.
2
7
  class Railtie < Rails::Railtie
3
8
  config.to_prepare do
4
9
  ComplexConfig::Provider.flush_cache