Class: Lich::Common::SettingsProxy

Inherits:
Object
  • Object
show all
Defined in:
documented/common/settings/settings_proxy.rb

Overview

A proxy class for managing settings with a target object. This class allows for delegation of method calls to the target object, while providing additional functionality such as logging and path management.

Examples:

Creating a SettingsProxy instance

proxy = SettingsProxy.new(SettingsModule, scope, path, target)

Constant Summary collapse

LOG_PREFIX =
"[SettingsProxy]".freeze
NON_DESTRUCTIVE_METHODS =

A list of non-destructive methods that can be called on the target.

[
  :+, :-, :&, :|, :*,
  :all?, :any?, :assoc, :at, :bsearch, :bsearch_index, :chunk, :chunk_while,
  :collect, :collect_concat, :compact, :compare_by_identity?, :count, :cycle,
  :default, :default_proc, :detect, :dig, :drop, :drop_while,
  :each_cons, :each_entry, :each_slice, :each_with_index, :each_with_object, :empty?,
  :entries, :except, :fetch, :fetch_values, :filter, :find, :find_all, :find_index,
  :first, :flat_map, :flatten, :frozen?, :grep, :grep_v, :group_by, :has_value?,
  :include?, :inject, :invert, :join, :key, :keys, :last, :lazy, :length,
  :map, :max, :max_by, :member?, :merge, :min, :min_by, :minmax, :minmax_by,
  :none?, :one?, :pack, :partition, :permutation, :product, :rassoc, :reduce,
  :reject, :reverse, :rotate, :sample, :select, :shuffle, :size, :slice,
  :slice_after, :slice_before, :slice_when, :sort, :sort_by, :sum,
  :take, :take_while, :to_a, :to_h, :to_proc, :transform_keys, :transform_values,
  :uniq, :values, :values_at, :zip
].freeze
NON_DESTRUCTIVE_CONTAINER_VIEWS =

Subset of non-destructive methods that return container “views” A subset of non-destructive methods that return container “views”.

[
  :map, :collect, :select, :filter, :reject, :find_all, :grep, :grep_v,
  :sort, :sort_by, :uniq, :compact, :flatten, :slice, :take, :drop, :values
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(settings_module, scope, path, target, detached: false) ⇒ SettingsProxy

Initializes a new SettingsProxy instance.

Parameters:

  • settings_module (Module)

    The settings module that this proxy belongs to.

  • scope (Object)

    The scope in which the settings are defined.

  • path (Array)

    The path to the settings.

  • target (Object)

    The target object that the proxy will delegate to.

  • detached (Boolean) (defaults to: false)

    Whether the proxy is detached from the target.



18
19
20
21
22
23
24
25
# File 'documented/common/settings/settings_proxy.rb', line 18

def initialize(settings_module, scope, path, target, detached: false)
  @settings_module = settings_module # This should be the Settings module itself
  @scope  = scope
  @path   = path.dup
  @target = target
  @detached = detached
  @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "INIT scope: #{@scope.inspect}, path: #{@path.inspect}, target_class: #{@target.class}, target_object_id: #{@target.object_id}, detached: #{@detached}" })
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

Handles calls to methods that are not explicitly defined in the proxy.

Parameters:

  • method (Symbol)

    The name of the method being called.

  • args (Array)

    The arguments for the method call.

  • block (Proc)

    An optional block for the method call.

Returns:

  • (Object)

    The result of the method call, or raises NoMethodError if not found.



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'documented/common/settings/settings_proxy.rb', line 327

def method_missing(method, *args, &block)
  @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "CALL scope: #{@scope.inspect}, path: #{@path.inspect}, method: #{method}, args: #{args.inspect}, target_object_id: #{@target.object_id}" })
  if @target.respond_to?(method)
    if NON_DESTRUCTIVE_METHODS.include?(method)
      @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "CALL   non-destructive method: #{method}" })
      target_dup = @target.dup
      unwrapped_args = args.map { |arg| arg.is_a?(SettingsProxy) ? @settings_module.unwrap_proxies(arg) : arg } # Corrected
      result = target_dup.send(method, *unwrapped_args, &block)
      # Minimal change: pass method name so we can tag views as detached and keep path
      return handle_non_destructive_result(method, result)
    else
      @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "CALL   destructive method: #{method}" })

      # NEW (5.12.7+): auto-reattach derived views before mutating
      # ensure destructive methods (.push) do not target a proxy non-destructive method (.sort)
      if detached?
        unless @settings_module._reattach_live!(self)
          @settings_module._log(Settings::LOG_LEVEL_ERROR, LOG_PREFIX, -> { "CALL   reattach failed; aborting destructive op #{method} on detached view" })
          return self
        end
      end

      unwrapped_args = args.map { |arg| arg.is_a?(SettingsProxy) ? @settings_module.unwrap_proxies(arg) : arg } # Corrected
      @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "CALL   target_before_op: #{@target.inspect}" })
      result = @target.send(method, *unwrapped_args, &block)
      @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "CALL   target_after_op: #{@target.inspect}" })
      @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "CALL   calling save_proxy_changes on settings module" })
      @settings_module.save_proxy_changes(self)
      return handle_method_result(result)
    end
  else
    super
  end
end

Instance Attribute Details

#pathObject (readonly)

Provides read access to the target, path, and scope attributes.



28
29
30
# File 'documented/common/settings/settings_proxy.rb', line 28

def path
  @path
end

#scopeObject (readonly)

Provides read access to the target, path, and scope attributes.



28
29
30
# File 'documented/common/settings/settings_proxy.rb', line 28

def scope
  @scope
end

#targetObject (readonly)

Provides read access to the target, path, and scope attributes.



28
29
30
# File 'documented/common/settings/settings_proxy.rb', line 28

def target
  @target
end

Instance Method Details

#[](key) ⇒ Object

Retrieves a value from the target using the specified key.

Parameters:

  • key (Object)

    The key to retrieve the value for.

Returns:

  • (Object)

    The value associated with the key, or a new SettingsProxy if the value is a container.



292
293
294
295
296
297
298
299
300
301
302
# File 'documented/common/settings/settings_proxy.rb', line 292

def [](key)
  @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "GET scope: #{@scope.inspect}, path: #{@path.inspect}, key: #{key.inspect}, target_object_id: #{@target.object_id}" })
  value = @target[key]
  if @settings_module.container?(value)
    new_path = @path.dup
    new_path << key
    SettingsProxy.new(@settings_module, @scope, new_path, value)
  else
    value
  end
end

#[]=(key, value) ⇒ Object

Sets a value in the target using the specified key.

Parameters:

  • key (Object)

    The key to set the value for.

  • value (Object)

    The value to set.

Returns:

  • (Object)

    The value that was set.



308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'documented/common/settings/settings_proxy.rb', line 308

def []=(key, value)
  @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "SET scope: #{@scope.inspect}, path: #{@path.inspect}, key: #{key.inspect}, value: #{value.inspect}, target_object_id: #{@target.object_id}" })
  actual_value = value.is_a?(SettingsProxy) ? @settings_module.unwrap_proxies(value) : value # Corrected to use @settings_module
  @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "SET   target_before_set: #{@target.inspect}" })
  @target[key] = actual_value
  @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "SET   target_after_set: #{@target.inspect}" })
  @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "SET   calling save_proxy_changes on settings module" })
  @settings_module.save_proxy_changes(self)
  # rubocop:disable Lint/Void
  # This is Ruby expected behavior to return the value.
  value
  # rubocop:enable Lint/Void
end

#binary_op(operator, other) ⇒ Object

Performs a binary operation with the target and another value.

Parameters:

  • operator (Symbol)

    The operator to use for the operation.

  • other (Object)

    The other value to operate with.

Returns:

  • (Object)

    The result of the operation.



46
47
48
49
# File 'documented/common/settings/settings_proxy.rb', line 46

def binary_op(operator, other)
  other_value = other.is_a?(SettingsProxy) ? other.target : other
  @target.send(operator, other_value)
end

#detached?Boolean

Checks if the proxy is detached from the target.

Returns:

  • (Boolean)

    True if the proxy is detached, false otherwise.



32
33
34
# File 'documented/common/settings/settings_proxy.rb', line 32

def detached?
  !!@detached
end

#each(&_block) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'documented/common/settings/settings_proxy.rb', line 250

def each(&_block)
  return enum_for(:each) unless block_given?
  if @target.respond_to?(:each)
    @target.each do |item|
      if @settings_module.container?(item)
        yield SettingsProxy.new(@settings_module, @scope, [], item, detached: true)
      else
        yield item
      end
    end
  end
  self
end

#handle_method_result(result) ⇒ Object



392
393
394
395
396
397
398
399
400
401
402
# File 'documented/common/settings/settings_proxy.rb', line 392

def handle_method_result(result)
  if result.equal?(@target)
    self # Return self if the method modified the target in-place and returned it
  elsif @settings_module.container?(result)
    # If a new container is returned (e.g. some destructive methods might return a new object)
    # Wrap it in a new proxy, maintaining the current path and scope.
    SettingsProxy.new(@settings_module, @scope, @path, result)
  else
    result
  end
end

#handle_non_destructive_result(method, result) ⇒ Object



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'documented/common/settings/settings_proxy.rb', line 362

def handle_non_destructive_result(method, result)
  @settings_module.reset_path_and_return(
    if @settings_module.container?(result)
      if @target.is_a?(Array) && [:find, :detect].include?(method)
        # For Array#find / Array#detect, identify the element index in the
        # original array and create a proxy that points to that element.
        idx = @target.index(result)

        if !idx.nil?
          element_path = @path.dup
          element_path << idx
          SettingsProxy.new(@settings_module, @scope, element_path, result)
        else
          # Fallback: if we somehow can't locate the element, preserve
          # the old behavior (path == @path, no index).
          is_view = NON_DESTRUCTIVE_CONTAINER_VIEWS.include?(method)
          SettingsProxy.new(@settings_module, @scope, @path.dup, result, detached: is_view)
        end
      else
        # Existing behavior for all other non-destructive container methods
        is_view = NON_DESTRUCTIVE_CONTAINER_VIEWS.include?(method)
        SettingsProxy.new(@settings_module, @scope, @path.dup, result, detached: is_view)
      end
    else
      # Non-container results (e.g., Hash#keys) stay as plain values
      result
    end
  )
end

#hashInteger

Returns the hash of the target object.

Returns:

  • (Integer)

    The hash value of the target.



59
60
61
# File 'documented/common/settings/settings_proxy.rb', line 59

def hash
  @target.hash
end

#inspectString

Returns a string representation of the target for inspection.

Returns:

  • (String)

    The inspected string of the target.



218
219
220
# File 'documented/common/settings/settings_proxy.rb', line 218

def inspect
  @target.inspect
end

#instance_of?(klass) ⇒ Boolean

Returns:

  • (Boolean)


242
243
244
# File 'documented/common/settings/settings_proxy.rb', line 242

def instance_of?(klass)
  @target.instance_of?(klass)
end

#is_a?(klass) ⇒ Boolean

Returns:

  • (Boolean)


234
235
236
# File 'documented/common/settings/settings_proxy.rb', line 234

def is_a?(klass)
  @target.is_a?(klass)
end

#kind_of?(klass) ⇒ Boolean

Returns:

  • (Boolean)


238
239
240
# File 'documented/common/settings/settings_proxy.rb', line 238

def kind_of?(klass)
  @target.kind_of?(klass)
end

#nil?Boolean

Checks if the target is nil.

Returns:

  • (Boolean)

    True if the target is nil, false otherwise.



38
39
40
# File 'documented/common/settings/settings_proxy.rb', line 38

def nil?
  @target.nil?
end

#pretty_print(pp) ⇒ Object

Pretty prints the target object.

Parameters:

  • pp (PP)

    The pretty print object to use.



230
231
232
# File 'documented/common/settings/settings_proxy.rb', line 230

def pretty_print(pp)
  pp.pp(@target)
end

#proxy_detailsString

Returns a string with details about the proxy.

Returns:

  • (String)

    A string representation of the proxy details.



224
225
226
# File 'documented/common/settings/settings_proxy.rb', line 224

def proxy_details
  "<SettingsProxy scope=#{@scope.inspect} path=#{@path.inspect} target_class=#{@target.class} target_object_id=#{@target.object_id} detached=#{@detached}>"
end

#respond_to?(method, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


246
247
248
# File 'documented/common/settings/settings_proxy.rb', line 246

def respond_to?(method, include_private = false)
  super || @target.respond_to?(method, include_private)
end

#respond_to_missing?(method, include_private = false) ⇒ Boolean

Checks if the target responds to a method that is not explicitly defined.

Parameters:

  • method (Symbol)

    The name of the method to check.

  • include_private (Boolean) (defaults to: false)

    Whether to include private methods in the check.

Returns:

  • (Boolean)

    True if the target responds to the method, false otherwise.



408
409
410
# File 'documented/common/settings/settings_proxy.rb', line 408

def respond_to_missing?(method, include_private = false)
  @target.respond_to?(method, include_private) || super
end

#to_aArray

Converts the target to an array, returning a default value if not possible.

Returns:

  • (Array)

    The array representation of the target, or [] if not convertible.



144
145
146
# File 'documented/common/settings/settings_proxy.rb', line 144

def to_a
  delegate_conversion(:to_a, default: [])
end

#to_aryArray

Converts the target to an array, raising an error if not possible.

Returns:

  • (Array)

    The array representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_ary.



151
152
153
# File 'documented/common/settings/settings_proxy.rb', line 151

def to_ary
  delegate_conversion(:to_ary, strict: true)
end

#to_cComplex

Converts the target to a complex number, returning a default value if not possible.

Returns:

  • (Complex)

    The complex representation of the target, or Complex(0, 0) if not convertible.



138
139
140
# File 'documented/common/settings/settings_proxy.rb', line 138

def to_c
  delegate_conversion(:to_c, default: Complex(0, 0))
end

#to_enum(*args, &block) ⇒ Enumerator

Converts the target to an enumerator, using default if not possible.

Parameters:

  • args (Array)

    Additional arguments for enumerator conversion.

  • block (Proc)

    An optional block for enumerator.

Returns:

  • (Enumerator)

    The enumerator representation of the target.



206
207
208
209
210
211
212
213
214
# File 'documented/common/settings/settings_proxy.rb', line 206

def to_enum(*args, &block)
  if @target.respond_to?(:to_enum)
    @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "to_enum: delegating" })
    @target.to_enum(*args, &block)
  else
    @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "to_enum: using default enum_for" })
    self.enum_for(*args, &block)
  end
end

#to_fFloat

Converts the target to a float, returning a default value if not possible.

Returns:

  • (Float)

    The float representation of the target, or 0.0 if not convertible.



125
126
127
# File 'documented/common/settings/settings_proxy.rb', line 125

def to_f
  delegate_conversion(:to_f, default: 0.0)
end

#to_hHash

Converts the target to a hash, returning a default value if not possible.

Returns:

  • (Hash)

    The hash representation of the target, or {} if not convertible.



157
158
159
# File 'documented/common/settings/settings_proxy.rb', line 157

def to_h
  delegate_conversion(:to_h, default: {})
end

#to_hashHash

Converts the target to a hash, raising an error if not possible.

Returns:

  • (Hash)

    The hash representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_hash.



164
165
166
# File 'documented/common/settings/settings_proxy.rb', line 164

def to_hash
  delegate_conversion(:to_hash, strict: true)
end

#to_iInteger

Converts the target to an integer, returning a default value if not possible.

Returns:

  • (Integer)

    The integer representation of the target, or 0 if not convertible.



112
113
114
# File 'documented/common/settings/settings_proxy.rb', line 112

def to_i
  delegate_conversion(:to_i, default: 0)
end

#to_intInteger

Converts the target to an integer, raising an error if not possible.

Returns:

  • (Integer)

    The integer representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_int.



119
120
121
# File 'documented/common/settings/settings_proxy.rb', line 119

def to_int
  delegate_conversion(:to_int, strict: true)
end

#to_ioIO

Converts the target to an IO object, raising an error if not possible.

Returns:

  • (IO)

    The IO representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_io.



191
192
193
# File 'documented/common/settings/settings_proxy.rb', line 191

def to_io
  delegate_conversion(:to_io, strict: true)
end

#to_json(*args) ⇒ String

Converts the target to JSON format.

Parameters:

  • args (Array)

    Additional arguments for JSON conversion.

Returns:

  • (String)

    The JSON representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_json.



172
173
174
175
176
177
178
179
# File 'documented/common/settings/settings_proxy.rb', line 172

def to_json(*args)
  if @target.respond_to?(:to_json)
    @settings_module._log(Settings::LOG_LEVEL_DEBUG, LOG_PREFIX, -> { "to_json: delegating with args" })
    @target.to_json(*args)
  else
    raise NoMethodError, "undefined method :to_json for #{@target.inspect}:#{@target.class}"
  end
end

#to_pathString

Converts the target to a path, raising an error if not possible.

Returns:

  • (String)

    The path representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_path.



198
199
200
# File 'documented/common/settings/settings_proxy.rb', line 198

def to_path
  delegate_conversion(:to_path, strict: true)
end

#to_procProc

Converts the target to a Proc, raising an error if not possible.

Returns:

  • (Proc)

    The Proc representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_proc.



184
185
186
# File 'documented/common/settings/settings_proxy.rb', line 184

def to_proc
  delegate_conversion(:to_proc, strict: true)
end

#to_rRational

Converts the target to a rational number, raising an error if not possible.

Returns:

  • (Rational)

    The rational representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_r.



132
133
134
# File 'documented/common/settings/settings_proxy.rb', line 132

def to_r
  delegate_conversion(:to_r, strict: true)
end

#to_sString

Converts the target to a string.

Returns:

  • (String)

    The string representation of the target.



92
93
94
# File 'documented/common/settings/settings_proxy.rb', line 92

def to_s
  delegate_conversion(:to_s, default: '')
end

#to_strString

Converts the target to a string, raising an error if not possible.

Returns:

  • (String)

    The string representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_str.



99
100
101
# File 'documented/common/settings/settings_proxy.rb', line 99

def to_str
  delegate_conversion(:to_str, strict: true)
end

#to_symSymbol

Converts the target to a symbol, raising an error if not possible.

Returns:

  • (Symbol)

    The symbol representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_sym.



106
107
108
# File 'documented/common/settings/settings_proxy.rb', line 106

def to_sym
  delegate_conversion(:to_sym, strict: true)
end