Class: Legion::Settings::Loader
- Inherits:
-
Object
- Object
- Legion::Settings::Loader
- Includes:
- Logging::Helper, OS
- Defined in:
- lib/legion/settings/loader.rb
Defined Under Namespace
Classes: Error
Instance Attribute Summary collapse
-
#errors ⇒ Object
readonly
Returns the value of attribute errors.
-
#loaded_files ⇒ Object
readonly
Returns the value of attribute loaded_files.
-
#merged_modules ⇒ Object
readonly
Returns the value of attribute merged_modules.
-
#settings ⇒ Object
readonly
Returns the value of attribute settings.
-
#warnings ⇒ Object
readonly
Returns the value of attribute warnings.
Class Method Summary collapse
Instance Method Summary collapse
-
#[](key) ⇒ Object
Direct key lookup — does NOT trigger indifferent_access! rebuild.
- #[]=(key, value) ⇒ Object
- #client_defaults ⇒ Object
-
#default_settings ⇒ Object
No more per-module defaults methods in the Loader.
-
#dig(*keys) ⇒ Object
Direct nested lookup — does NOT trigger indifferent_access! rebuild.
- #dns_defaults ⇒ Object
- #hexdigest ⇒ Object
-
#initialize ⇒ Loader
constructor
A new instance of Loader.
- #load_client_overrides ⇒ Object
- #load_directory(directory) ⇒ Object
- #load_dns_bootstrap(cache_dir: nil) ⇒ Object
- #load_env ⇒ Object
- #load_file(file) ⇒ Object
- #load_module_default(config) ⇒ Object
- #load_module_settings(config) ⇒ Object
- #load_overrides! ⇒ Object
- #mark_dirty! ⇒ Object
-
#resolve_fqdn! ⇒ String?
Lazily resolve the FQDN on first access instead of blocking at init.
- #set_env! ⇒ Object
- #to_hash ⇒ Object
- #validate ⇒ Object
Methods included from OS
linux?, mac?, #os, unix?, windows?
Constructor Details
#initialize ⇒ Loader
Returns a new instance of Loader.
39 40 41 42 43 44 45 46 47 |
# File 'lib/legion/settings/loader.rb', line 39 def initialize @warnings = [] @errors = [] @settings = default_settings @indifferent_access = false @loaded_files = [] @merged_modules = {} log.debug('Initialized Legion::Settings::Loader with default settings') end |
Instance Attribute Details
#errors ⇒ Object (readonly)
Returns the value of attribute errors.
20 21 22 |
# File 'lib/legion/settings/loader.rb', line 20 def errors @errors end |
#loaded_files ⇒ Object (readonly)
Returns the value of attribute loaded_files.
20 21 22 |
# File 'lib/legion/settings/loader.rb', line 20 def loaded_files @loaded_files end |
#merged_modules ⇒ Object (readonly)
Returns the value of attribute merged_modules.
20 21 22 |
# File 'lib/legion/settings/loader.rb', line 20 def merged_modules @merged_modules end |
#settings ⇒ Object (readonly)
Returns the value of attribute settings.
20 21 22 |
# File 'lib/legion/settings/loader.rb', line 20 def settings @settings end |
#warnings ⇒ Object (readonly)
Returns the value of attribute warnings.
20 21 22 |
# File 'lib/legion/settings/loader.rb', line 20 def warnings @warnings end |
Class Method Details
.default_directories ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/legion/settings/loader.rb', line 22 def self.default_directories env_dirs = ENV.fetch('LEGION_SETTINGS_DIRS', nil) if env_dirs && !env_dirs.strip.empty? env_dirs_list = env_dirs.split(File::PATH_SEPARATOR).map(&:strip).reject(&:empty?).map { |p| File.(p) } return env_dirs_list unless env_dirs_list.empty? end dirs = [File.('~/.legionio/settings')] if OS.windows? appdata = ENV.fetch('APPDATA', nil) dirs << File.join(appdata, 'legionio', 'settings') if appdata && !appdata.strip.empty? else dirs << '/etc/legionio/settings' end dirs end |
Instance Method Details
#[](key) ⇒ Object
Direct key lookup — does NOT trigger indifferent_access! rebuild. This is the hot path called by every Settings access. Supports both symbol and string keys without converting the whole tree.
148 149 150 151 152 153 |
# File 'lib/legion/settings/loader.rb', line 148 def [](key) result = @settings[key] return result unless result.nil? && key.is_a?(String) @settings[key.to_sym] end |
#[]=(key, value) ⇒ Object
167 168 169 170 |
# File 'lib/legion/settings/loader.rb', line 167 def []=(key, value) @settings[key] = value mark_dirty! end |
#client_defaults ⇒ Object
69 70 71 72 73 74 75 76 |
# File 'lib/legion/settings/loader.rb', line 69 def client_defaults { hostname: system_hostname, address: system_address, name: "#{::Socket.gethostname.tr('.', '_')}.#{::Process.pid}", ready: false } end |
#default_settings ⇒ Object
No more per-module defaults methods in the Loader. Tier 1 deps (json, logging) are called directly in default_settings. Tier 2 libraries (transport, cache, etc.) self-register via Legion::Settings.register_library when they load.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/legion/settings/loader.rb', line 83 def default_settings { # --- Tier 1: gemspec dependencies (always installed with legion-settings) --- # legion-logging: always available, has Settings.default logging: Legion::Logging::Settings.default, # legion-json: always available, no Settings module yet — stub # until legion-json adds Legion::JSON::Settings.default json: Concurrent::Hash.new, # --- Structural: owned by legion-settings itself --- client: client_defaults, cluster: Concurrent::Hash.new, dns: dns_defaults, extensions: Concurrent::Hash[ core: %w[ lex-node lex-tasker lex-scheduler lex-health lex-ping lex-telemetry lex-metering lex-log lex-audit lex-conditioner lex-transformer lex-exec lex-lex lex-codegen ], ai: %w[lex-claude lex-openai lex-gemini], gaia: %w[lex-tick lex-mesh lex-apollo], categories: { core: { type: :list, tier: 1 }, ai: { type: :list, tier: 2 }, gaia: { type: :list, tier: 3 }, agentic: { type: :prefix, tier: 4 } }, blocked: [], reserved_prefixes: %w[core ai agentic gaia], reserved_words: %w[transport cache crypt data settings json logging llm rbac legion], agentic: { allowed: nil, blocked: [] }, parallel_pool_size: 24 ], reload: false, reloading: false, auto_install_missing_lex: true, default_extension_settings: {}, role: { profile: nil, extensions: [] }, region: { current: nil, primary: nil, failover: nil, peers: [], default_affinity: 'any', data_residency: {} }, process: { role: 'full' }, # --- Tier 2: stubs for libraries that self-register via register_library --- # These ensure Settings[:key] returns a hash (not nil) before # the owning library loads. The library replaces these with its # full defaults when it calls Legion::Settings.register_library. absorbers: Concurrent::Hash.new, cache: Concurrent::Hash.new, crypt: Concurrent::Hash.new, data: Concurrent::Hash.new, transport: Concurrent::Hash.new } end |
#dig(*keys) ⇒ Object
Direct nested lookup — does NOT trigger indifferent_access! rebuild.
156 157 158 159 160 161 162 163 164 165 |
# File 'lib/legion/settings/loader.rb', line 156 def dig(*keys) keys.reduce(self) do |current, key| return nil unless current.respond_to?(:[]) value = current.is_a?(Loader) ? current[key] : (current[key] || current[key.to_s]) return nil if value.nil? && !current.is_a?(Loader) value end end |
#dns_defaults ⇒ Object
49 50 51 52 53 54 55 56 57 58 |
# File 'lib/legion/settings/loader.rb', line 49 def dns_defaults resolv_config = read_resolv_config { fqdn: nil, # lazy — resolved on first access via resolve_fqdn! default_domain: resolv_config[:search_domains]&.first, search_domains: resolv_config[:search_domains] || [], nameservers: resolv_config[:nameservers] || [], bootstrap: { enabled: true } } end |
#hexdigest ⇒ Object
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/legion/settings/loader.rb', line 172 def hexdigest if @hexdigest && @indifferent_access @hexdigest else hash = case legion_service_name when 'client', 'rspec' to_hash else to_hash.reject do |key, _value| key.to_s == 'client' end end @hexdigest = Digest::SHA256.hexdigest(hash.to_s) end end |
#load_client_overrides ⇒ Object
259 260 261 262 263 264 265 266 267 268 |
# File 'lib/legion/settings/loader.rb', line 259 def load_client_overrides @settings[:client][:subscriptions] ||= [] if @settings[:client][:subscriptions].is_a?(Array) @settings[:client][:subscriptions] << "client:#{@settings[:client][:name]}" @settings[:client][:subscriptions].uniq! mark_dirty! else log.warn('unable to apply legion client overrides, reason: client subscriptions is not an array') end end |
#load_directory(directory) ⇒ Object
248 249 250 251 252 253 254 255 256 257 |
# File 'lib/legion/settings/loader.rb', line 248 def load_directory(directory) path = directory.gsub(/\\(?=\S)/, '/') if File.readable?(path) && File.executable?(path) files = Dir.glob(File.join(path, '**', '*.json')) files.each { |file| load_file(file) } log.info("Settings: loaded directory #{path} (#{files.size} files)") else load_error('insufficient permissions for loading', directory: directory) end end |
#load_dns_bootstrap(cache_dir: nil) ⇒ Object
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/legion/settings/loader.rb', line 193 def load_dns_bootstrap(cache_dir: nil) return if ENV['LEGION_DNS_BOOTSTRAP'] == 'false' domain = @settings.dig(:dns, :default_domain) return unless domain return unless @settings.dig(:dns, :bootstrap, :enabled) dir = cache_dir || File.('~/.legionio/settings') bootstrap = DnsBootstrap.new(default_domain: domain, cache_dir: dir) config = if bootstrap.cache_exists? load_dns_from_cache(bootstrap) else load_dns_first_boot(bootstrap) end return unless config merge_dns_config(config, bootstrap) end |
#load_env ⇒ Object
188 189 190 191 |
# File 'lib/legion/settings/loader.rb', line 188 def load_env load_api_env load_privacy_env end |
#load_file(file) ⇒ Object
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/legion/settings/loader.rb', line 229 def load_file(file) log.debug("Trying to load file #{file}") if File.file?(file) && File.readable?(file) begin contents = read_config_file(file) config = contents.empty? ? {} : Legion::JSON.load(contents) @settings = deep_merge(@settings, config) mark_dirty! @loaded_files << file log.debug("Loaded settings file #{file}") rescue Legion::JSON::ParseError => e log.error("config file must be valid json: #{file}") log.error(" parse error: #{e.}") end else log.warn("Config file does not exist or is not readable file:#{file}") end end |
#load_module_default(config) ⇒ Object
222 223 224 225 226 227 |
# File 'lib/legion/settings/loader.rb', line 222 def load_module_default(config) mod_name = config.keys.first log.debug("Loading module defaults: #{mod_name}") @settings = deep_merge(config, @settings) mark_dirty! end |
#load_module_settings(config) ⇒ Object
214 215 216 217 218 219 220 |
# File 'lib/legion/settings/loader.rb', line 214 def load_module_settings(config) mod_name = config.keys.first log.debug("Loading module settings: #{mod_name}") @merged_modules = deep_merge(@merged_modules, config) @settings = deep_merge(config, @settings) mark_dirty! end |
#load_overrides! ⇒ Object
270 271 272 |
# File 'lib/legion/settings/loader.rb', line 270 def load_overrides! load_client_overrides if %w[client rspec].include?(legion_service_name) end |
#mark_dirty! ⇒ Object
391 392 393 394 |
# File 'lib/legion/settings/loader.rb', line 391 def mark_dirty! @indifferent_access = false @hexdigest = nil end |
#resolve_fqdn! ⇒ String?
Lazily resolve the FQDN on first access instead of blocking at init.
63 64 65 66 67 |
# File 'lib/legion/settings/loader.rb', line 63 def resolve_fqdn! return @settings[:dns][:fqdn] if @settings.dig(:dns, :fqdn) @settings[:dns][:fqdn] = detect_fqdn end |
#set_env! ⇒ Object
274 275 276 |
# File 'lib/legion/settings/loader.rb', line 274 def set_env! ENV['LEGION_LOADED_TEMPFILE'] = create_loaded_tempfile! end |
#to_hash ⇒ Object
137 138 139 140 141 142 143 |
# File 'lib/legion/settings/loader.rb', line 137 def to_hash unless @indifferent_access indifferent_access! @hexdigest = nil end @settings end |