legion-settings 1.3.25 → 1.3.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '028759fea7f4684e657e735676b17ebe147f71781e8bf3aac758fb226c8be81d'
4
- data.tar.gz: 56d57df1ca491e5976c2f654701aad0d93fd8d3cf8347c4a4e2c9eb3a487941a
3
+ metadata.gz: a4d1e8b1f3f0f96ee8db9d11604d914fc253e430827e37f7e30cc244151dc75b
4
+ data.tar.gz: 1ed13e9f16b97fc33acc72e484671794210ac4750aef5502fc86e685ec2f9324
5
5
  SHA512:
6
- metadata.gz: c75da04c00df4cd9db84c50c3327e9b5c80ad5a4137531cb1aad9d5e8efc8809736d97359c8de2c791aed1848e1b2b0c2948d27a50fee24ed3263e54de9d63ab
7
- data.tar.gz: 51d80d06a89f8ccc4284d6ff90741e2f083f08827ecd8c6ab853d7ff913aad24780ac22e25b0999fcd9464544161b15f5983bd824ea62ac3cfce6ca4e3b1acd2
6
+ metadata.gz: 47ab18a67eef8c35d86e52e9deec5716916754e5a84e0faec1ba3bbed4f24e9f4bb5c7749268eee23c96056ce85ef11c47787ac2df06fa5b918365730ddf6926
7
+ data.tar.gz: c76877c9e9a19f44c33a816a0580c265f721fe6dd0ef486eb06ec377ac6aedd5ae60fe52b4aaf4d8feb59bb18527f6127da40a0611d27718de3be63af5581b97
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Legion::Settings Changelog
2
2
 
3
+ ## [1.3.26] - 2026-04-02
4
+
5
+ ### Changed
6
+ - Added a runtime dependency on `legion-logging >= 1.4.0` and moved `Settings`, `Loader`, `Resolver`, `ProjectEnv`, and `AgentLoader` onto component-aware logging helpers
7
+ - Added `Legion::Logging::Helper` integration to top-level `Settings` access and DNS bootstrap logging so component tags and per-component levels apply consistently
8
+ - `extensions.default_extension_settings` now defaults to `{}` instead of injecting a synthetic logger config
9
+ - `Legion::Settings::Helper#settings` now returns `{}` when an extension has no explicit settings
10
+ - `region.default_affinity` now defaults to `any`
11
+ - `[]`, `dig`, and no-arg `load` now share the same project-env-aware load path, and the README now documents the actual `load`/`Loader.default_directories` split
12
+
13
+ ### Fixed
14
+ - `validate!` now clears stale validation errors before rebuilding state, and nested schema branches now fail when a hash-shaped setting is replaced with a scalar
15
+ - Loader mutation paths now invalidate indifferent-access and digest caches consistently, `load_module_default` no longer overwrites existing scalar settings, and resolver traversal now handles arrays of hashes
16
+ - `Resolver` now treats clustered Vault connectivity from `Legion::Crypt` as available for `vault://` resolution instead of relying only on the top-level `crypt.vault.connected` flag
17
+ - Logger-backed settings code paths now use consistent `log.debug/info/warn/error` behavior instead of mixed direct `Legion::Logging.*` calls
18
+
3
19
  ## [1.3.25] - 2026-03-31
4
20
 
5
21
  ### Added
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Configuration management module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Loads settings from JSON files, directories, and environment variables. Provides a unified `Legion::Settings[:key]` accessor used by all other Legion gems.
4
4
 
5
- **Version**: 1.3.22
5
+ **Version**: 1.3.26
6
6
 
7
7
  ## Installation
8
8
 
@@ -21,20 +21,28 @@ gem 'legion-settings'
21
21
  ```ruby
22
22
  require 'legion/settings'
23
23
 
24
- Legion::Settings.load(config_dir: './') # loads all .json files in the directory
24
+ Legion::Settings.load # loads defaults, env, DNS bootstrap, and nearest .legionio.env
25
+ Legion::Settings.load(config_dir: './settings') # also loads all .json files in the directory
25
26
 
26
27
  Legion::Settings[:client][:hostname]
27
- Legion::Settings[:transport][:connection][:host]
28
+ Legion::Settings.dig(:transport, :connection, :host)
28
29
  ```
29
30
 
30
- ### Config Paths (checked in order)
31
+ `[]` and `dig` will auto-load settings on first access, and implicit access follows the same overlay/project-env/base precedence as explicit `load`.
31
32
 
32
- 1. `/etc/legionio/`
33
- 2. `~/.legionio/settings/`
34
- 3. `~/legionio/`
35
- 4. `./settings/`
33
+ ### Config Loading
36
34
 
37
- Each Legion module registers its own defaults via `merge_settings` during startup.
35
+ `Legion::Settings.load` only consumes the paths you pass via `config_file`, `config_dir`, or `config_dirs`.
36
+
37
+ If a caller wants the canonical Legion search directories, use `Legion::Settings::Loader.default_directories`:
38
+
39
+ 1. `~/.legionio/settings`
40
+ 2. `/etc/legionio/settings` on Unix-like systems
41
+ 3. `%APPDATA%\\legionio\\settings` on Windows when `APPDATA` is present
42
+
43
+ `LegionIO` uses those directories during daemon boot. Library consumers can choose to pass any directory set they want.
44
+
45
+ Each Legion module registers its own defaults via `merge_settings` during startup, and the nearest `.legionio.env` file is merged on top of base settings. Request overlays applied through `with_overlay` take highest precedence.
38
46
 
39
47
  ### Secret Resolution
40
48
 
@@ -76,25 +84,25 @@ Legion::Settings.validate! # raises ValidationError if any settings are invalid
76
84
 
77
85
  # In development, warn instead of raising:
78
86
  # Set LEGION_DEV=true or Legion::Settings.set_prop(:dev, true)
79
- # validate! will warn to $stderr (or Legion::Logging) instead of raising
87
+ # validate! will warn through the configured logger instead of raising
80
88
  ```
81
89
 
82
90
  ### Logging Defaults
83
91
 
84
- The `logging` key includes a `transport` sub-section (new in 1.3.22) that controls whether log events are forwarded over the message bus:
92
+ The `logging` key includes a `transport` sub-section that controls whether log events are forwarded over the message bus:
85
93
 
86
94
  ```json
87
95
  {
88
96
  "logging": {
89
97
  "level": "info",
90
98
  "format": "text",
91
- "log_file": null,
99
+ "log_file": "./legionio/logs/legion.log",
92
100
  "log_stdout": true,
93
101
  "trace": true,
94
102
  "async": true,
95
103
  "include_pid": false,
96
104
  "transport": {
97
- "enabled": false,
105
+ "enabled": true,
98
106
  "forward_logs": true,
99
107
  "forward_exceptions": true
100
108
  }
@@ -102,7 +110,7 @@ The `logging` key includes a `transport` sub-section (new in 1.3.22) that contro
102
110
  }
103
111
  ```
104
112
 
105
- When `transport.enabled` is `true`, log events and unhandled exceptions are published to the AMQP bus so a central log consumer can aggregate them. Disabled by default to avoid a dependency on `legion-transport` at boot.
113
+ When `transport.enabled` is `true`, log events and unhandled exceptions are published to the AMQP bus so a central log consumer can aggregate them.
106
114
 
107
115
  ## Requirements
108
116
 
@@ -27,4 +27,5 @@ Gem::Specification.new do |spec|
27
27
  }
28
28
 
29
29
  spec.add_dependency 'legion-json', '>= 1.2.0'
30
+ spec.add_dependency 'legion-logging', '>= 1.5.0'
30
31
  end
@@ -2,10 +2,13 @@
2
2
 
3
3
  require 'yaml'
4
4
  require 'json'
5
+ require 'legion/logging'
5
6
 
6
7
  module Legion
7
8
  module Settings
8
9
  module AgentLoader
10
+ extend Legion::Logging::Helper
11
+
9
12
  EXTENSIONS = %w[.yaml .yml .json].freeze
10
13
  GLOB = '*.{yaml,yml,json}'
11
14
 
@@ -44,12 +47,17 @@ module Legion
44
47
 
45
48
  private
46
49
 
50
+ def resolve_logger_settings
51
+ raw_logging = Legion::Settings.loader&.settings&.dig(:logging) if Legion::Settings.respond_to?(:loader)
52
+ raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
53
+ end
54
+
47
55
  def log_debug(message)
48
- Legion::Logging.debug(message) if defined?(Legion::Logging)
56
+ log.debug(message)
49
57
  end
50
58
 
51
59
  def log_warn(message)
52
- defined?(Legion::Logging) ? Legion::Logging.warn(message) : warn(message)
60
+ log.warn(message)
53
61
  end
54
62
  end
55
63
  end
@@ -5,10 +5,13 @@ require 'json'
5
5
  require 'net/http'
6
6
  require 'resolv'
7
7
  require 'uri'
8
+ require 'legion/logging'
8
9
 
9
10
  module Legion
10
11
  module Settings
11
12
  class DnsBootstrap
13
+ include Legion::Logging::Helper
14
+
12
15
  CACHE_FILENAME = '_dns_bootstrap.json'
13
16
  HOSTNAME_PREFIX = 'legion-bootstrap'
14
17
  URL_PATH = '/legion/bootstrap.json'
@@ -28,7 +31,7 @@ module Legion
28
31
  Resolv.getaddress(@hostname)
29
32
  true
30
33
  rescue Resolv::ResolvError, Resolv::ResolvTimeout => e
31
- log_debug("Legion::Settings::DnsBootstrap#resolve? could not resolve #{@hostname}: #{e.message}")
34
+ log.debug("Legion::Settings::DnsBootstrap#resolve? could not resolve #{@hostname}: #{e.message}")
32
35
  false
33
36
  end
34
37
 
@@ -45,7 +48,7 @@ module Legion
45
48
 
46
49
  ::JSON.parse(response.body, symbolize_names: true)
47
50
  rescue StandardError => e
48
- log_warn("DNS bootstrap fetch failed for #{@url}: #{e.message}")
51
+ log.warn("DNS bootstrap fetch failed for #{@url}: #{e.message}")
49
52
  nil
50
53
  end
51
54
 
@@ -67,11 +70,11 @@ module Legion
67
70
  return nil unless File.exist?(@cache_path)
68
71
 
69
72
  raw = ::JSON.parse(File.read(@cache_path), symbolize_names: true)
70
- log_debug("DNS bootstrap cache hit: #{@cache_path}")
73
+ log.debug("DNS bootstrap cache hit: #{@cache_path}")
71
74
  raw.delete(:_dns_bootstrap_meta)
72
75
  raw
73
76
  rescue ::JSON::ParserError
74
- log_warn("DNS bootstrap cache corrupt, deleting: #{@cache_path}")
77
+ log.warn("DNS bootstrap cache corrupt, deleting: #{@cache_path}")
75
78
  FileUtils.rm_f(@cache_path)
76
79
  nil
77
80
  end
@@ -82,12 +85,9 @@ module Legion
82
85
 
83
86
  private
84
87
 
85
- def log_debug(message)
86
- Legion::Logging.debug(message) if defined?(Legion::Logging)
87
- end
88
-
89
- def log_warn(message)
90
- defined?(Legion::Logging) ? Legion::Logging.warn(message) : warn(message)
88
+ def resolve_logger_settings
89
+ raw_logging = Legion::Settings.loader&.settings&.dig(:logging) if Legion::Settings.respond_to?(:loader)
90
+ raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
91
91
  end
92
92
  end
93
93
  end
@@ -8,7 +8,7 @@ module Legion
8
8
  if Legion::Settings[:extensions]&.key?(ext_key)
9
9
  Legion::Settings[:extensions][ext_key]
10
10
  else
11
- { logger: { level: 'info', extended: false, internal: false } }
11
+ {}
12
12
  end
13
13
  end
14
14
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'resolv'
4
4
  require 'socket'
5
+ require 'digest'
6
+ require 'tmpdir'
7
+ require 'legion/logging'
5
8
  require 'legion/settings/os'
6
9
  require_relative 'dns_bootstrap'
7
10
 
@@ -9,6 +12,7 @@ module Legion
9
12
  module Settings
10
13
  class Loader
11
14
  include Legion::Settings::OS
15
+ include Legion::Logging::Helper
12
16
 
13
17
  class Error < RuntimeError; end
14
18
  attr_reader :warnings, :errors, :loaded_files, :settings
@@ -36,6 +40,7 @@ module Legion
36
40
  @settings = default_settings
37
41
  @indifferent_access = false
38
42
  @loaded_files = []
43
+ log.debug('Initialized Legion::Settings::Loader with default settings')
39
44
  end
40
45
 
41
46
  def dns_defaults
@@ -138,16 +143,14 @@ module Legion
138
143
  reload: false,
139
144
  reloading: false,
140
145
  auto_install_missing_lex: true,
141
- default_extension_settings: {
142
- logger: { level: 'info', trace: false, extended: false }
143
- },
146
+ default_extension_settings: {},
144
147
  logging: logging_defaults,
145
148
  absorbers: absorbers_defaults,
146
149
  transport: { connected: false },
147
150
  data: { connected: false },
148
151
  role: { profile: nil, extensions: [] },
149
152
  region: { current: nil, primary: nil, failover: nil, peers: [],
150
- default_affinity: 'prefer_local', data_residency: {} },
153
+ default_affinity: 'any', data_residency: {} },
151
154
  process: { role: 'full' },
152
155
  dns: dns_defaults
153
156
  }
@@ -171,7 +174,7 @@ module Legion
171
174
 
172
175
  def []=(key, value)
173
176
  @settings[key] = value
174
- @indifferent_access = false
177
+ mark_dirty!
175
178
  end
176
179
 
177
180
  def hexdigest
@@ -220,16 +223,14 @@ module Legion
220
223
  mod_name = config.keys.first
221
224
  log_debug("Loading module settings: #{mod_name}")
222
225
  @settings = deep_merge(config, @settings)
223
- @indifferent_access = false
226
+ mark_dirty!
224
227
  end
225
228
 
226
229
  def load_module_default(config)
227
230
  mod_name = config.keys.first
228
231
  log_debug("Loading module defaults: #{mod_name}")
229
- merged = deep_merge(@settings, config)
230
- deep_diff(@settings, merged) unless @loaded_files.empty?
231
- @settings = merged
232
- @indifferent_access = false
232
+ @settings = deep_merge(config, @settings)
233
+ mark_dirty!
233
234
  end
234
235
 
235
236
  def load_file(file)
@@ -238,11 +239,10 @@ module Legion
238
239
  begin
239
240
  contents = read_config_file(file)
240
241
  config = contents.empty? ? {} : Legion::JSON.load(contents)
241
- merged = deep_merge(@settings, config)
242
- deep_diff(@settings, merged) unless @loaded_files.empty?
243
- @settings = merged
244
- # @indifferent_access = false
242
+ @settings = deep_merge(@settings, config)
243
+ mark_dirty!
245
244
  @loaded_files << file
245
+ log.debug("Loaded settings file #{file}")
246
246
  rescue Legion::JSON::ParseError => e
247
247
  log_error("config file must be valid json: #{file}")
248
248
  log_error(" parse error: #{e.message}")
@@ -255,7 +255,7 @@ module Legion
255
255
  def load_directory(directory)
256
256
  path = directory.gsub(/\\(?=\S)/, '/')
257
257
  if File.readable?(path) && File.executable?(path)
258
- files = Dir.glob(File.join(path, '**{,/*/**}/*.json')).uniq
258
+ files = Dir.glob(File.join(path, '**', '*.json'))
259
259
  files.each { |file| load_file(file) }
260
260
  log_info("Settings: loaded directory #{path} (#{files.size} files)")
261
261
  else
@@ -268,7 +268,7 @@ module Legion
268
268
  if @settings[:client][:subscriptions].is_a?(Array)
269
269
  @settings[:client][:subscriptions] << "client:#{@settings[:client][:name]}"
270
270
  @settings[:client][:subscriptions].uniq!
271
- @indifferent_access = false
271
+ mark_dirty!
272
272
  else
273
273
  log_warn('unable to apply legion client overrides, reason: client subscriptions is not an array')
274
274
  end
@@ -290,6 +290,11 @@ module Legion
290
290
 
291
291
  private
292
292
 
293
+ def resolve_logger_settings
294
+ raw_logging = instance_variable_defined?(:@settings) ? @settings&.[](:logging) : nil
295
+ raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
296
+ end
297
+
293
298
  def load_dns_from_cache(bootstrap)
294
299
  config = bootstrap.read_cache
295
300
  start_dns_background_refresh(bootstrap) if config
@@ -311,7 +316,7 @@ module Legion
311
316
  hostname: bootstrap.hostname,
312
317
  url: bootstrap.url
313
318
  }
314
- @indifferent_access = false
319
+ mark_dirty!
315
320
  end
316
321
 
317
322
  def start_dns_background_refresh(bootstrap)
@@ -357,14 +362,14 @@ module Legion
357
362
  @settings[:api] ||= {}
358
363
  @settings[:api][:port] = ENV['LEGION_API_PORT'].to_i
359
364
  log_warn("using api port environment variable, api: #{@settings[:api]}")
360
- @indifferent_access = false
365
+ mark_dirty!
361
366
  end
362
367
 
363
368
  def load_privacy_env
364
369
  return unless ENV['LEGION_ENTERPRISE_PRIVACY'] == 'true'
365
370
 
366
371
  @settings[:enterprise_data_privacy] = true
367
- @indifferent_access = false
372
+ mark_dirty!
368
373
  end
369
374
 
370
375
  def read_config_file(file)
@@ -394,19 +399,6 @@ module Legion
394
399
  merged
395
400
  end
396
401
 
397
- def deep_diff(hash_one, hash_two)
398
- keys = hash_one.keys.concat(hash_two.keys).uniq
399
- keys.each_with_object({}) do |key, diff|
400
- next if hash_one[key] == hash_two[key]
401
-
402
- diff[key] = if hash_one[key].is_a?(Hash) && hash_two[key].is_a?(Hash)
403
- deep_diff(hash_one[key], hash_two[key])
404
- else
405
- [hash_one[key], hash_two[key]]
406
- end
407
- end
408
- end
409
-
410
402
  def create_loaded_tempfile!
411
403
  dir = ENV['LEGION_LOADED_TEMPFILE_DIR'] || Dir.tmpdir
412
404
  file_name = "legion_#{legion_service_name}_loaded_files"
@@ -415,6 +407,15 @@ module Legion
415
407
  path
416
408
  end
417
409
 
410
+ public
411
+
412
+ def mark_dirty!
413
+ @indifferent_access = false
414
+ @hexdigest = nil
415
+ end
416
+
417
+ private
418
+
418
419
  def legion_service_name
419
420
  File.basename($PROGRAM_NAME).split('-').last
420
421
  end
@@ -422,7 +423,7 @@ module Legion
422
423
  def system_hostname
423
424
  Socket.gethostname
424
425
  rescue StandardError => e
425
- Legion::Logging.debug("Legion::Settings::Loader#system_hostname failed: #{e.message}") if defined?(Legion::Logging)
426
+ log_debug("Legion::Settings::Loader#system_hostname failed: #{e.message}")
426
427
  'unknown'
427
428
  end
428
429
 
@@ -431,7 +432,7 @@ module Legion
431
432
  preferred = addresses.find { |a| rfc1918?(a.ip_address) }
432
433
  (preferred || addresses.first)&.ip_address || 'unknown'
433
434
  rescue StandardError => e
434
- Legion::Logging.debug("Legion::Settings::Loader#system_address failed: #{e.message}") if defined?(Legion::Logging)
435
+ log_debug("Legion::Settings::Loader#system_address failed: #{e.message}")
435
436
  'unknown'
436
437
  end
437
438
 
@@ -442,19 +443,19 @@ module Legion
442
443
  end
443
444
 
444
445
  def log_info(message)
445
- defined?(Legion::Logging) ? Legion::Logging.info(message) : $stdout.puts(message)
446
+ log.info(message)
446
447
  end
447
448
 
448
449
  def log_debug(message)
449
- Legion::Logging.debug(message) if defined?(Legion::Logging)
450
+ log.debug(message)
450
451
  end
451
452
 
452
453
  def log_warn(message)
453
- defined?(Legion::Logging) ? Legion::Logging.warn(message) : warn(message)
454
+ log.warn(message)
454
455
  end
455
456
 
456
457
  def log_error(message)
457
- defined?(Legion::Logging) ? Legion::Logging.error(message) : warn(message)
458
+ log.error(message)
458
459
  end
459
460
 
460
461
  def warning(message, data = {})
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/logging'
4
+
3
5
  module Legion
4
6
  module Settings
5
7
  # Per-project `.legionio.env` config file loader.
@@ -19,6 +21,8 @@ module Legion
19
21
  # Resolution order (lowest → highest priority):
20
22
  # global settings < .legionio.env < request overlay (#9)
21
23
  module ProjectEnv
24
+ extend Legion::Logging::Helper
25
+
22
26
  ENV_FILENAME = '.legionio.env'
23
27
 
24
28
  class << self
@@ -87,6 +91,11 @@ module Legion
87
91
 
88
92
  private
89
93
 
94
+ def resolve_logger_settings
95
+ raw_logging = Legion::Settings.loader&.settings&.dig(:logging) if Legion::Settings.respond_to?(:loader)
96
+ raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
97
+ end
98
+
90
99
  def set_nested(hash, keys, value)
91
100
  *parents, leaf = keys
92
101
  target = parents.reduce(hash) do |h, k|
@@ -108,11 +117,11 @@ module Legion
108
117
  end
109
118
 
110
119
  def log_debug(message)
111
- defined?(Legion::Logging) ? Legion::Logging.debug(message) : nil
120
+ log.debug(message)
112
121
  end
113
122
 
114
123
  def log_warn(message)
115
- defined?(Legion::Logging) ? Legion::Logging.warn(message) : warn(message)
124
+ log.warn(message)
116
125
  end
117
126
  end
118
127
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/logging'
4
+
3
5
  module Legion
4
6
  module Settings
5
7
  module Resolver
8
+ extend Legion::Logging::Helper
9
+
6
10
  VAULT_PATTERN = %r{\Avault://(.+?)#(.+)\z}
7
11
  ENV_PATTERN = %r{\Aenv://(.+)\z}
8
12
  LEASE_PATTERN = %r{\Alease://(.+?)#(.+)\z}
@@ -81,60 +85,79 @@ module Legion
81
85
  end
82
86
 
83
87
  def count_vault_refs(hash)
84
- return 0 unless hash.is_a?(Hash)
85
-
86
- hash.sum do |_key, value|
87
- case value
88
- when String then value.match?(VAULT_PATTERN) ? 1 : 0
89
- when Array then value.count { |v| v.is_a?(String) && v.match?(VAULT_PATTERN) }
90
- when Hash then count_vault_refs(value)
91
- else 0
92
- end
88
+ case hash
89
+ when String then hash.match?(VAULT_PATTERN) ? 1 : 0
90
+ when Array then hash.sum { |value| count_vault_refs(value) }
91
+ when Hash then hash.sum { |_key, value| count_vault_refs(value) }
92
+ else 0
93
93
  end
94
94
  end
95
95
 
96
96
  def vault_connected?
97
97
  return false unless defined?(Legion::Crypt)
98
98
  return false unless defined?(Legion::Settings)
99
+ return Legion::Crypt.vault_connected? if Legion::Crypt.respond_to?(:vault_connected?)
99
100
 
100
- Legion::Settings[:crypt][:vault][:connected] == true
101
+ Legion::Settings[:crypt][:vault][:connected] == true ||
102
+ (Legion::Crypt.respond_to?(:connected_clusters) && Legion::Crypt.connected_clusters.any?)
101
103
  rescue StandardError => e
102
104
  log_debug("Legion::Settings::Resolver#vault_connected? failed: #{e.message}")
103
105
  false
104
106
  end
105
107
 
106
- def walk(hash, path:, &block)
107
- hash.each do |key, value|
108
- current_path = path.empty? ? key.to_s : "#{path}.#{key}"
109
-
110
- case value
111
- when Hash
112
- walk(value, path: current_path, &block)
113
- when String
114
- next unless value.match?(URI_PATTERN)
115
-
116
- resolved = resolve_single(value)
117
- if resolved.nil?
118
- log_warn("Settings resolver: could not resolve #{current_path} (#{value})")
119
- block&.call(:unresolved)
120
- else
121
- hash[key] = resolved
122
- register_lease_ref(value, current_path) if value.match?(LEASE_PATTERN)
123
- block&.call(:resolved)
124
- end
125
- when Array
126
- next unless resolvable_chain?(value)
127
-
128
- resolved = resolve_chain(value)
129
- if resolved.nil?
130
- log_warn("Settings resolver: fallback chain exhausted for #{current_path}")
131
- block&.call(:unresolved)
132
- else
133
- hash[key] = resolved
134
- register_lease_refs_from_chain(value, current_path)
135
- block&.call(:resolved)
136
- end
108
+ def walk(hash, path:, &)
109
+ case hash
110
+ when Hash
111
+ hash.each do |key, value|
112
+ current_path = path.empty? ? key.to_s : "#{path}.#{key}"
113
+ walk_value(hash, key, value, current_path, &)
114
+ end
115
+ when Array
116
+ hash.each_with_index do |value, index|
117
+ current_path = "#{path}[#{index}]"
118
+ walk_value(hash, index, value, current_path, &)
119
+ end
120
+ end
121
+ end
122
+
123
+ def walk_value(container, key, value, current_path, &)
124
+ case value
125
+ when Hash
126
+ walk(value, path: current_path, &)
127
+ when String
128
+ handle_string_value(container, key, value, current_path) { |status| yield(status) if block_given? }
129
+ when Array
130
+ handle_array_value(container, key, value, current_path) { |status| yield(status) if block_given? }
131
+ end
132
+ end
133
+
134
+ def handle_string_value(container, key, value, current_path)
135
+ return unless value.match?(URI_PATTERN)
136
+
137
+ resolved = resolve_single(value)
138
+ if resolved.nil?
139
+ log_warn("Settings resolver: could not resolve #{current_path} (#{value})")
140
+ yield(:unresolved) if block_given?
141
+ else
142
+ container[key] = resolved
143
+ register_lease_ref(value, current_path) if value.match?(LEASE_PATTERN)
144
+ yield(:resolved) if block_given?
145
+ end
146
+ end
147
+
148
+ def handle_array_value(container, key, value, current_path, &)
149
+ if resolvable_chain?(value) && value.all? { |entry| !entry.is_a?(Hash) && !entry.is_a?(Array) }
150
+ resolved = resolve_chain(value)
151
+ if resolved.nil?
152
+ log_warn("Settings resolver: fallback chain exhausted for #{current_path}")
153
+ yield(:unresolved) if block_given?
154
+ else
155
+ container[key] = resolved
156
+ register_lease_refs_from_chain(value, current_path)
157
+ yield(:resolved) if block_given?
137
158
  end
159
+ else
160
+ walk(value, path: current_path, &)
138
161
  end
139
162
  end
140
163
 
@@ -207,40 +230,29 @@ module Legion
207
230
  end
208
231
 
209
232
  def count_lease_refs(hash)
210
- return 0 unless hash.is_a?(Hash)
211
-
212
- hash.sum do |_key, value|
213
- case value
214
- when String then value.match?(LEASE_PATTERN) ? 1 : 0
215
- when Array then value.count { |v| v.is_a?(String) && v.match?(LEASE_PATTERN) }
216
- when Hash then count_lease_refs(value)
217
- else 0
218
- end
233
+ case hash
234
+ when String then hash.match?(LEASE_PATTERN) ? 1 : 0
235
+ when Array then hash.sum { |value| count_lease_refs(value) }
236
+ when Hash then hash.sum { |_key, value| count_lease_refs(value) }
237
+ else 0
219
238
  end
220
239
  end
221
240
 
241
+ def resolve_logger_settings
242
+ raw_logging = Legion::Settings.loader&.settings&.dig(:logging) if Legion::Settings.respond_to?(:loader)
243
+ raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
244
+ end
245
+
222
246
  def log_info(message)
223
- if defined?(Legion::Logging)
224
- Legion::Logging.info(message)
225
- else
226
- $stdout.puts(message)
227
- end
247
+ log.info(message)
228
248
  end
229
249
 
230
250
  def log_warn(message)
231
- if defined?(Legion::Logging)
232
- Legion::Logging.warn(message)
233
- else
234
- warn(message)
235
- end
251
+ log.warn(message)
236
252
  end
237
253
 
238
254
  def log_debug(message)
239
- if defined?(Legion::Logging)
240
- Legion::Logging.debug(message)
241
- else
242
- $stdout.puts(message)
243
- end
255
+ log.debug(message)
244
256
  end
245
257
  end
246
258
  end
@@ -113,7 +113,14 @@ module Legion
113
113
  if constraint.is_a?(Hash) && constraint.key?(:type)
114
114
  validate_leaf(constraint, value, mod_name, current_path, errors)
115
115
  elsif constraint.is_a?(Hash)
116
- validate_node(constraint, value, mod_name, current_path, errors) if value.is_a?(Hash)
116
+ next if value.nil?
117
+
118
+ if value.is_a?(Hash)
119
+ validate_node(constraint, value, mod_name, current_path, errors)
120
+ else
121
+ errors << { module: mod_name, path: current_path,
122
+ message: "expected Hash, got #{value.class} (#{value.inspect})" }
123
+ end
117
124
  end
118
125
  end
119
126
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Settings
5
- VERSION = '1.3.25'
5
+ VERSION = '1.3.26'
6
6
  end
7
7
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/json'
4
+ require 'legion/logging'
4
5
  require 'legion/settings/version'
5
6
  require 'legion/json/parse_error'
6
7
  require 'legion/settings/loader'
@@ -17,6 +18,8 @@ module Legion
17
18
  class << self
18
19
  attr_accessor :loader
19
20
 
21
+ include Legion::Logging::Helper
22
+
20
23
  def load(options = {})
21
24
  has_config = options[:config_file] || options[:config_dir] || options[:config_dirs]&.any?
22
25
 
@@ -51,8 +54,8 @@ module Legion
51
54
  end
52
55
 
53
56
  def [](key)
54
- logger.info('Legion::Settings was not loading, auto loading now!') if @loader.nil?
55
- ensure_loader
57
+ logger.info('Legion::Settings was not loaded, auto-loading now') if @loader.nil?
58
+ load if @loader.nil?
56
59
  overlay_val = Overlay.overlay_for(key)
57
60
  base_val = @loader[key]
58
61
  if overlay_val.is_a?(Hash) && base_val.is_a?(Hash)
@@ -63,15 +66,23 @@ module Legion
63
66
  base_val
64
67
  end
65
68
  rescue NoMethodError, TypeError => e
66
- Legion::Logging.debug("Legion::Settings#[] key=#{key} failed: #{e.message}") if defined?(Legion::Logging)
69
+ logger.debug("Legion::Settings#[] key=#{key} failed: #{e.message}")
67
70
  nil
68
71
  end
69
72
 
70
73
  def dig(*keys)
71
- ensure_loader
72
- @loader.dig(*keys)
74
+ return nil if keys.empty?
75
+
76
+ logger.info('Legion::Settings was not loaded, auto-loading now') if @loader.nil?
77
+ load if @loader.nil?
78
+
79
+ root = self[keys.first]
80
+ return root if keys.length == 1
81
+ return nil unless root.respond_to?(:dig)
82
+
83
+ root.dig(*keys[1..])
73
84
  rescue NoMethodError, TypeError => e
74
- Legion::Logging.debug("Legion::Settings#dig keys=#{keys.inspect} failed: #{e.message}") if defined?(Legion::Logging)
85
+ logger.debug("Legion::Settings#dig keys=#{keys.inspect} failed: #{e.message}")
75
86
  nil
76
87
  end
77
88
 
@@ -116,7 +127,9 @@ module Legion
116
127
  # @return [String, nil] path to the loaded file, or nil if none found
117
128
  def load_project_env(start_dir: nil)
118
129
  ensure_loader
119
- ProjectEnv.load_into(@loader.settings, start_dir: start_dir)
130
+ path = ProjectEnv.load_into(@loader.settings, start_dir: start_dir)
131
+ @loader.mark_dirty! if path
132
+ path
120
133
  end
121
134
 
122
135
  def dev_mode?
@@ -124,7 +137,7 @@ module Legion
124
137
 
125
138
  Legion::Settings[:dev] ? true : false
126
139
  rescue StandardError => e
127
- Legion::Logging.debug("Legion::Settings#dev_mode? failed: #{e.message}") if defined?(Legion::Logging)
140
+ logger.debug("Legion::Settings#dev_mode? failed: #{e.message}")
128
141
  false
129
142
  end
130
143
 
@@ -133,7 +146,7 @@ module Legion
133
146
 
134
147
  Legion::Settings[:enterprise_data_privacy] ? true : false
135
148
  rescue StandardError => e
136
- Legion::Logging.debug("Legion::Settings#enterprise_privacy? failed: #{e.message}") if defined?(Legion::Logging)
149
+ logger.debug("Legion::Settings#enterprise_privacy? failed: #{e.message}")
137
150
  false
138
151
  end
139
152
 
@@ -180,20 +193,16 @@ module Legion
180
193
  end
181
194
 
182
195
  def logger
183
- @logger = if ::Legion.const_defined?('Logging')
184
- ::Legion::Logging
185
- else
186
- require 'logger'
187
- l = ::Logger.new($stdout)
188
- l.formatter = proc do |severity, datetime, _progname, msg|
189
- "[#{datetime.strftime('%Y-%m-%d %H:%M:%S %z')}] #{severity} #{msg}\n"
190
- end
191
- l
192
- end
196
+ log
193
197
  end
194
198
 
195
199
  private
196
200
 
201
+ def resolve_logger_settings
202
+ raw_logging = @loader&.settings&.dig(:logging)
203
+ raw_logging.is_a?(Hash) ? raw_logging : Legion::Logging::Settings.default
204
+ end
205
+
197
206
  def deep_merge_for_overlay(base, overlay)
198
207
  result = base.dup
199
208
  overlay.each do |key, value|
@@ -212,6 +221,7 @@ module Legion
212
221
 
213
222
  @loader = Legion::Settings::Loader.new
214
223
  @loader.load_env
224
+ logger.debug('Initialized Legion::Settings loader without config files')
215
225
  @loader
216
226
  end
217
227
 
@@ -224,11 +234,7 @@ module Legion
224
234
  label = count == 1 ? 'error' : 'errors'
225
235
  message = "Legion::Settings dev mode: #{count} configuration #{label} detected (not raising):\n"
226
236
  message += errs.map { |e| " [#{e[:module]}] #{e[:path]}: #{e[:message]}" }.join("\n")
227
- if ::Legion.const_defined?('Logging')
228
- ::Legion::Logging.warn(message)
229
- else
230
- warn(message)
231
- end
237
+ logger.warn(message)
232
238
  end
233
239
 
234
240
  def validate_module_on_merge(mod_name)
@@ -240,6 +246,7 @@ module Legion
240
246
  end
241
247
 
242
248
  def revalidate_all_modules
249
+ @loader.errors.clear
243
250
  schema.registered_modules.each do |mod_name|
244
251
  values = @loader[mod_name]
245
252
  next unless values.is_a?(Hash)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.25
4
+ version: 1.3.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 1.2.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: legion-logging
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.5.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.5.0
26
40
  description: A gem written to handle LegionIO Settings in a consistent way across
27
41
  extensions
28
42
  email: