Class: Lich::Common::SettingsProxy

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

Overview

SettingsProxy is defined here but relies on Settings module being fully defined first, especially Settings._log. The actual require_relative for settings_proxy.rb is now at the end of settings.rb. Proxy class for managing settings with a target object. This class allows for delegation of method calls to a target object, while providing additional functionality such as logging and handling detached states.

Examples:

Creating a SettingsProxy instance

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

Constant Summary collapse

LOG_PREFIX =
"[SettingsProxy]".freeze
NON_DESTRUCTIVE_METHODS =
[
  :+, :-, :&, :|, :*,
  :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”

[
  :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

Minimal change: add detached flag (default false) Initializes a new SettingsProxy instance.

Examples:

proxy = SettingsProxy.new(Settings, scope, path, target, detached: true)

Parameters:

  • settings_module (Module)

    The Settings module itself.

  • scope (Object)

    The scope in which the settings are defined.

  • path (Array)

    The path to the settings.

  • target (Object)

    The target object to delegate to.

  • detached (Boolean) (defaults to: false)

    Whether the proxy is detached (default: false).



25
26
27
28
29
30
31
32
# File 'documented/common/settings/settings_proxy.rb', line 25

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.

Parameters:

  • method (Symbol)

    The method name being called.

  • args (Array)

    The arguments for the method.

  • block (Proc)

    An optional block for the method.

Returns:

  • (Object)

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



323
324
325
326
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
# File 'documented/common/settings/settings_proxy.rb', line 323

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)

Returns the value of attribute path.



34
35
36
# File 'documented/common/settings/settings_proxy.rb', line 34

def path
  @path
end

#scopeObject (readonly)

Returns the value of attribute scope.



34
35
36
# File 'documented/common/settings/settings_proxy.rb', line 34

def scope
  @scope
end

#targetObject (readonly)

Returns the value of attribute target.



34
35
36
# File 'documented/common/settings/settings_proxy.rb', line 34

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.



288
289
290
291
292
293
294
295
296
297
298
# File 'documented/common/settings/settings_proxy.rb', line 288

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.



304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'documented/common/settings/settings_proxy.rb', line 304

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 apply (e.g., :+, :-).

  • other (Object)

    The other operand for the operation.

Returns:

  • (Object)

    The result of the operation.



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

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

#detached?Boolean

Minimal change: expose detached? status Checks if the proxy is in a detached state.

Examples:

if proxy.detached?
  puts "Proxy is detached"
end

Returns:

  • (Boolean)

    Returns true if the proxy is detached, false otherwise.



43
44
45
# File 'documented/common/settings/settings_proxy.rb', line 43

def detached?
  !!@detached
end

#each {|Object| ... } ⇒ self

Minimal change: items yielded from #each are “views” over the container. Mark them detached so mutations during a derived iteration won’t clobber root. Iterates over the target, yielding each item. Items yielded from #each are “views” over the container.

Yields:

  • (Object)

    Each item in the target.

Returns:

  • (self)

    Returns the current instance.



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

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



370
371
372
373
374
375
376
377
378
379
380
# File 'documented/common/settings/settings_proxy.rb', line 370

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

Minimal change: keep path (not []), and tag view proxies as detached.



359
360
361
362
363
364
365
366
367
368
# File 'documented/common/settings/settings_proxy.rb', line 359

def handle_non_destructive_result(method, result)
  @settings_module.reset_path_and_return(
    if @settings_module.container?(result)
      is_view = NON_DESTRUCTIVE_CONTAINER_VIEWS.include?(method)
      SettingsProxy.new(@settings_module, @scope, @path.dup, result, detached: is_view)
    else
      result
    end
  )
end

#hashInteger

Returns the hash of the target object.

Returns:

  • (Integer)

    The hash value of the target.



70
71
72
# File 'documented/common/settings/settings_proxy.rb', line 70

def hash
  @target.hash
end

#inspectString

Updated inspect method to show the target’s inspect string Returns a string representation of the proxy’s target.

Returns:

  • (String)

    The inspect string of the target.



211
212
213
# File 'documented/common/settings/settings_proxy.rb', line 211

def inspect
  @target.inspect
end

#instance_of?(klass) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#is_a?(klass) ⇒ Boolean

Returns:

  • (Boolean)


226
227
228
# File 'documented/common/settings/settings_proxy.rb', line 226

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

#kind_of?(klass) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#nil?Boolean

Checks if the target is nil.

Returns:

  • (Boolean)

    Returns true if the target is nil, false otherwise.



49
50
51
# File 'documented/common/settings/settings_proxy.rb', line 49

def nil?
  @target.nil?
end

#pretty_print(pp) ⇒ Object



222
223
224
# File 'documented/common/settings/settings_proxy.rb', line 222

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

#proxy_detailsString

New method to show the proxy’s internal details Returns a string with the proxy’s internal details.

Returns:

  • (String)

    A detailed string representation of the proxy.



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

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)


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

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, including private methods.

Parameters:

  • method (Symbol)

    The method name to check.

  • include_private (Boolean) (defaults to: false)

    Whether to include private methods in the check.

Returns:

  • (Boolean)

    Returns true if the method is found, false otherwise.



386
387
388
# File 'documented/common/settings/settings_proxy.rb', line 386

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

#to_aObject

Collection conversions



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

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

#to_aryObject



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

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

#to_cObject



147
148
149
# File 'documented/common/settings/settings_proxy.rb', line 147

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

#to_enum(*args, &block) ⇒ Object

Special case for to_enum



198
199
200
201
202
203
204
205
206
# File 'documented/common/settings/settings_proxy.rb', line 198

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_fObject



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

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

#to_hObject



160
161
162
# File 'documented/common/settings/settings_proxy.rb', line 160

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

#to_hashObject



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

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

#to_iInteger

Note:

Returns 0 if the target does not respond to to_i.

Numeric conversions Converts the target to an integer.

Returns:

  • (Integer)

    The integer representation of the target.



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

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

#to_intObject



135
136
137
# File 'documented/common/settings/settings_proxy.rb', line 135

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

#to_ioObject



189
190
191
# File 'documented/common/settings/settings_proxy.rb', line 189

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

#to_json(*args) ⇒ String

added 20250620 for JSON.pretty_generate Converts the target to JSON format.

Examples:

json_output = proxy.to_json

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.



175
176
177
178
179
180
181
182
# File 'documented/common/settings/settings_proxy.rb', line 175

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_pathObject



193
194
195
# File 'documented/common/settings/settings_proxy.rb', line 193

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

#to_procObject

Other common conversions



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

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

#to_rObject



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

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

#to_sString

String conversions Converts the target to a string.

Returns:

  • (String)

    The string representation of the target.



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

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

#to_strString

Converts the target to a string, strict version.

Returns:

  • (String)

    The string representation of the target.

Raises:

  • (NoMethodError)

    If the target does not respond to to_str.



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

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

#to_symObject



123
124
125
# File 'documented/common/settings/settings_proxy.rb', line 123

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