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 dynamic delegation of method calls to a target object, while maintaining a context of scope and path for settings management.

See Also:

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, script_name: nil) ⇒ SettingsProxy

Initializes a new SettingsProxy instance.

Parameters:

  • settings_module (Module)

    the settings module to use for logging and management

  • scope (Object)

    the scope in which the settings are applied

  • path (Array)

    the path to the settings

  • target (Object)

    the target object to delegate method calls to

  • detached (Boolean) (defaults to: false)

    whether the proxy is detached from the target (default: false)

  • script_name (String, nil) (defaults to: nil)

    optional script name for logging



21
22
23
24
25
26
27
28
29
# File 'documented/common/settings/settings_proxy.rb', line 21

def initialize(settings_module, scope, path, target, detached: false, script_name: nil)
  @settings_module = settings_module # This should be the Settings module itself
  @scope  = scope
  @path   = path.dup
  @target = target
  @detached = detached
  @script_name = script_name
  @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}, script_name: #{@script_name.inspect}" })
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.

This method delegates the call to the target object if it responds to the method.

Parameters:

  • method (Symbol)

    the name of the method being called

  • args (Array)

    arguments for the method call

  • block (Proc)

    optional block for the method call

Returns:

  • (Object)

    the result of the method call or raises NoMethodError



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'documented/common/settings/settings_proxy.rb', line 286

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.



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

def path
  @path
end

#scopeObject (readonly)

Returns the value of attribute scope.



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

def scope
  @scope
end

#script_nameObject (readonly)

Returns the value of attribute script_name.



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

def script_name
  @script_name
end

#targetObject (readonly)

Returns the value of attribute target.



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

def target
  @target
end

Instance Method Details

#[](key) ⇒ Object



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

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, script_name: @script_name)
  else
    value
  end
end

#[]=(key, value) ⇒ Object



264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'documented/common/settings/settings_proxy.rb', line 264

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 object.

Parameters:

  • operator (Symbol)

    the operator to apply (e.g., :+, :-, :==)

  • other (Object)

    the other operand for the operation

Returns:

  • (Object)

    the result of the operation



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

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 its target.

Returns:

  • (Boolean)

    true if the proxy is detached, false otherwise



36
37
38
# File 'documented/common/settings/settings_proxy.rb', line 36

def detached?
  !!@detached
end

#each(&_block) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'documented/common/settings/settings_proxy.rb', line 215

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, script_name: @script_name)
      else
        yield item
      end
    end
  end
  self
end

#handle_method_result(result) ⇒ Object

Handles the result of method calls that may modify the target.

Parameters:

  • result (Object)

    the result of the method call

Returns:

  • (Object)

    self if the target was modified in-place, otherwise the result



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

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, script_name: @script_name)
  else
    result
  end
end

#handle_non_destructive_result(method, result) ⇒ Object

Handles the result of non-destructive method calls.

Parameters:

  • method (Symbol)

    the method that was called

  • result (Object)

    the result of the method call

Returns:

  • (Object)

    the processed result, potentially wrapped in a new SettingsProxy



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

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, script_name: @script_name)
        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, script_name: @script_name)
        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, script_name: @script_name)
      end
    else
      # Non-container results (e.g., Hash#keys) stay as plain values
      result
    end
  )
end

#hashInteger

Returns the hash value of the target object.

Returns:

  • (Integer)

    the hash value of the target



63
64
65
# File 'documented/common/settings/settings_proxy.rb', line 63

def hash
  @target.hash
end

#inspectObject



187
188
189
# File 'documented/common/settings/settings_proxy.rb', line 187

def inspect
  @target.inspect
end

#instance_of?(klass) ⇒ Boolean

Returns:

  • (Boolean)


207
208
209
# File 'documented/common/settings/settings_proxy.rb', line 207

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

#is_a?(klass) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#kind_of?(klass) ⇒ Boolean

Returns:

  • (Boolean)


203
204
205
# File 'documented/common/settings/settings_proxy.rb', line 203

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

#nil?Boolean

Returns:

  • (Boolean)


40
41
42
# File 'documented/common/settings/settings_proxy.rb', line 40

def nil?
  @target.nil?
end

#pretty_print(pp) ⇒ Object



195
196
197
# File 'documented/common/settings/settings_proxy.rb', line 195

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

#proxy_detailsObject



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

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

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

Returns:

  • (Boolean)


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

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

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

Returns:

  • (Boolean)


372
373
374
# File 'documented/common/settings/settings_proxy.rb', line 372

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

#to_aObject



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

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

#to_aryObject



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

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

#to_cObject



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

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

#to_enum(*args, &block) ⇒ Object



177
178
179
180
181
182
183
184
185
# File 'documented/common/settings/settings_proxy.rb', line 177

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



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

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

#to_hObject



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

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

#to_hashObject



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

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

#to_iObject



115
116
117
# File 'documented/common/settings/settings_proxy.rb', line 115

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

#to_intObject



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

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

#to_ioObject



169
170
171
# File 'documented/common/settings/settings_proxy.rb', line 169

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

#to_json(*args) ⇒ String

Converts the target object to JSON format.

Parameters:

  • args (Array)

    optional arguments for JSON conversion

Returns:

  • (String)

    the JSON representation of the target object

Raises:

  • (NoMethodError)

    if the target does not respond to :to_json



156
157
158
159
160
161
162
163
# File 'documented/common/settings/settings_proxy.rb', line 156

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



173
174
175
# File 'documented/common/settings/settings_proxy.rb', line 173

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

#to_procObject



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

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

#to_rObject



127
128
129
# File 'documented/common/settings/settings_proxy.rb', line 127

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

#to_sObject



103
104
105
# File 'documented/common/settings/settings_proxy.rb', line 103

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

#to_strObject



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

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

#to_symObject



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

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