legion-settings 1.2.0 → 1.3.0

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: 295484b08142fb1f6411ac9fbcd7d8f2222070c9a24d365c32df967281dc7f99
4
- data.tar.gz: 9c2edecfcd659f6646256444fac34769c4d909df8247dab6965bbd0533067c23
3
+ metadata.gz: b2fb957d328761484c232467b3f37dfea86359a31726a6f3910e2e02838e3977
4
+ data.tar.gz: f10dcf26575430c481b466ebf490e3331d2d1c8ba841b438ca15560c8554c965
5
5
  SHA512:
6
- metadata.gz: 30905e3f6ebbd1195ad34cea678b187c0e05700d65ed2593dd9ad7d735e8f7db771f16cdc86ae87b99abf0887c3a8ddd3429a4ae1073ea65ba904fb894578d32
7
- data.tar.gz: 81349d2d33e693b560b78842a4530eedd6aaf9a723785bb31a4783255b1bd0101e8089817f616a58f99ee3737b3f3fdecbfe5c9ec56a392f906b01ec3db45d70
6
+ metadata.gz: b69cabe6c5265040c40791eb9b0c9c284c42438b11b9c146113b57fa9dc6c87d4c28072a3ce569cf9a5f34d3151bac4adaf8ed0a923fd8cecbcf40b8aae6103c
7
+ data.tar.gz: cbeaf56feeaca1cb4a02fdf76cf501e82d5619d489228570ec0906033e437249e3d85e98db652072c73b145bb80162f66f206dc6e1b0c79266606b7e647bf922
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ ci:
9
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
10
+
11
+ release:
12
+ needs: ci
13
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
14
+ uses: LegionIO/.github/.github/workflows/release.yml@main
15
+ secrets:
16
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.rubocop.yml CHANGED
@@ -1,18 +1,50 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.4
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
1
6
  Layout/LineLength:
2
- Max: 120
7
+ Max: 160
8
+
9
+ Layout/SpaceAroundEqualsInParameterDefault:
10
+ EnforcedStyle: space
11
+
12
+ Layout/HashAlignment:
13
+ EnforcedHashRocketStyle: table
14
+ EnforcedColonStyle: table
15
+
3
16
  Metrics/MethodLength:
4
- Max: 30
17
+ Max: 50
18
+
5
19
  Metrics/ClassLength:
6
20
  Max: 1500
21
+
22
+ Metrics/ModuleLength:
23
+ Max: 1500
24
+
7
25
  Metrics/BlockLength:
8
- Max: 50
26
+ Max: 40
27
+ Exclude:
28
+ - 'spec/**/*'
29
+
30
+ Metrics/AbcSize:
31
+ Max: 60
32
+
33
+ Metrics/CyclomaticComplexity:
34
+ Max: 15
35
+
36
+ Metrics/PerceivedComplexity:
37
+ Max: 17
38
+
9
39
  Style/Documentation:
10
40
  Enabled: false
11
- AllCops:
12
- TargetRubyVersion: 2.6
13
- NewCops: enable
14
- SuggestExtensions: false
41
+
42
+ Style/SymbolArray:
43
+ Enabled: true
44
+
15
45
  Style/FrozenStringLiteralComment:
46
+ Enabled: true
47
+ EnforcedStyle: always
48
+
49
+ Naming/FileName:
16
50
  Enabled: false
17
- Gemspec/RequiredRubyVersion:
18
- Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,4 +1,24 @@
1
1
  # Legion::Settings Changelog
2
2
 
3
+ ## [1.3.0] - 2026-03-16
4
+
5
+ ### Added
6
+ - Universal secret resolver: `vault://` and `env://` URI references in any settings value
7
+ - Fallback chain support via arrays (first non-nil wins)
8
+ - `Legion::Settings.resolve_secrets!` method for explicit resolution phase
9
+ - Vault read caching within a single resolution pass
10
+
11
+ ## [1.2.2] - 2026-03-16
12
+
13
+ ### Added
14
+ - `role` key in default settings with `profile` and `extensions` fields for extension profile filtering
15
+
16
+ ## v1.2.1
17
+
18
+ ### Added
19
+ - `dev_mode?` method — returns true when `LEGION_DEV=true` env var or `Settings[:dev]` is set
20
+ - Dev mode soft validation: `validate!` warns instead of raising when dev mode is active
21
+ - Warning output via `Legion::Logging.warn` (falls back to `$stderr` if logging unavailable)
22
+
3
23
  ## v1.2.0
4
- Moving from BitBucket to GitHub inside the Optum org. All git history is reset from this point on
24
+ Moving from BitBucket to GitHub. All git history is reset from this point on
data/CLAUDE.md ADDED
@@ -0,0 +1,154 @@
1
+ # legion-settings: Configuration Management for LegionIO
2
+
3
+ **Repository Level 3 Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
5
+
6
+ ## Purpose
7
+
8
+ Hash-like configuration store for the LegionIO framework. Loads settings from JSON files, directories, and environment variables. Provides a unified `Legion::Settings[:key]` accessor used by all other Legion gems. Includes schema-based validation with type inference, enum constraints, and cross-module checks.
9
+
10
+ **GitHub**: https://github.com/LegionIO/legion-settings
11
+ **License**: Apache-2.0
12
+
13
+ ## Architecture
14
+
15
+ ```
16
+ Legion::Settings (singleton module)
17
+ ├── .load(config_dir:, config_file:, config_dirs:) # Initialize loader
18
+ ├── .[](:key) # Hash-like accessor (auto-loads if needed)
19
+ ├── .set_prop(key, value) # Set a value
20
+ ├── .merge_settings(key, hash) # Merge module defaults + register schema
21
+ ├── .define_schema(key, overrides) # Add enum/required constraints
22
+ ├── .add_cross_validation(&block) # Register cross-module validation
23
+ ├── .validate! # Run all validations, raise on errors
24
+ ├── .schema # Access Schema instance
25
+ ├── .errors # Access collected errors
26
+
27
+ ├── Loader # Core: loads env vars, files, directories, merges settings
28
+ │ ├── .load_env # Load environment variables (LEGION_API_PORT)
29
+ │ ├── .load_file # Load single JSON file
30
+ │ ├── .load_directory # Load all JSON files from directory
31
+ │ ├── .load_module_settings # Merge with module priority
32
+ │ └── .load_module_default # Merge with default priority
33
+
34
+ ├── Schema # Type inference, validation, unknown key detection
35
+ │ ├── .register # Infer types from defaults
36
+ │ ├── .define_override # Add enum/required/type constraints
37
+ │ ├── .validate_module # Validate values against schema
38
+ │ └── .detect_unknown_keys # Find typos via Levenshtein distance
39
+
40
+ ├── ValidationError # Collects all errors, raises once with formatted message
41
+ ├── OS # OS detection helpers
42
+ └── CORE_MODULES # [:transport, :cache, :crypt, :data, :logging, :client]
43
+ ```
44
+
45
+ ### Key Design Patterns
46
+
47
+ - **Auto-Load on Access**: `Legion::Settings[:key]` auto-loads if not initialized
48
+ - **Directory-Based Config**: Loads all `.json` files from config directories (default paths: `/etc/legionio`, `~/legionio`, `./settings`)
49
+ - **Module Merging**: Each Legion module registers its defaults via `merge_settings` during startup
50
+ - **Schema Inference**: Types are inferred from default values — no manual schema definitions needed
51
+ - **Two-Pass Validation**: Per-module on merge (catches type mismatches immediately) + cross-module on `validate!` (catches dependency conflicts)
52
+ - **Self-Service Registration**: LEX modules register schemas alongside defaults via `merge_settings` — no core changes needed
53
+ - **Fail-Fast**: `validate!` collects all errors and raises `ValidationError` once with a formatted message
54
+ - **Lazy Logging**: Falls back to `::Logger.new($stdout)` if `Legion::Logging` isn't loaded yet
55
+
56
+ ## Dependencies
57
+
58
+ | Gem | Purpose |
59
+ |-----|---------|
60
+ | `legion-json` (>= 1.2) | JSON file parsing |
61
+
62
+ ## File Map
63
+
64
+ | Path | Purpose |
65
+ |------|---------|
66
+ | `lib/legion/settings.rb` | Module entry, singleton accessors, schema integration, validation orchestration |
67
+ | `lib/legion/settings/loader.rb` | Config loading from env/files/directories, deep merge, indifferent access |
68
+ | `lib/legion/settings/schema.rb` | Type inference, validation logic, unknown key detection (Levenshtein) |
69
+ | `lib/legion/settings/validation_error.rb` | Error collection and formatted reporting |
70
+ | `lib/legion/settings/os.rb` | OS detection helpers |
71
+ | `lib/legion/settings/resolver.rb` | Secret resolution: `vault://` and `env://` URI references, fallback chains |
72
+ | `lib/legion/settings/version.rb` | VERSION constant |
73
+ | `spec/legion/settings_spec.rb` | Core settings module tests |
74
+ | `spec/legion/settings_module_spec.rb` | Module-level accessor and merge tests |
75
+ | `spec/legion/loader_spec.rb` | Loader: env/file/directory loading tests |
76
+ | `spec/legion/settings/schema_spec.rb` | Schema validation tests |
77
+ | `spec/legion/settings/validation_error_spec.rb` | Error formatting tests |
78
+ | `spec/legion/settings/integration_spec.rb` | End-to-end validation tests |
79
+ | `spec/legion/settings/role_defaults_spec.rb` | Role profile default settings tests |
80
+ | `spec/legion/settings/resolver_spec.rb` | Secret resolver tests (env://, vault://, fallback chains) |
81
+
82
+ ## Secret Resolution
83
+
84
+ Settings values can reference external secret sources using URI syntax. Resolved in-place via `Legion::Settings.resolve_secrets!` (called automatically after `Legion::Crypt.start` in the boot sequence).
85
+
86
+ ### URI Schemes
87
+
88
+ | Scheme | Format | Resolution |
89
+ |--------|--------|------------|
90
+ | `vault://` | `vault://path/to/secret#key` | `Legion::Crypt.read(path)[key]` |
91
+ | `env://` | `env://ENV_VAR_NAME` | `ENV['ENV_VAR_NAME']` |
92
+ | *(plain string)* | `"guest"` | Returned as-is |
93
+
94
+ ### Fallback Chains
95
+
96
+ Array values are tried in order — first non-nil wins:
97
+
98
+ ```json
99
+ {
100
+ "transport": {
101
+ "connection": {
102
+ "password": ["vault://secret/data/rabbitmq#password", "env://RABBITMQ_PASSWORD", "guest"]
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Logging Strategy
109
+
110
+ - Vault not connected + vault refs exist: one summary warning with count
111
+ - Individual vault path failures: debug level
112
+ - Entire chain resolves to nil: one warning per key path
113
+ - Success: info summary with resolved counts
114
+
115
+ ### Implementation
116
+
117
+ `Legion::Settings::Resolver` module with `module_function`. Called via `Legion::Settings.resolve_secrets!` which delegates to `Resolver.resolve_secrets!(@loader.to_hash)`. Vault reads are cached by path within a single resolution pass.
118
+
119
+ ## Role in LegionIO
120
+
121
+ **Core configuration gem** - every other Legion gem reads its configuration from `Legion::Settings`. Settings are organized by module key:
122
+
123
+ ```ruby
124
+ Legion::Settings[:transport] # legion-transport config
125
+ Legion::Settings[:cache] # legion-cache config
126
+ Legion::Settings[:crypt] # legion-crypt config
127
+ Legion::Settings[:data] # legion-data config
128
+ Legion::Settings[:client] # Node identity (name, hostname, ready state)
129
+ Legion::Settings[:role] # Extension profile filtering (profile, extensions)
130
+ ```
131
+
132
+ ### Validation Usage
133
+
134
+ ```ruby
135
+ # Modules register defaults (schema inferred automatically)
136
+ Legion::Settings.merge_settings('transport', { host: 'localhost', port: 5672 })
137
+
138
+ # Optional: add constraints beyond type inference
139
+ Legion::Settings.define_schema('cache', { driver: { enum: %w[dalli redis] } })
140
+
141
+ # Optional: cross-module validation
142
+ Legion::Settings.add_cross_validation do |settings, errors|
143
+ if settings[:crypt][:cluster_secret].nil? && settings[:transport][:connected]
144
+ errors << { module: :crypt, path: 'crypt.cluster_secret', message: 'required when transport is connected' }
145
+ end
146
+ end
147
+
148
+ # Validate all at once (raises ValidationError with all collected errors)
149
+ Legion::Settings.validate!
150
+ ```
151
+
152
+ ---
153
+
154
+ **Maintained By**: Matthew Iverson (@Esity)
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
data/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2021 Optum
189
+ Copyright 2021 Esity
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
data/README.md CHANGED
@@ -1,36 +1,57 @@
1
- Legion::Settings
2
- =====
1
+ # legion-settings
3
2
 
4
- Legion::Settings is a hash like class used to store LegionIO Settings.
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.
5
4
 
6
- Supported Ruby versions and implementations
7
- ------------------------------------------------
5
+ ## Installation
8
6
 
9
- Legion::Json should work identically on:
7
+ ```bash
8
+ gem install legion-settings
9
+ ```
10
10
 
11
- * JRuby 9.2+
12
- * Ruby 2.4+
11
+ Or add to your Gemfile:
13
12
 
13
+ ```ruby
14
+ gem 'legion-settings'
15
+ ```
14
16
 
15
- Installation and Usage
16
- ------------------------
17
+ ## Usage
17
18
 
18
- You can verify your installation using this piece of code:
19
+ ```ruby
20
+ require 'legion/settings'
19
21
 
20
- ```bash
21
- gem install legion-json
22
+ Legion::Settings.load(config_dir: './') # loads all .json files in the directory
23
+
24
+ Legion::Settings[:client][:hostname]
25
+ Legion::Settings[:transport][:connection][:host]
22
26
  ```
23
27
 
24
- ```ruby
25
- require 'legion-settings'
26
- Legion::Settings.load(config_dir: './') # will automatically load json files it has access to inside this dir
28
+ ### Config Paths (checked in order)
27
29
 
28
- Legion::Settings[:client][:hostname]
29
- Legion::Settings[:client][:new_attribute] = 'foobar'
30
+ 1. `/etc/legionio/`
31
+ 2. `~/legionio/`
32
+ 3. `./settings/`
33
+
34
+ Each Legion module registers its own defaults via `merge_settings` during startup.
35
+
36
+ ### Schema Validation
37
+
38
+ Types are inferred automatically from default values. Optional constraints can be added:
30
39
 
40
+ ```ruby
41
+ Legion::Settings.merge_settings('mymodule', { host: 'localhost', port: 8080 })
42
+ Legion::Settings.define_schema('mymodule', { port: { required: true } })
43
+ Legion::Settings.validate! # raises ValidationError if any settings are invalid
44
+
45
+ # In development, warn instead of raising:
46
+ # Set LEGION_DEV=true or Legion::Settings.set_prop(:dev, true)
47
+ # validate! will warn to $stderr (or Legion::Logging) instead of raising
31
48
  ```
32
49
 
33
- Authors
34
- ----------
50
+ ## Requirements
51
+
52
+ - Ruby >= 3.4
53
+ - `legion-json`
54
+
55
+ ## License
35
56
 
36
- * [Matthew Iverson](https://github.com/Esity) - current maintainer
57
+ Apache-2.0
@@ -6,24 +6,24 @@ Gem::Specification.new do |spec|
6
6
  spec.name = 'legion-settings'
7
7
  spec.version = Legion::Settings::VERSION
8
8
  spec.authors = ['Esity']
9
- spec.email = %w[matthewdiverson@gmail.com ruby@optum.com]
9
+ spec.email = ['matthewdiverson@gmail.com']
10
10
 
11
11
  spec.summary = 'Legion::Settings'
12
12
  spec.description = 'A gem written to handle LegionIO Settings in a consistent way across extensions'
13
- spec.homepage = 'https://github.com/Optum/legion-settings'
13
+ spec.homepage = 'https://github.com/LegionIO/legion-settings'
14
14
  spec.license = 'Apache-2.0'
15
15
  spec.require_paths = ['lib']
16
- spec.required_ruby_version = '>= 2.4'
16
+ spec.required_ruby_version = '>= 3.4'
17
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
- spec.test_files = spec.files.select { |p| p =~ %r{^test/.*_test.rb} }
19
- spec.extra_rdoc_files = %w[README.md LICENSE CHANGELOG.md]
18
+ spec.extra_rdoc_files = %w[README.md LICENSE CHANGELOG.md]
20
19
  spec.metadata = {
21
- 'bug_tracker_uri' => 'https://github.com/Optum/legion-settings/issues',
22
- 'changelog_uri' => 'https://github.com/Optum/legion-settings/src/main/CHANGELOG.md',
23
- 'documentation_uri' => 'https://github.com/Optum/legion-settings',
24
- 'homepage_uri' => 'https://github.com/Optum/LegionIO',
25
- 'source_code_uri' => 'https://github.com/Optum/legion-settings',
26
- 'wiki_uri' => 'https://github.com/Optum/legion-settings/wiki'
20
+ 'bug_tracker_uri' => 'https://github.com/LegionIO/legion-settings/issues',
21
+ 'changelog_uri' => 'https://github.com/LegionIO/legion-settings/blob/main/CHANGELOG.md',
22
+ 'documentation_uri' => 'https://github.com/LegionIO/legion-settings',
23
+ 'homepage_uri' => 'https://github.com/LegionIO/LegionIO',
24
+ 'source_code_uri' => 'https://github.com/LegionIO/legion-settings',
25
+ 'wiki_uri' => 'https://github.com/LegionIO/legion-settings/wiki',
26
+ 'rubygems_mfa_required' => 'true'
27
27
  }
28
28
 
29
29
  spec.add_dependency 'legion-json', '>= 1.2'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'socket'
2
4
  require 'legion/settings/os'
3
5
 
@@ -20,37 +22,38 @@ module Legion
20
22
  def client_defaults
21
23
  {
22
24
  hostname: system_hostname,
23
- address: system_address,
24
- name: "#{::Socket.gethostname.tr('.', '_')}.#{::Process.pid}",
25
- ready: false
25
+ address: system_address,
26
+ name: "#{::Socket.gethostname.tr('.', '_')}.#{::Process.pid}",
27
+ ready: false
26
28
  }
27
29
  end
28
30
 
29
31
  def default_settings
30
32
  {
31
- client: client_defaults,
32
- cluster: { public_keys: {} },
33
- crypt: {
34
- cluster_secret: nil,
33
+ client: client_defaults,
34
+ cluster: { public_keys: {} },
35
+ crypt: {
36
+ cluster_secret: nil,
35
37
  cluster_secret_timeout: 5,
36
- vault: { connected: false }
38
+ vault: { connected: false }
37
39
  },
38
- cache: { enabled: true, connected: false, driver: 'dalli' },
39
- extensions: {},
40
- reload: false,
41
- reloading: false,
42
- auto_install_missing_lex: true,
40
+ cache: { enabled: true, connected: false, driver: 'dalli' },
41
+ extensions: {},
42
+ reload: false,
43
+ reloading: false,
44
+ auto_install_missing_lex: true,
43
45
  default_extension_settings: {
44
46
  logger: { level: 'info', trace: false, extended: false }
45
47
  },
46
- logging: {
47
- level: 'info',
48
- location: 'stdout',
49
- trace: true,
48
+ logging: {
49
+ level: 'info',
50
+ location: 'stdout',
51
+ trace: true,
50
52
  backtrace_logging: true
51
53
  },
52
- transport: { connected: false },
53
- data: { connected: false }
54
+ transport: { connected: false },
55
+ data: { connected: false },
56
+ role: { profile: nil, extensions: [] }
54
57
  }
55
58
  end
56
59
 
@@ -66,6 +69,11 @@ module Legion
66
69
  to_hash[key]
67
70
  end
68
71
 
72
+ def []=(key, value)
73
+ @settings[key] = value
74
+ @indifferent_access = false
75
+ end
76
+
69
77
  def hexdigest
70
78
  if @hexdigest && @indifferent_access
71
79
  @hexdigest
@@ -147,8 +155,9 @@ module Legion
147
155
  end
148
156
 
149
157
  def validate
150
- validator = Validator.new
151
- @errors += validator.run(@settings, legion_service_name)
158
+ Legion::Settings.validate!
159
+ rescue Legion::Settings::ValidationError
160
+ # errors are already collected in @errors
152
161
  end
153
162
 
154
163
  private
@@ -191,11 +200,12 @@ module Legion
191
200
  end
192
201
 
193
202
  def read_config_file(file)
194
- contents = IO.read(file)
203
+ contents = File.read(file).dup
195
204
  if contents.respond_to?(:force_encoding)
196
205
  encoding = ::Encoding::ASCII_8BIT
197
206
  contents = contents.force_encoding(encoding)
198
- contents.sub!("\xEF\xBB\xBF".force_encoding(encoding), '')
207
+ bom = (+"\xEF\xBB\xBF").force_encoding(encoding)
208
+ contents.sub!(bom, '')
199
209
  else
200
210
  contents.sub!(/^\357\273\277/, '')
201
211
  end
@@ -216,7 +226,6 @@ module Legion
216
226
  merged
217
227
  end
218
228
 
219
- # rubocop:disable Metrics/AbcSize
220
229
  def deep_diff(hash_one, hash_two)
221
230
  keys = hash_one.keys.concat(hash_two.keys).uniq
222
231
  keys.each_with_object({}) do |key, diff|
@@ -229,15 +238,12 @@ module Legion
229
238
  end
230
239
  end
231
240
  end
232
- # rubocop:enable Metrics/AbcSize
233
241
 
234
242
  def create_loaded_tempfile!
235
243
  dir = ENV['LEGION_LOADED_TEMPFILE_DIR'] || Dir.tmpdir
236
244
  file_name = "legion_#{legion_service_name}_loaded_files"
237
245
  path = File.join(dir, file_name)
238
- File.open(path, 'w') do |file|
239
- file.write(@loaded_files.join(':'))
240
- end
246
+ File.write(path, @loaded_files.join(':'))
241
247
  path
242
248
  end
243
249
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Legion
2
4
  module Settings
3
5
  module OS
@@ -14,15 +16,10 @@ module Legion
14
16
  end
15
17
 
16
18
  def self.linux?
17
- OS.unix? and !OS.mac?
18
- end
19
-
20
- def self.jruby?
21
- RUBY_ENGINE == 'jruby'
19
+ OS.unix? && !OS.mac?
22
20
  end
23
21
 
24
22
  def os
25
- return 'jruby' if jruby?
26
23
  return 'windows' if windows?
27
24
  return 'mac' if mac?
28
25
  return 'unix' if unix?