Module: Lich::Common::Settings
- Defined in:
- documented/common/settings.rb
Overview
Centralized settings module providing persistent storage, retrieval, and navigation of per-script configuration data. Settings are persisted to a SQLite database and cached in-memory for performance. Supports nested Hash/Array structures with proxy-based live navigation.
Defined Under Namespace
Classes: CircularReferenceError
Constant Summary collapse
- LOG_LEVEL_NONE =
Logging Configuration
0- LOG_LEVEL_ERROR =
1- LOG_LEVEL_INFO =
2- LOG_LEVEL_DEBUG =
3- DEFAULT_SCOPE =
":".freeze
- @@log_level =
Default: logging disabled
LOG_LEVEL_NONE- @@log_prefix =
"[SettingsModule]".freeze
Class Method Summary collapse
-
.[](name) ⇒ Object
Retrieves a setting by name at the current path.
-
.[]=(name, value) ⇒ Object
Sets a setting by name at the current path, persisting to the database.
-
._log(level, prefix, message_proc) ⇒ Object
Internal logging method message_proc is a lambda/proc to delay string construction.
-
._reattach_live!(proxy) ⇒ Object
Internal non-destructive correction method (issue from Lich 5.12.6) non-destructive methods (.sort) cannot own a proxy that requires update (.push).
-
.auto ⇒ void
deprecated
Deprecated.
This method is not applicable
-
.auto=(_val) ⇒ void
deprecated
Deprecated.
This method is not applicable
-
.autoload ⇒ void
deprecated
Deprecated.
This method is not applicable
-
.clear ⇒ void
deprecated
Deprecated.
This method is not applicable
-
.container?(value) ⇒ Boolean
private
Checks if a value is a container type (Hash or Array).
-
.current_script_settings(scope = DEFAULT_SCOPE, script_name: nil) ⇒ Hash, Array
private
Retrieves a duplicate of the current script's settings for the given scope.
-
.get_log_level ⇒ Integer
Returns the current logging level of the Settings module.
-
.get_scoped_setting(scope_string, key_name, script_name: nil) ⇒ Object
private
Retrieves a single key-value setting from a specific scope without affecting the current path navigator state.
-
.get_value_from_container(container, key) ⇒ Object?
private
Retrieves a value from a Hash or Array container by key/index.
-
.load ⇒ Hash, Array
deprecated
Deprecated.
Use Settings.refresh_data instead
-
.method_missing(method, *args, &block) ⇒ Object
private
Delegates method calls to the path navigator, enabling method-chain-based navigation syntax (e.g., Settings.my_key.nested_value).
-
.navigate_to_path(create_missing = true, scope = DEFAULT_SCOPE) ⇒ Array
private
Navigates through nested Hash/Array structures using the current path navigator's path, optionally creating missing intermediate nodes.
-
.refresh_data(scope = DEFAULT_SCOPE) ⇒ Hash, Array
private
Clears the cache for the given scope and reloads settings from the database.
-
.reset_path_and_return(value) ⇒ Object
private
Resets the path navigator's internal path state and returns the given value.
-
.respond_to_missing?(method_name, include_private = false) ⇒ Boolean
private
Indicates that this module responds to method names delegated to the path navigator.
-
.root_proxy_for(scope, script_name: Script.current.name) ⇒ SettingsProxy
Creates a root-level SettingsProxy for the given scope.
-
.save ⇒ Object
Legacy Support Methods.
-
.save_all ⇒ Object
Deprecated No-Op Methods from Original.
-
.save_proxy_changes(proxy) ⇒ void
private
Persists changes made through a SettingsProxy to the database, handling both root-level and nested saves.
-
.save_to_database(data_to_save, scope = DEFAULT_SCOPE, script_name: nil) ⇒ void
private
Persists a settings structure to the database after unwrapping any proxies.
-
.set_log_level(level) ⇒ void
Sets the logging level for the Settings module.
-
.set_script_settings(scope = DEFAULT_SCOPE, name, value, script_name: nil) ⇒ Object
private
Sets a key-value pair in the script's settings at the current scope and path, persisting to the database.
-
.to_h(scope = DEFAULT_SCOPE) ⇒ Hash, Array
deprecated
Deprecated.
Use Settings.to_hash instead
-
.to_hash(scope = DEFAULT_SCOPE) ⇒ Hash, Array
Returns the current script's settings as an unwrapped Hash or Array (snapshot), suitable for serialization or inspection without proxies.
-
.wrap_value_if_container(value, scope, path_array, script_name: nil) ⇒ SettingsProxy, Object
private
Wraps a value in a SettingsProxy if it is a container (Hash or Array), otherwise returns the value unchanged.
Class Method Details
.[](name) ⇒ Object
Retrieves a setting by name at the current path. Wraps container values (Hash/Array) in SettingsProxy for live navigation. Activates safe navigation if the key is not found or value is nil.
550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 |
# File 'documented/common/settings.rb', line 550 def self.[](name) scope_to_use = DEFAULT_SCOPE _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[]: name: #{name.inspect}, current_path: #{@path_navigator.path.inspect}, safe_nav: #{@safe_navigation_active}" }) if @path_navigator.path.empty? data_for_scope = current_script_settings(scope_to_use) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[] (top-level): data_for_scope (DUP) (object_id: #{data_for_scope.object_id}): #{data_for_scope.inspect}" }) value = get_value_from_container(data_for_scope, name) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[] (top-level): value for '#{name}': #{value.inspect}" }) if value.nil? && !data_for_scope.is_a?(Array) && (!data_for_scope.is_a?(Hash) || !data_for_scope.key?(name)) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.[] (top-level): Key '#{name}' not found or value is nil. Activating safe_navigation." }) @safe_navigation_active = true end return reset_path_and_return(wrap_value_if_container(value, scope_to_use, [name])) else current_target, _ = navigate_to_path(false, scope_to_use) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[] (path-based): current_target: #{current_target.inspect}" }) value = get_value_from_container(current_target, name) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[] (path-based): value for '#{name}': #{value.inspect}" }) new_path = @path_navigator.path + [name] if value.nil? && !current_target.is_a?(Array) && (!current_target.is_a?(Hash) || !current_target.key?(name)) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.[] (path-based): Key '#{name}' not found or value is nil in path. Activating safe_navigation." }) @safe_navigation_active = true end return reset_path_and_return(wrap_value_if_container(value, scope_to_use, new_path)) end end |
.[]=(name, value) ⇒ Object
Sets a setting by name at the current path, persisting to the database. Disables safe navigation on assignment.
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 |
# File 'documented/common/settings.rb', line 586 def self.[]=(name, value) scope_to_use = DEFAULT_SCOPE _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[]=: name: #{name.inspect}, value: #{value.inspect}, current_path: #{@path_navigator.path.inspect}" }) @safe_navigation_active = false # Reset safe navigation on assignment if @path_navigator.path.empty? set_script_settings(scope_to_use, name, value) else target, root_settings = navigate_to_path(true, scope_to_use) if target && (target.is_a?(Hash) || target.is_a?(Array)) actual_value = value.is_a?(SettingsProxy) ? unwrap_proxies(value) : value target[name] = actual_value save_to_database(root_settings, scope_to_use) reset_path_and_return(value) else _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "Settings.[]=: Cannot assign to non-container or nil target at path #{@path_navigator.path.inspect}" }) reset_path_and_return(nil) end end end |
._log(level, prefix, message_proc) ⇒ Object
Internal logging method message_proc is a lambda/proc to delay string construction
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'documented/common/settings.rb', line 74 def self._log(level, prefix, ) return unless Lich.respond_to?(:log) return unless level <= @@log_level level_str = case level when LOG_LEVEL_ERROR then "[ERROR]" when LOG_LEVEL_INFO then "[INFO]" when LOG_LEVEL_DEBUG then "[DEBUG]" else "[UNKNOWN]" end begin = .call Lich.log("#{prefix} #{level_str} #{}") rescue => e Lich.log("#{prefix} [ERROR] Logging failed: #{e.} - Original message proc: #{.source_location if .respond_to?(:source_location)}") end end |
._reattach_live!(proxy) ⇒ Object
Internal non-destructive correction method (issue from Lich 5.12.6) non-destructive methods (.sort) cannot own a proxy that requires update (.push)
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'documented/common/settings.rb', line 95 def self._reattach_live!(proxy) script_name = Script.current.name scope = proxy.scope path = proxy.path _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "reattach_live!: scope=#{scope.inspect} path=#{path.inspect}" }) @path_navigator.reset_path @path_navigator.set_path(path) live, _root = @path_navigator.navigate_to_path(script_name, true, scope) if live.nil? _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "reattach_live!: failed to resolve live target for path=#{path.inspect} scope=#{scope.inspect}" }) return false end # Swap the proxy onto the live object via SettingsProxy API (encapsulated) # Centralizes invariants/logging within SettingsProxy itself. proxy.rebind_to_live!(live) true end |
.auto ⇒ void
This method is not applicable
This method returns an undefined value.
Legacy deprecated no-op method.
722 723 724 725 726 |
# File 'documented/common/settings.rb', line 722 def self.auto Lich.deprecated('Settings.auto', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.auto called (legacy deprecated no-op)." }) nil end |
.auto=(_val) ⇒ void
This method is not applicable
This method returns an undefined value.
Legacy deprecated no-op method.
713 714 715 716 |
# File 'documented/common/settings.rb', line 713 def self.auto=(_val) Lich.deprecated('Settings.auto=(val)', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.auto= called (legacy deprecated no-op)." }) end |
.autoload ⇒ void
This method is not applicable
This method returns an undefined value.
Legacy deprecated no-op method.
732 733 734 735 736 |
# File 'documented/common/settings.rb', line 732 def self.autoload Lich.deprecated('Settings.autoload', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.autoload called (legacy deprecated no-op)." }) nil end |
.clear ⇒ void
This method is not applicable
This method returns an undefined value.
Legacy deprecated no-op method.
702 703 704 705 706 |
# File 'documented/common/settings.rb', line 702 def self.clear Lich.deprecated('Settings.clear', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.clear called (legacy deprecated no-op)." }) nil end |
.container?(value) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Checks if a value is a container type (Hash or Array).
128 129 130 |
# File 'documented/common/settings.rb', line 128 def self.container?(value) value.is_a?(Hash) || value.is_a?(Array) end |
.current_script_settings(scope = DEFAULT_SCOPE, script_name: nil) ⇒ Hash, Array
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Retrieves a duplicate of the current script's settings for the given scope. Results are cached; this method returns a fresh copy from cache or loads from database if not cached.
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 |
# File 'documented/common/settings.rb', line 396 def self.current_script_settings(scope = DEFAULT_SCOPE, script_name: nil) # Use provided script_name or fall back to Script.current.name script_name = script_name || Script.current.name cache_key = "#{script_name || ""}::#{scope}" # Use an empty string if script_name is nil _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "current_script_settings: Request for scope: #{scope.inspect}, cache_key: #{cache_key}" }) cached_data = @settings_cache[cache_key] if cached_data _log(LOG_LEVEL_INFO, @@log_prefix, -> { "current_script_settings: Cache hit for #{cache_key} (object_id: #{cached_data.object_id}). Returning DUP: #{cached_data.inspect}" }) return cached_data.dup else _log(LOG_LEVEL_INFO, @@log_prefix, -> { "current_script_settings: Cache miss for #{cache_key}. Loading from DB." }) settings = @db_adapter.get_settings(script_name, scope) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "current_script_settings: Loaded from DB (object_id: #{settings.object_id}): #{settings.inspect}" }) @settings_cache[cache_key] = settings _log(LOG_LEVEL_INFO, @@log_prefix, -> { "current_script_settings: Stored in cache (object_id: #{@settings_cache[cache_key].object_id}). Returning DUP." }) return settings.dup end end |
.get_log_level ⇒ Integer
Returns the current logging level of the Settings module.
68 69 70 |
# File 'documented/common/settings.rb', line 68 def self.get_log_level @@log_level end |
.get_scoped_setting(scope_string, key_name, script_name: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Retrieves a single key-value setting from a specific scope without affecting the current path navigator state. Wraps container values in SettingsProxy.
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 |
# File 'documented/common/settings.rb', line 615 def self.get_scoped_setting(scope_string, key_name, script_name: nil) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "get_scoped_setting: scope: #{scope_string.inspect}, key: #{key_name.inspect}, script_name: #{script_name.inspect}" }) data_for_scope = current_script_settings(scope_string, script_name: script_name) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "get_scoped_setting: data_for_scope (DUP) (object_id: #{data_for_scope.object_id}): #{data_for_scope.inspect}" }) value = get_value_from_container(data_for_scope, key_name) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "get_scoped_setting: value for '#{key_name}': #{value.inspect}" }) if value.nil? && key_name key_absent_in_hash = data_for_scope.is_a?(Hash) && !data_for_scope.key?(key_name) key_invalid_for_array = data_for_scope.is_a?(Array) && (!key_name.is_a?(Integer) || key_name < 0 || key_name >= data_for_scope.length) if key_absent_in_hash || key_invalid_for_array || (data_for_scope.nil? || (data_for_scope.is_a?(Hash) && data_for_scope.empty?)) _log(Settings::LOG_LEVEL_INFO, @@log_prefix, -> { "get_scoped_setting: Key '#{key_name}' not found in scope '#{scope_string}'. Value will be nil, supporting '|| default' idiom." }) end end wrap_value_if_container(value, scope_string, key_name ? [key_name] : [], script_name: script_name) end |
.get_value_from_container(container, key) ⇒ Object?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Retrieves a value from a Hash or Array container by key/index. Returns nil if the key does not exist, the index is out of bounds, or the container is not a Hash/Array.
780 781 782 783 784 785 786 787 788 789 790 791 |
# File 'documented/common/settings.rb', line 780 def self.get_value_from_container(container, key) if container.is_a?(Hash) container[key] elsif container.is_a?(Array) && key.is_a?(Integer) container[key] elsif container.is_a?(Array) && !key.is_a?(Integer) _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "get_value_from_container: Attempted to access Array with non-Integer key: #{key.inspect}" }) nil else nil end end |
.load ⇒ Hash, Array
Use refresh_data instead
Legacy method; equivalent to refresh_data with default scope.
676 677 678 679 |
# File 'documented/common/settings.rb', line 676 def self.load _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.load called (legacy, aliasing to refresh_data)." }) refresh_data end |
.method_missing(method, *args, &block) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Delegates method calls to the path navigator, enabling method-chain-based navigation syntax (e.g., Settings.my_key.nested_value). Returns nil if safe navigation is active and a non-empty path exists.
748 749 750 751 752 753 754 755 756 757 758 759 760 |
# File 'documented/common/settings.rb', line 748 def self.method_missing(method, *args, &block) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "method_missing: method: #{method}, args: #{args.inspect}, path: #{@path_navigator.path.inspect}" }) if @safe_navigation_active && !@path_navigator.path.empty? if method.to_s.end_with?("=") _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "method_missing: Attempted assignment (#{method}) on a nil path due to safe navigation." }) return reset_path_and_return(nil) end return reset_path_and_return(nil) end _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "method_missing: Delegating to path_navigator: #{method}" }) @path_navigator.send(method, *args, &block) end |
.navigate_to_path(create_missing = true, scope = DEFAULT_SCOPE) ⇒ Array
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Navigates through nested Hash/Array structures using the current path navigator's path, optionally creating missing intermediate nodes.
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 |
# File 'documented/common/settings.rb', line 479 def self.navigate_to_path(create_missing = true, scope = DEFAULT_SCOPE) root_for_scope = current_script_settings(scope) return [root_for_scope, root_for_scope] if @path_navigator.path.empty? target = root_for_scope @path_navigator.path.each do |key| if target.is_a?(Hash) && target.key?(key) target = target[key] elsif target.is_a?(Array) && key.is_a?(Integer) && key >= 0 && key < target.length target = target[key] elsif create_missing && (target.is_a?(Hash) || target.is_a?(Array)) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "navigate_to_path: Creating missing segment '#{key}' in DUPPED structure for scope #{scope.inspect}" }) new_node = key.is_a?(Integer) ? [] : {} if target.is_a?(Hash) target[key] = new_node elsif target.is_a?(Array) && key.is_a?(Integer) target[key] = new_node end target = new_node else return [nil, root_for_scope] end end [target, root_for_scope] end |
.refresh_data(scope = DEFAULT_SCOPE) ⇒ Hash, Array
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Clears the cache for the given scope and reloads settings from the database.
453 454 455 456 457 458 459 |
# File 'documented/common/settings.rb', line 453 def self.refresh_data(scope = DEFAULT_SCOPE) script_name = Script.current.name cache_key = "#{script_name || ""}::#{scope}" # Use an empty string if script_name is nil _log(LOG_LEVEL_INFO, @@log_prefix, -> { "refresh_data: Deleting cache for scope: #{scope.inspect}, cache_key: #{cache_key}" }) @settings_cache.delete(cache_key) current_script_settings(scope) end |
.reset_path_and_return(value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resets the path navigator's internal path state and returns the given value. Used after navigation operations to reset state for subsequent calls.
467 468 469 |
# File 'documented/common/settings.rb', line 467 def self.reset_path_and_return(value) @path_navigator.reset_path_and_return(value) end |
.respond_to_missing?(method_name, include_private = false) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Indicates that this module responds to method names delegated to the path navigator.
768 769 770 |
# File 'documented/common/settings.rb', line 768 def self.respond_to_missing?(method_name, include_private = false) @path_navigator.respond_to?(method_name, include_private) || super end |
.root_proxy_for(scope, script_name: Script.current.name) ⇒ SettingsProxy
Creates a root-level SettingsProxy for the given scope.
This factory ensures the proxy directly targets the cached root object for the (script_name, scope) pair. By using the cached root, it avoids "identity drift" bugs where the proxy's @target differs from the object being persisted by save_proxy_changes.
191 192 193 194 195 196 197 198 199 |
# File 'documented/common/settings.rb', line 191 def self.root_proxy_for(scope, script_name: Script.current.name) raise ArgumentError, "scope must be a non-empty String" if scope.nil? || scope.to_s.strip.empty? script_name ||= "" cache_key = "#{script_name}::#{scope}" root = @settings_cache[cache_key] ||= @db_adapter.get_settings(script_name, scope) SettingsProxy.new(self, scope, [], root, script_name: script_name) end |
.save ⇒ Object
Legacy Support Methods
667 668 669 670 |
# File 'documented/common/settings.rb', line 667 def self.save _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.save called (legacy no-op)." }) :noop end |
.save_all ⇒ Object
Deprecated No-Op Methods from Original
692 693 694 695 696 |
# File 'documented/common/settings.rb', line 692 def self.save_all Lich.deprecated('Settings.save_all', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.save_all called (legacy deprecated no-op)." }) nil end |
.save_proxy_changes(proxy) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Persists changes made through a SettingsProxy to the database, handling both root-level and nested saves. Manages cache consistency and refreshes stale roots before save to prevent overwrites. Logs comprehensive diagnostics at each step.
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 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 320 321 322 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 357 358 359 360 361 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 |
# File 'documented/common/settings.rb', line 208 def self.save_proxy_changes(proxy) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Initiated for proxy.scope: #{proxy.scope.inspect}, proxy.path: #{proxy.path.inspect}, proxy.target_object_id: #{proxy.target.object_id}" }) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: proxy.target data: #{proxy.target.inspect}" }) path = proxy.path scope = proxy.scope # Use proxy.script_name if available (for InstanceSettings), fall back to Script.current.name script_name = proxy.respond_to?(:script_name) && proxy.script_name ? proxy.script_name : Script.current.name cache_key = "#{script_name || ""}::#{scope}" # Local helper to keep cache in sync with the just-persisted root sync_cache = lambda do |root_obj| cached = @settings_cache[cache_key] if cached && !cached.equal?(root_obj) if cached.is_a?(Hash) && root_obj.is_a?(Hash) cached.replace(root_obj) elsif cached.is_a?(Array) && root_obj.is_a?(Array) cached.clear cached.concat(root_obj) else @settings_cache[cache_key] = root_obj end elsif cached.nil? @settings_cache[cache_key] = root_obj end _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Cache synchronized post-save for #{cache_key}" }) end # --- Refresh-before-save to prevent stale-cache overwrites --- # IMPORTANT: If proxy.target IS the cached object, we must NOT replace it # from the database, as that would wipe out the changes we're trying to save. # Only refresh when the proxy is detached or working with a different object. fresh_root = @db_adapter.get_settings(script_name, scope) cached = @settings_cache[cache_key] if cached # Skip refresh if proxy.target is the same object as cached - we'd wipe out our changes! if cached.equal?(proxy.target) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Skipping refresh - proxy.target IS the cached object (object_id: #{cached.object_id})" }) current_root_for_scope = cached elsif cached.is_a?(Hash) && fresh_root.is_a?(Hash) cached.replace(fresh_root) current_root_for_scope = cached elsif cached.is_a?(Array) && fresh_root.is_a?(Array) cached.clear cached.concat(fresh_root) current_root_for_scope = cached else @settings_cache[cache_key] = fresh_root current_root_for_scope = fresh_root end _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_proxy_changes: Cache state for #{cache_key} (object_id: #{current_root_for_scope.object_id}): #{current_root_for_scope.inspect}" }) else @settings_cache[cache_key] = fresh_root current_root_for_scope = fresh_root _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_proxy_changes: Cache populated from DB for #{cache_key} (object_id: #{current_root_for_scope.object_id}): #{current_root_for_scope.inspect}" }) end # ------------------------------------------------------------- # EMPTY PATH -> Save *current root* (not proxy.target). Also covers detached "view" proxies. if path.empty? _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Empty path; saving CURRENT ROOT for scope #{scope.inspect}" }) unless current_root_for_scope.is_a?(Hash) || current_root_for_scope.is_a?(Array) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_proxy_changes: Root not a container; initializing {} for scope #{scope.inspect}" }) current_root_for_scope = {} @settings_cache[cache_key] = current_root_for_scope end if proxy.respond_to?(:detached?) && proxy.detached? _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Proxy is detached (view); persisting current root without copying view target." }) save_to_database(current_root_for_scope, scope, script_name: script_name) sync_cache.call(current_root_for_scope) return nil end # Root identity drift: sync proxy.target into cached root if different objects (same-type containers). if !current_root_for_scope.equal?(proxy.target) if proxy.target.is_a?(Hash) && current_root_for_scope.is_a?(Hash) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Root identity mismatch (cache #{current_root_for_scope.object_id} vs proxy #{proxy.target.object_id}); copying via Hash#replace" }) current_root_for_scope.replace(proxy.target) elsif proxy.target.is_a?(Array) && current_root_for_scope.is_a?(Array) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Root identity mismatch (Array); copying elements" }) current_root_for_scope.clear current_root_for_scope.concat(proxy.target) else _log(LOG_LEVEL_WARN, @@log_prefix, -> { "save_proxy_changes: Root/target type mismatch; persisting current root only (root=#{current_root_for_scope.class}, target=#{proxy.target.class})." }) end end save_to_database(current_root_for_scope, scope, script_name: script_name) sync_cache.call(current_root_for_scope) return nil end _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: script_name: #{script_name.inspect}, cache_key: #{cache_key}" }) # From here on, we're saving into a nested path. Ensure root is a container. unless current_root_for_scope.is_a?(Hash) || current_root_for_scope.is_a?(Array) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_proxy_changes: Root not a container; initializing {} for scope #{scope.inspect}" }) current_root_for_scope = {} @settings_cache[cache_key] = current_root_for_scope end parent_path = path[0...-1] leaf_key = path.last # Pre-navigation diagnostics _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Navigation preflight - parent_path=#{parent_path.inspect} (#{parent_path.map { |s| s.class }.inspect}), leaf_key=#{leaf_key.inspect} (#{leaf_key.class}), root_class=#{current_root_for_scope.class}" }) # Navigate **within the freshly-loaded root** (do NOT re-fetch via PathNavigator here). begin parent = current_root_for_scope parent_path.each_with_index do |seg, idx| next_seg = parent_path[idx + 1] if parent.is_a?(Hash) unless parent.key?(seg) parent[seg] = next_seg.is_a?(Integer) ? [] : {} _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Created #{parent[seg].class} at #{parent_path[0..idx].inspect} for scope #{scope.inspect}" }) end parent = parent[seg] elsif parent.is_a?(Array) unless seg.is_a?(Integer) && seg >= 0 _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Non-integer or negative index #{seg.inspect} for Array at #{parent_path[0..idx].inspect} in scope #{scope.inspect}" }) return nil end if seg >= parent.length (parent.length..seg).each { parent << nil } _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Extended Array to index #{seg} at #{parent_path[0..idx].inspect} for scope #{scope.inspect}" }) end parent[seg] ||= (next_seg.is_a?(Integer) ? [] : {}) parent = parent[seg] else _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Parent is not a container at #{parent_path[0..idx].inspect} (#{parent.class}) for scope #{scope.inspect}" }) return nil end end rescue => e _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Local navigation raised #{e.class}: #{e.}. "\ "scope=#{scope.inspect}, script_name=#{script_name.inspect}, cache_key=#{cache_key}, "\ "parent_path=#{parent_path.inspect}, leaf_key=#{leaf_key.inspect}" }) bt = (e.backtrace || [])[0, 5] _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Backtrace (top 5): #{bt.join(' | ')}" }) return nil end _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Navigated/created parent (object_id: #{parent.object_id}, class=#{parent.class}): #{parent.inspect}" }) if parent.is_a?(Hash) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Setting Hash key #{leaf_key.inspect} with proxy.target (object_id: #{proxy.target.object_id})" }) parent[leaf_key] = proxy.target elsif parent.is_a?(Array) && leaf_key.is_a?(Integer) if leaf_key < 0 _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Negative array index #{leaf_key} not supported at path #{path.inspect} in scope #{scope.inspect}" }) return nil end if leaf_key >= parent.length (parent.length..leaf_key).each { parent << nil } _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Extended Array to index #{leaf_key} for parent at path #{parent_path.inspect}" }) end parent[leaf_key] = proxy.target _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Set Array index #{leaf_key} with proxy.target (object_id: #{proxy.target.object_id})" }) else _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Cannot set value at path #{path.inspect} in scope #{scope.inspect}; "\ "parent_class=#{parent.class}, leaf_key_class=#{leaf_key.class}" }) return nil end _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: root after update (object_id: #{current_root_for_scope.object_id}): #{current_root_for_scope.inspect}" }) save_to_database(current_root_for_scope, scope, script_name: script_name) sync_cache.call(current_root_for_scope) end |
.save_to_database(data_to_save, scope = DEFAULT_SCOPE, script_name: nil) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Does nothing and returns nil if script_name is nil or empty
This method returns an undefined value.
Persists a settings structure to the database after unwrapping any proxies. Updates the in-memory cache with the unwrapped data.
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
# File 'documented/common/settings.rb', line 425 def self.save_to_database(data_to_save, scope = DEFAULT_SCOPE, script_name: nil) # Use provided script_name or fall back to Script.current.name script_name = script_name || Script.current.name if script_name.nil? || script_name.empty? _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_to_database: Aborting save. script_name is nil or empty. Scope: #{scope.inspect}. Data will NOT be persisted." }) return nil # Explicitly return nil end cache_key = "#{script_name}::#{scope}" # script_name is guaranteed to be non-nil/non-empty here _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_to_database: Saving for script: '#{script_name}', scope: #{scope.inspect}, cache_key: #{cache_key}" }) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_to_database: Data BEFORE unwrap_proxies (object_id: #{data_to_save.object_id}): #{data_to_save.inspect}" }) unwrapped_settings = unwrap_proxies(data_to_save) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_to_database: Data AFTER unwrap_proxies (object_id: #{unwrapped_settings.object_id}): #{unwrapped_settings.inspect}" }) @db_adapter.save_settings(script_name, unwrapped_settings, scope) _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_to_database: Data saved to DB for script '#{script_name}'." }) @settings_cache[cache_key] = unwrapped_settings _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_to_database: Cache updated for #{cache_key} with saved data (object_id: #{@settings_cache[cache_key].object_id})." }) end |
.set_log_level(level) ⇒ void
This method returns an undefined value.
Sets the logging level for the Settings module.
52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'documented/common/settings.rb', line 52 def self.set_log_level(level) numeric_level = case level when :none, LOG_LEVEL_NONE then LOG_LEVEL_NONE when :error, LOG_LEVEL_ERROR then LOG_LEVEL_ERROR when :info, LOG_LEVEL_INFO then LOG_LEVEL_INFO when :debug, LOG_LEVEL_DEBUG then LOG_LEVEL_DEBUG else _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "Invalid log level specified: #{level.inspect}. Defaulting to NONE." }) LOG_LEVEL_NONE end @@log_level = numeric_level end |
.set_script_settings(scope = DEFAULT_SCOPE, name, value, script_name: nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Sets a key-value pair in the script's settings at the current scope and path, persisting to the database.
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 |
# File 'documented/common/settings.rb', line 514 def self.set_script_settings(scope = DEFAULT_SCOPE, name, value, script_name: nil) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "set_script_settings: scope: #{scope.inspect}, name: #{name.inspect}, value: #{value.inspect}, script_name: #{script_name.inspect}, current_path: #{@path_navigator.path.inspect}" }) unwrapped_value = unwrap_proxies(value) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "set_script_settings: unwrapped_value: #{unwrapped_value.inspect}" }) current_root = current_script_settings(scope, script_name: script_name) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "set_script_settings: current_root (DUP) for scope #{scope.inspect} (object_id: #{current_root.object_id}): #{current_root.inspect}" }) if @path_navigator.path.empty? _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "set_script_settings: Path is empty. Setting '#{name}' on current_root." }) current_root[name] = unwrapped_value save_to_database(current_root, scope, script_name: script_name) else if !@path_navigator.path.empty? _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "set_script_settings: WARNING: Called with non-empty path_navigator path: #{@path_navigator.path.inspect}. This is unusual for Char/GameSettings direct assignment." }) end if current_root.is_a?(Hash) current_root[name] = unwrapped_value save_to_database(current_root, scope, script_name: script_name) else _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "set_script_settings: current_root for scope #{scope.inspect} is not a Hash. Cannot set key '#{name}'. Root class: #{current_root.class}" }) end end reset_path_and_return(value) end |
.to_h(scope = DEFAULT_SCOPE) ⇒ Hash, Array
686 687 688 689 |
# File 'documented/common/settings.rb', line 686 def self.to_h(scope = DEFAULT_SCOPE) # Added scope to match to_hash for consistency if used directly _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.to_h called (legacy, aliasing to to_hash)." }) self.to_hash(scope) end |
.to_hash(scope = DEFAULT_SCOPE) ⇒ Hash, Array
Returns the current script's settings as an unwrapped Hash or Array (snapshot), suitable for serialization or inspection without proxies.
658 659 660 661 662 663 664 |
# File 'documented/common/settings.rb', line 658 def self.to_hash(scope = DEFAULT_SCOPE) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "to_hash: scope: #{scope.inspect}" }) data = current_script_settings(scope) unwrapped_data = unwrap_proxies(data) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "to_hash: Returning unwrapped data (snapshot): #{unwrapped_data.inspect}" }) return unwrapped_data end |
.wrap_value_if_container(value, scope, path_array, script_name: nil) ⇒ SettingsProxy, Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Wraps a value in a SettingsProxy if it is a container (Hash or Array), otherwise returns the value unchanged.
642 643 644 645 646 647 648 649 650 651 |
# File 'documented/common/settings.rb', line 642 def self.wrap_value_if_container(value, scope, path_array, script_name: nil) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "wrap_value_if_container: value_class: #{value.class}, scope: #{scope.inspect}, path: #{path_array.inspect}, script_name: #{script_name.inspect}" }) if container?(value) proxy = SettingsProxy.new(self, scope, path_array, value, script_name: script_name) _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "wrap_value_if_container: Wrapped in proxy: #{proxy.inspect}" }) return proxy else return value end end |