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.
@@ -2,15 +2,54 @@ require 'json'
2
2
  require 'tins/xt/ask_and_send'
3
3
  require 'tins/thread_local'
4
4
 
5
+ # A settings class that provides structured access to configuration data
6
+ #
7
+ # The Settings class serves as a container for configuration values, offering
8
+ # nested access through method calls and providing utilities for converting
9
+ # between different representations like hashes, YAML, and JSON. It supports
10
+ # environment-specific lookups and can be deeply frozen to prevent modification
11
+ # after initialization.
12
+ #
13
+ # @see ComplexConfig::Provider
14
+ # @see ComplexConfig::Config
15
+ # @see ComplexConfig::Settings#to_h
16
+ # @see ComplexConfig::Settings#to_yaml
17
+ # @see ComplexConfig::Settings#to_json
5
18
  class ComplexConfig::Settings < BasicObject
6
19
  include ::Kernel
7
20
  include ::Tins::AskAndSend
8
21
 
9
22
  class << self
23
+
24
+ # The [] method converts a hash-like object into a Settings object
25
+ #
26
+ # This method serves as a convenience accessor that delegates to the
27
+ # from_hash class method, enabling quick conversion of hash-like structures
28
+ # into ComplexConfig::Settings objects for structured configuration access.
29
+ #
30
+ # @param a [Object] the object to convert, which may respond to to_hash or to_ary
31
+ #
32
+ # @return [ComplexConfig::Settings, Array, Object] a Settings object if the
33
+ # input responds to to_hash, an array of converted elements if it responds to
34
+ # to_ary, or the original object if neither applies
35
+ #
36
+ # @see ComplexConfig::Settings.from_hash for the underlying conversion logic
10
37
  def [](*a)
11
38
  from_hash(*a)
12
39
  end
13
40
 
41
+ # The from_hash method converts a hash-like object into a Settings object
42
+ #
43
+ # This method recursively processes hash-like objects and arrays,
44
+ # converting them into Settings objects with appropriate nested structures
45
+ # while preserving non-hash, non-array values as-is
46
+ #
47
+ # @param object [Object] the object to convert, which may respond to
48
+ # to_hash or to_ary
49
+ #
50
+ # @return [ComplexConfig::Settings, Array, Object] a Settings object if the
51
+ # input responds to to_hash, an array of converted elements if it
52
+ # responds to to_ary, or the original object if neither applies
14
53
  def from_hash(object)
15
54
  case
16
55
  when object.respond_to?(:to_hash)
@@ -26,6 +65,24 @@ class ComplexConfig::Settings < BasicObject
26
65
  end
27
66
  end
28
67
 
68
+ # The build method constructs a Settings object from a hash with optional
69
+ # name prefixing
70
+ #
71
+ # This method takes a name and hash, sets the name as the prefix for the
72
+ # Settings object, validates that the hash can be converted to a hash, then
73
+ # converts it using from_hash. It ensures the name_prefix is reset to nil
74
+ # after the operation completes.
75
+ #
76
+ # @param name [String, nil] The name to use as prefix for the Settings
77
+ # object, or nil
78
+ #
79
+ # @param hash [Object] The object to convert to a Settings object, must
80
+ # respond to to_hash
81
+ #
82
+ # @return [ComplexConfig::Settings] A new Settings object built from the
83
+ # provided hash
84
+ #
85
+ # @raise [TypeError] if the hash parameter does not respond to to_hash
29
86
  def build(name, hash)
30
87
  name.nil? or self.name_prefix = name.to_sym
31
88
  hash.respond_to?(:to_hash) or raise TypeError, 'require hash to build'
@@ -36,11 +93,37 @@ class ComplexConfig::Settings < BasicObject
36
93
 
37
94
  extend Tins::ThreadLocal
38
95
 
96
+ # The thread_local method sets up a thread-local variable for the
97
+ # name_prefix attribute
98
+ #
99
+ # This method configures a thread-local storage mechanism for the
100
+ # name_prefix attribute, allowing each thread to maintain its own
101
+ # independent value for this attribute while sharing the same class-level
102
+ # configuration.
103
+ #
104
+ # @return [String] the name prefix of the setting
39
105
  thread_local :name_prefix
40
106
  end
41
107
 
108
+ # The name_prefix attribute accessor provides read and write access to the
109
+ # name prefix setting
110
+ #
111
+ # This method allows getting and setting the name_prefix instance variable,
112
+ # which is used to prefix configuration keys and provide context for
113
+ # configuration lookups.
114
+ #
115
+ # @attr [String, nil] name_prefix the new name prefix value that was set
42
116
  attr_accessor :name_prefix
43
117
 
118
+ # The initialize method sets up a new Settings object with optional hash
119
+ # initialization
120
+ #
121
+ # This method creates a new instance of the Settings class, initializing it
122
+ # with an optional hash of values. It sets the name_prefix from the
123
+ # class-level attribute and prepares an internal table for storing
124
+ # configuration attributes.
125
+ #
126
+ # @param hash [Hash, nil] An optional hash containing initial configuration values
44
127
  def initialize(hash = nil)
45
128
  self.name_prefix = self.class.name_prefix
46
129
  @table = {}
@@ -52,24 +135,75 @@ class ComplexConfig::Settings < BasicObject
52
135
  end
53
136
  end
54
137
 
138
+ # The initialize_copy method creates a duplicate of the current object
139
+ #
140
+ # This method is called when an object is being duplicated, typically through
141
+ # the dup or clone methods. It performs a deep copy of the internal table
142
+ # structure while preserving the object's state and ensuring that
143
+ # modifications to the copy don't affect the original object.
144
+ #
145
+ # @param orig [Object] The original object being copied
146
+ # @return [self] Returns the duplicated object instance for chaining
55
147
  def initialize_copy(orig)
56
148
  super
57
149
  @table = @table.dup
58
150
  self
59
151
  end
60
152
 
153
+ # The attribute_set? method checks whether a specific attribute has been set
154
+ # in the configuration
155
+ #
156
+ # This method verifies if a given attribute name exists in the internal table
157
+ # of configuration settings, returning true if it has been explicitly set and
158
+ # false otherwise. It converts the provided name to a symbol before
159
+ # performing the lookup.
160
+ #
161
+ # @param name [Object] the name of the attribute to check for existence
162
+ # @return [TrueClass, FalseClass] true if the attribute is set, false otherwise
61
163
  def attribute_set?(name)
62
164
  @table.key?(name.to_sym)
63
165
  end
64
166
 
167
+ # The attribute_names method retrieves all attribute names stored in the
168
+ # configuration
169
+ #
170
+ # This method provides access to the internal table of attribute names that
171
+ # have been set on the current Settings object. It returns an array
172
+ # containing all the symbolized keys that represent the configured
173
+ # attributes.
174
+ #
175
+ # @return [Array<Symbol>] an array of symbolized attribute names that have
176
+ # been set on this Settings object
65
177
  def attribute_names
66
178
  @table.keys
67
179
  end
68
180
 
181
+ # The attribute_values method retrieves all values stored in the
182
+ # configuration table
183
+ #
184
+ # This method provides access to the internal table of configuration values,
185
+ # returning an array containing all the values that have been set on the
186
+ # current Settings object. It exposes the underlying data structure for
187
+ # direct inspection or processing.
188
+ #
189
+ # @return [Array<Object>] an array of all configuration values stored in the
190
+ # table
69
191
  def attribute_values
70
192
  @table.values
71
193
  end
72
194
 
195
+ # The attributes_update method merges configuration attributes from another
196
+ # source
197
+ #
198
+ # This method updates the current object's internal table with attributes
199
+ # from another source, converting it to a Settings object if necessary. It
200
+ # performs a deep merge of the attribute data while preserving the existing
201
+ # structure.
202
+ #
203
+ # @param other [Object] the source containing attributes to merge, which can
204
+ # be any object that responds to to_hash or to_ary
205
+ #
206
+ # @return [void] Returns nothing
73
207
  def attributes_update(other)
74
208
  unless other.is_a? self.class
75
209
  other = self.class.from_hash(other)
@@ -77,6 +211,18 @@ class ComplexConfig::Settings < BasicObject
77
211
  @table.update(other.table)
78
212
  end
79
213
 
214
+ # The attributes_update_if_nil method merges configuration attributes from
215
+ # another source, updating only nil values
216
+ #
217
+ # This method updates the current object's internal table with attributes
218
+ # from another source, but only assigns new values when the existing keys
219
+ # have nil values. It preserves existing non-nil attribute values while
220
+ # allowing nil values to be overridden.
221
+ #
222
+ # @param other [Object] the source containing attributes to merge, which can
223
+ # be any object that responds to to_hash or to_ary
224
+ #
225
+ # @return [void] Returns nothing
80
226
  def attributes_update_if_nil(other)
81
227
  unless other.is_a? self.class
82
228
  other = self.class.from_hash(other)
@@ -86,6 +232,18 @@ class ComplexConfig::Settings < BasicObject
86
232
  end
87
233
  end
88
234
 
235
+ # The replace_attributes method replaces all attributes with those from the
236
+ # provided hash
237
+ #
238
+ # This method updates the current object's internal table by replacing all
239
+ # existing attributes with new ones derived from the given hash. It converts
240
+ # the hash into a Settings object structure and then updates the internal
241
+ # table with the new data.
242
+ #
243
+ # @param hash [Object] the source containing attributes to replace with,
244
+ # which can be any object that responds to to_hash or to_ary
245
+ #
246
+ # @return [self] returns self to allow for method chaining
89
247
  def replace_attributes(hash)
90
248
  @table = self.class.from_hash(hash).table
91
249
  self
@@ -98,10 +256,19 @@ class ComplexConfig::Settings < BasicObject
98
256
  # self
99
257
  #end
100
258
 
259
+ # The to_h method converts the settings object into a hash representation
260
+ #
261
+ # This method recursively transforms the internal table of configuration
262
+ # attributes into a nested hash structure, preserving the hierarchical
263
+ # organization of settings while handling various value types including
264
+ # arrays, nested settings objects, and primitive values.
265
+ #
266
+ # @return [Hash] a hash representation of the settings object with all nested
267
+ # structures converted to their hash equivalents
101
268
  def to_h
102
269
  table_enumerator.each_with_object({}) do |(k, v), h|
103
270
  h[k] =
104
- if ::Array === v
271
+ if v.respond_to?(:to_ary)
105
272
  v.to_ary.map { |x| (x.ask_and_send(:to_h) rescue x) || x }
106
273
  elsif v.respond_to?(:to_h)
107
274
  if v.nil?
@@ -115,30 +282,97 @@ class ComplexConfig::Settings < BasicObject
115
282
  end
116
283
  end
117
284
 
285
+ # The == method compares this settings object with another object for
286
+ # equality
287
+ #
288
+ # This method checks if the given object responds to to_h and then compares
289
+ # the hash representation of this settings object with the hash
290
+ # representation of the other object to determine if they are equal
291
+ #
292
+ # @param other [Object] the object to compare with this settings object
293
+ # @return [TrueClass, FalseClass] true if the other object responds to to_h
294
+ # and their hash representations are equal, false otherwise
118
295
  def ==(other)
119
296
  other.respond_to?(:to_h) && to_h == other.to_h
120
297
  end
121
298
 
299
+ # The to_yaml method converts the settings object into YAML format
300
+ #
301
+ # This method transforms the configuration data stored in the settings object
302
+ # into a YAML string representation, making it suitable for serialization and
303
+ # storage in YAML files.
304
+ #
305
+ # @return [String] a YAML formatted string representation of the settings object
122
306
  def to_yaml
123
307
  to_h.to_yaml
124
308
  end
125
309
 
310
+ # The to_json method converts the settings object into JSON format
311
+ #
312
+ # This method transforms the configuration data stored in the settings object
313
+ # into a JSON string representation, making it suitable for serialization and
314
+ # interchange with other systems that consume JSON data.
315
+ #
316
+ # @param a [Array] Additional arguments to pass to the underlying to_json method
317
+ #
318
+ # @return [String] a JSON formatted string representation of the settings object
126
319
  def to_json(*a)
127
320
  to_h.to_json(*a)
128
321
  end
129
322
 
323
+ # The to_tree method converts the settings object into a tree representation
324
+ #
325
+ # This method transforms the hierarchical configuration data stored in the
326
+ # settings object into a tree structure that can be used for visualization or
327
+ # display purposes. It utilizes the Tree.convert class method to perform the
328
+ # actual conversion process.
329
+ #
330
+ # @return [ComplexConfig::Tree] a tree representation of the settings object
331
+ # hierarchy
130
332
  def to_tree
131
333
  ::ComplexConfig::Tree.convert(name_prefix, self)
132
334
  end
133
335
 
336
+ # The size method returns the number of attributes in the settings object
337
+ #
338
+ # This method counts all configured attributes by enumerating through the
339
+ # internal table and returning the total number of key-value pairs stored in
340
+ # the settings object
341
+ #
342
+ # @return [Integer] the count of attributes stored in this settings object
134
343
  def size
135
344
  each.count
136
345
  end
137
346
 
347
+ # The empty? method checks whether the settings object contains no attributes
348
+ #
349
+ # This method determines if the current Settings object has zero configured
350
+ # attributes by comparing its size to zero. It provides a convenient way to
351
+ # test for emptiness without having to manually check the size or iterate
352
+ # through all attributes.
353
+ #
354
+ # @return [TrueClass, FalseClass] true if the settings object has no
355
+ # attributes, false otherwise
138
356
  def empty?
139
357
  size == 0
140
358
  end
141
359
 
360
+ # The attributes_list method generates a formatted string representation of
361
+ # all configuration attributes
362
+ #
363
+ # This method creates a human-readable list of all configuration attributes
364
+ # by combining their paths and values into a structured format with
365
+ # customizable separators for paths and key-value pairs
366
+ #
367
+ # @param pair_sep [String] the separator to use between attribute paths and
368
+ # their values, defaults to ' = '
369
+ #
370
+ # @param path_sep [String] the separator to use between path components,
371
+ # defaults to '.'
372
+ #
373
+ # @return [String] a formatted string containing all attribute paths and
374
+ # their corresponding values or the class name if no attributes are
375
+ # present
142
376
  def attributes_list(pair_sep: ' = ', path_sep: ?.)
143
377
  empty? and return self.class.name
144
378
  pathes(path_sep: path_sep).inject('') do |result, (path, value)|
@@ -146,11 +380,33 @@ class ComplexConfig::Settings < BasicObject
146
380
  end
147
381
  end
148
382
 
149
- def to_s(*)
383
+ # The to_s method provides a string representation of the settings object
384
+ #
385
+ # This method returns a human-readable string representation of the settings
386
+ # object, either by returning the class name when the object is empty, or by
387
+ # converting the object to a tree structure and then to a string for
388
+ # non-empty objects
389
+ #
390
+ # @param a [Array] Additional arguments passed to the method (not used)
391
+ # @return [String] The string representation of the settings object
392
+ def to_s(*a)
150
393
  empty? and return self.class.name
151
394
  to_tree.to_s
152
395
  end
153
396
 
397
+ # The pathes method recursively builds a hash of configuration paths and
398
+ # their values
399
+ #
400
+ # This method traverses a nested hash structure and constructs a flattened
401
+ # hash where keys are dot-separated paths representing the hierarchical
402
+ # structure of the original data, and values are the corresponding leaf
403
+ # values from the original structure
404
+ #
405
+ # @param hash [Hash] the hash to process, defaults to the instance's table
406
+ # @param path_sep [String] the separator to use between path components, defaults to '.'
407
+ # @param prefix [String] the prefix to prepend to each path, defaults to the name_prefix
408
+ # @param result [Hash] the hash to accumulate results in, defaults to an empty hash
409
+ # @return [Hash] a flattened hash with paths as keys and values as leaf values
154
410
  def pathes(hash = table, path_sep: ?., prefix: name_prefix.to_s, result: {})
155
411
  hash.each do |key, value|
156
412
  path = prefix.empty? ? key.to_s : "#{prefix}#{path_sep}#{key}"
@@ -185,15 +441,40 @@ class ComplexConfig::Settings < BasicObject
185
441
 
186
442
  alias inspect to_s
187
443
 
444
+ # The pretty_print method formats the object for pretty printing
445
+ #
446
+ # This method takes a PrettyPrint object and uses it to format the object's
447
+ # string representation for display purposes
448
+ #
449
+ # @param q [PrettyPrint] the pretty printer object to use for formatting
450
+ # @return [void] Returns nothing
188
451
  def pretty_print(q)
189
452
  q.text inspect
190
453
  end
191
454
 
455
+ # The freeze method freezes the internal table and calls the superclass
456
+ # freeze method
457
+ #
458
+ # This method ensures that the configuration data stored in the internal
459
+ # table is frozen, preventing further modifications to the configuration
460
+ # settings. It then delegates to the parent class's freeze method to complete
461
+ # the freezing process.
462
+ #
463
+ # @return [self] Returns self to allow for method chaining after freezing
192
464
  def freeze
193
465
  @table.freeze
194
466
  super
195
467
  end
196
468
 
469
+ # The deep_freeze method recursively freezes all nested objects within the
470
+ # configuration
471
+ #
472
+ # This method traverses the internal table of configuration attributes and
473
+ # applies deep freezing to each value, ensuring that all nested settings
474
+ # objects and their contents are immutable It also freezes the internal table
475
+ # itself to prevent modification of the attribute structure
476
+ #
477
+ # @return [self] Returns self to allow for method chaining after freezing
197
478
  def deep_freeze
198
479
  table_enumerator.each do |_, v|
199
480
  v.ask_and_send(:deep_freeze) || (v.freeze rescue v)
@@ -201,6 +482,16 @@ class ComplexConfig::Settings < BasicObject
201
482
  freeze
202
483
  end
203
484
 
485
+ # The attribute_get method retrieves a configuration attribute value by name
486
+ #
487
+ # This method attempts to fetch a configuration attribute value first from
488
+ # the internal table, and if the attribute is not set, it applies registered
489
+ # plugins to generate a value. It provides a unified way to access
490
+ # configuration attributes that may be dynamically generated.
491
+ #
492
+ # @param name [Object] the name of the attribute to retrieve
493
+ # @return [Object, nil] the value of the attribute if found, or nil if not
494
+ # found
204
495
  def attribute_get(name)
205
496
  if !attribute_set?(name) and
206
497
  value = ::ComplexConfig::Provider.apply_plugins(self, name)
@@ -211,8 +502,18 @@ class ComplexConfig::Settings < BasicObject
211
502
  end
212
503
  end
213
504
 
505
+ # Alias for {attribute_get}
506
+ #
507
+ # @see attribute_get
214
508
  alias [] attribute_get
215
509
 
510
+ # The attribute_get! method retrieves a configuration attribute value by
511
+ # name, raising an exception if the attribute is not set
512
+ #
513
+ # @param name [Object] the name of the attribute to retrieve
514
+ # @return [Object] the value of the attribute if found
515
+ # @raise [ComplexConfig::AttributeMissing] if the attribute is not set and no
516
+ # plugin can provide a value
216
517
  def attribute_get!(name)
217
518
  if attribute_set?(name)
218
519
  attribute_get(name)
@@ -221,32 +522,103 @@ class ComplexConfig::Settings < BasicObject
221
522
  end
222
523
  end
223
524
 
525
+ # The []= method assigns a value to a configuration attribute
526
+ #
527
+ # This method stores a configuration attribute value in the internal table
528
+ # using the attribute name as a symbol key. It converts the attribute name
529
+ # to a symbol before storing the value.
530
+ #
531
+ # @param name [Object] the name of the attribute to assign
532
+ # @param value [Object] the value to assign to the attribute
533
+ # @return [Object] the assigned value
224
534
  def []=(name, value)
225
535
  @table[name.to_sym] = value
226
536
  end
227
537
 
538
+ # The each method iterates over all configuration attributes
539
+ #
540
+ # This method provides enumeration support for the configuration settings,
541
+ # yielding each key-value pair from the internal table to the provided block.
542
+ # It delegates to the table enumerator to ensure consistent iteration behavior
543
+ # across different contexts.
544
+ #
545
+ # @yield [key, value] Yields each configuration attribute key and its
546
+ # corresponding value
547
+ # @yieldparam key [Object] The configuration attribute key
548
+ # @yieldparam value [Object] The configuration attribute value
549
+ # @return [self] Returns self to allow for method chaining after enumeration
228
550
  def each(&block)
229
551
  table_enumerator.each(&block)
230
552
  end
231
553
 
232
554
  protected
233
555
 
556
+ # The table attribute reader provides access to the internal hash table
557
+ # storing configuration attributes
558
+ #
559
+ # This method returns the internal @table instance variable that holds all
560
+ # configuration attributes and their values for this settings object. The
561
+ # returned hash is used internally for fast lookup of configuration values
562
+ # and is not intended to be modified directly by external code.
563
+ #
564
+ # @return [Hash] the internal hash table containing all configuration
565
+ # attributes and their values
566
+ # @api private
234
567
  attr_reader :table
235
568
 
236
569
  private
237
570
 
571
+ # The table_enumerator method provides an enumerator for iterating over the
572
+ # internal configuration table
573
+ #
574
+ # This method returns an enumerator object that can be used to iterate over
575
+ # all key-value pairs stored in the internal @table instance variable. It
576
+ # delegates to the enum_for method of the @table hash to provide consistent
577
+ # enumeration behavior.
578
+ #
579
+ # @return [Enumerator] an enumerator for the internal configuration table hash
238
580
  def table_enumerator
239
581
  @table.enum_for(:each)
240
582
  end
241
583
 
584
+ # The respond_to_missing? method determines if the object responds to a given
585
+ # method name
586
+ #
587
+ # This method is part of Ruby's method missing protocol and is used to
588
+ # dynamically determine whether the object should be considered as responding
589
+ # to a particular method. It checks if the method name ends with a question
590
+ # mark (indicating a safe navigation query) or if the corresponding attribute
591
+ # has been explicitly set.
592
+ #
593
+ # @param id [Object] the method name being checked
594
+ # @param include_private [Boolean] whether to consider private methods
595
+ # (unused in this implementation)
596
+ # @return [Boolean] true if the object responds to the method, false otherwise
242
597
  def respond_to_missing?(id, include_private = false)
243
598
  id =~ /\?\z/ || attribute_set?(id)
244
599
  end
245
600
 
601
+ # The skip method raises a skip exception to bypass plugin execution
602
+ #
603
+ # @return [void] This method always raises a :skip exception and never
604
+ # returns normally
246
605
  def skip
247
606
  throw :skip
248
607
  end
249
608
 
609
+ # The method_missing method handles dynamic attribute access and assignment
610
+ #
611
+ # This method intercepts calls to undefined methods on the Settings object,
612
+ # providing support for attribute retrieval, assignment, existence checking,
613
+ # and plugin-based value resolution. It processes method names ending with
614
+ # '?' for existence checks or safe navigation, '=' for assignment, and other
615
+ # names for attribute lookup or plugin execution.
616
+ #
617
+ # @param id [Object] The name of the method being called
618
+ # @param a [Array] Arguments passed to the method
619
+ # @param b [Proc] Block passed to the method
620
+ #
621
+ # @return [Object] The result of the dynamic attribute operation
250
622
  def method_missing(id, *a, &b)
251
623
  case
252
624
  when id =~ /\?\z/