ultra_settings 2.8.0 → 2.8.1

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: 846f2c54ef9d4019e4f97ad02dcc962813a29fccc6128fe1e18ef9f263c01c2e
4
- data.tar.gz: 7d28f71ae610362fd8e2c4e0ffe2ae885d7b9f270fc0e942f41fd7e8daff18fc
3
+ metadata.gz: d6db7d9c5a607d4c0318190ba8357f0577f9d542d08fbc77e6253e3776372476
4
+ data.tar.gz: 52171f7843724be2e867f2cf3547b934a530249173f8f2c583548f442f9ceb22
5
5
  SHA512:
6
- metadata.gz: 8829e070b1f37474fc6673804d219d1485b2cb36c5d81f9dcd4b88ff6623bd1f99a5a135adedf4cc2a2f1865ac27d74b8f405b01896aabc3da2b5e41226333f3
7
- data.tar.gz: 0e906fddfea5764d653beb41c99d534c30b44974a83b8f7e3de1d76dd1656e367fe160743f4ac65ec422bf0e1c7d0c35b28ff2d5e3ce4252bf70d53d0f367709
6
+ metadata.gz: 660219db715918d464d403064ac96920dde3acf7be57e85b3de921ecc7fe4b3ef641f9ef8b386ea931a603ad7cfab5f4f9ddd9cbc4f151226e4d3359d5a25dce
7
+ data.tar.gz: b4b40f05c524a3bd75fbb237c631e4643f229c50134fadf819acd1dded92855ebb3070eeaa8ee508a36bdbb13ec759a3fd6eb9c140db90d092ebd649a88bb8cf
data/AGENTS.md ADDED
@@ -0,0 +1,191 @@
1
+ # UltraSettings AI Coding Agent Instructions
2
+
3
+ ## Project Overview
4
+ UltraSettings is a Ruby gem for managing application configuration from multiple sources (environment variables, runtime settings, YAML files) with built-in type safety, validation, and a web UI for documentation.
5
+
6
+ ## Core Architecture
7
+
8
+ ### Configuration Classes (Singleton Pattern)
9
+ - All configuration classes extend `UltraSettings::Configuration` and use Ruby's `Singleton` module
10
+ - Each configuration class has one instance accessible via `.instance`
11
+ - Register configurations globally: `UltraSettings.add(:test)` creates `UltraSettings.test` accessor
12
+ - Configuration classes support inheritance; subclasses maintain separate singleton instances
13
+
14
+ ### Field Definition DSL
15
+ Fields are defined using the `field` class method with extensive options:
16
+ ```ruby
17
+ field :timeout, type: :float, default: 1.0,
18
+ default_if: ->(val) { val <= 0 },
19
+ description: "Network timeout in seconds",
20
+ secret: false
21
+ ```
22
+
23
+ ### Multi-Source Value Resolution (Precedence Order)
24
+ 1. **Environment Variables** - Highest priority (e.g., `MY_SERVICE_TIMEOUT`)
25
+ 2. **Runtime Settings** - Dynamic configuration from external stores (e.g., Redis, `super_settings` gem)
26
+ 3. **YAML Files** - File-based defaults with ERB support
27
+ 4. **Default Values** - Fallback specified in field definition
28
+
29
+ ### Type System
30
+ Supported types: `:string` (default), `:symbol`, `:integer`, `:float`, `:boolean`, `:datetime`, `:array`
31
+ - Boolean fields auto-generate `?` predicate methods (e.g., `enabled?`)
32
+ - Array type parses CSV strings from env vars, supports proper arrays from YAML
33
+ - Empty strings coerce to `nil` across all sources
34
+
35
+ ## Key Conventions
36
+
37
+ ### Naming Patterns
38
+ - **Configuration class names**: Must end in `Configuration` (e.g., `MyServiceConfiguration`)
39
+ - **Field names**: Match `/\A[a-z_][a-zA-Z0-9_]*\z/` pattern (snake_case)
40
+ - **Environment variable defaults**: `MY_SERVICE_FOO` for `MyServiceConfiguration#foo`
41
+ - **Runtime setting defaults**: `my_service.foo` (lowercase with dots)
42
+ - **YAML keys**: Match field name by default
43
+
44
+ ### Customization Attributes
45
+ Set on configuration classes to override defaults:
46
+ - `env_var_prefix`, `env_var_delimiter`, `env_var_upcase` - Control environment variable naming
47
+ - `runtime_setting_prefix`, `runtime_setting_delimiter`, `runtime_setting_upcase` - Control runtime setting naming
48
+ - `configuration_file` - Specify explicit YAML file path
49
+ - `fields_secret_by_default` - Default secret status (true by default for security)
50
+ - Disable sources entirely: `environment_variables_disabled`, `runtime_settings_disabled`, `yaml_config_disabled`
51
+
52
+ ### Thread Safety
53
+ - All memoized values protected by `Mutex`
54
+ - Override values are thread-local (keyed by `Thread.current.object_id`)
55
+ - Static fields are cached permanently after first access
56
+
57
+ ## Testing Patterns
58
+
59
+ ### Test Setup (spec/spec_helper.rb)
60
+ ```ruby
61
+ # Configure YAML path and environment
62
+ UltraSettings.yaml_config_path = Pathname.new(__dir__) + "config"
63
+ UltraSettings.yaml_config_env = "test"
64
+
65
+ # Register configurations
66
+ UltraSettings.add(:test)
67
+ ```
68
+
69
+ ### Overriding Configuration Values
70
+ Use `override!` method for temporary value changes in tests:
71
+ ```ruby
72
+ # Via UltraSettings module
73
+ UltraSettings.override!(test: {foo: "bar"}) { ... }
74
+
75
+ # Via configuration class
76
+ TestConfiguration.override!(foo: "bar") { ... }
77
+
78
+ # Via configuration instance
79
+ TestConfiguration.instance.override!(foo: "bar") { ... }
80
+ ```
81
+
82
+ ### RSpec Integration Example
83
+ ```ruby
84
+ RSpec.configure do |config|
85
+ config.around do |example|
86
+ if example.metadata[:ultra_settings]
87
+ UltraSettings.override!(example.metadata[:ultra_settings]) do
88
+ example.run
89
+ end
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ ### Climate Control for Environment Variables
96
+ Uses `climate_control` gem to safely modify environment variables in tests:
97
+ ```ruby
98
+ RSpec.describe "config", env: {TIMEOUT: "5"} do
99
+ # Test with environment variable set
100
+ end
101
+ ```
102
+
103
+ ## Web UI Features
104
+
105
+ ### Mounting the Rack App
106
+ ```ruby
107
+ # config.ru or Rails routes
108
+ mount UltraSettings::RackApp.new(color_scheme: :system), at: "/settings"
109
+ ```
110
+
111
+ ### Key Capabilities
112
+ - Displays all registered configurations with descriptions
113
+ - Shows field metadata: type, description, sources, but **NOT actual values**
114
+ - Respects secret field marking (values hidden)
115
+ - Includes links to edit runtime settings via `UltraSettings.runtime_settings_url`
116
+ - Views in `lib/ultra_settings/*_view.rb` use ERB templates from `app/` directory
117
+
118
+ ## Common Development Workflows
119
+
120
+ ### Running Tests
121
+ ```bash
122
+ bundle exec rake spec # Default task runs all specs
123
+ bundle exec rspec spec/ultra_settings/configuration_spec.rb # Specific file
124
+ ```
125
+
126
+ ### Checking Code Style
127
+ Uses Standard Ruby style guide (testdouble/standard):
128
+ ```bash
129
+ bundle exec standardrb --fix
130
+ ```
131
+
132
+ ### Release Process
133
+ - Can only release from `main` branch (enforced in Rakefile)
134
+ - Version stored in `VERSION` file (no hardcoded version in gemspec)
135
+
136
+ ## Special Features
137
+
138
+ ### Static Fields
139
+ - Marked with `static: true`, values never change after first access
140
+ - Cannot be set from runtime settings (initialization-time only)
141
+ - Use for settings referenced during app boot
142
+
143
+ ### Secret Fields
144
+ - All fields secret by default unless `fields_secret_by_default = false`
145
+ - Masked in `__to_hash__` output
146
+ - Runtime settings disabled on secret fields unless `UltraSettings.runtime_settings_secure = true`
147
+ - Secret status can be dynamic via Proc
148
+
149
+ ### Conditional Defaults
150
+ Use `default_if` with Proc or method name to apply defaults based on loaded value:
151
+ ```ruby
152
+ field :timeout, default: 1.0, default_if: ->(val) { val <= 0 }
153
+ ```
154
+
155
+ ### Introspection Methods
156
+ - `__source__(name)` - Returns which source provided the value (`:env`, `:runtime`, `:yaml`, `:default`)
157
+ - `__value_from_source__(name, source)` - Fetch value from specific source
158
+ - `__to_hash__` - Serialize current configuration as hash (secrets masked)
159
+
160
+ ## Important Implementation Details
161
+
162
+ ### Dynamic Method Generation
163
+ Fields create getter methods via `class_eval` for performance (avoids `method_missing`)
164
+
165
+ ### YAML Configuration
166
+ - Supports environment-specific sections (e.g., `development`, `test`, `production`)
167
+ - Special `shared` section merged with environment-specific config
168
+ - ERB templates evaluated before YAML parsing
169
+ - Files searched in `UltraSettings.yaml_config_path`
170
+
171
+ ### Runtime Settings Integration
172
+ - Must implement `[]` method accepting string argument
173
+ - Optional `array` method for native array support
174
+ - Use `UninitializedRuntimeSettings` during boot to catch premature access
175
+ - `super_settings` gem is recommended companion implementation
176
+
177
+ ## File Organization
178
+
179
+ - `lib/ultra_settings/configuration.rb` - Core configuration class (630 lines)
180
+ - `lib/ultra_settings/field.rb` - Field metadata and resolution logic
181
+ - `lib/ultra_settings/coerce.rb` - Type coercion utilities
182
+ - `lib/ultra_settings/*.rb` - Supporting classes for web UI, YAML, helpers
183
+ - `spec/test_configs/` - Example configuration classes for testing
184
+ - `spec/config/` - YAML configuration files for tests
185
+ - `app/` - ERB templates and assets for web UI
186
+
187
+ ## Key Dependencies
188
+ - Ruby >= 2.5
189
+ - Development: `rspec`, `climate_control`, `nokogiri`, `bundler`
190
+ - Optional: `super_settings` gem for runtime settings store
191
+ - Rails integration via `railtie.rb` when Rails is detected
data/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.8.1
8
+
9
+ ### Added
10
+
11
+ - Improved web UI indication for which data sources on fields have been overridden by data sources with higher precedence.
12
+ - Added rake tasks for auditing configuration data sources.
13
+ - A companion gem `yard-ultra_settings` is now available to provide YARD integration for documenting UltraSettings configuration classes. See the [yard-ultra_settings](https://github.com/bdurand/ultra_settings/yard_plugin) gem for more information.
14
+
7
15
  ## 2.8.0
8
16
 
9
17
  ### Added
data/MIT-LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2023 Brian Durand
1
+ Copyright 2026 Brian Durand
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -520,6 +520,74 @@ class MyServiceConfiguration < UltraSettings::Configuration
520
520
  end
521
521
  ```
522
522
 
523
+ ### Rails Tasks
524
+
525
+ You can audit your configuration data sources with rake tasks to identify potential optimizations with these rake tasks:
526
+
527
+ ```bash
528
+ # Output showing environment variables that are set, but which do not need to be set
529
+ # since they match the default values.
530
+ bundle exec rails ultra_settings:unnecessary_env_vars
531
+
532
+ # Output showing runtime settings that are set, but which do not need to be set
533
+ # since they match the default values.
534
+ bundle exec rails ultra_settings:unnecessary_runtime_settings
535
+
536
+ # Output showing environment variables that are set but which could be loaded from
537
+ # runtime settings instead.
538
+ bundle exec rails ultra_settings:env_vars_can_be_runtime_setting
539
+
540
+ # Output showing environment variables that are set that could be candidates for
541
+ # adding default values to the configuration fields.
542
+ bundle exec rails ultra_settings:env_vars_without_default
543
+ ```
544
+
545
+ ## Generating Documentation
546
+
547
+ ### YARD Plugin
548
+
549
+ For projects using [YARD](https://yardoc.org/) for documentation, there is a companion plugin gem that automatically generates documentation for configuration fields.
550
+
551
+ Add to your Gemfile:
552
+
553
+ ```ruby
554
+ group :development, :test do
555
+ gem 'yard-ultra_settings'
556
+ end
557
+ ```
558
+
559
+ Then add the following to your `.yardopts` file:
560
+
561
+ ```
562
+ --plugin ultra_settings
563
+ ```
564
+
565
+ Once configured, the plugin automatically enhances YARD documentation for any classes that inherit from `UltraSettings::Configuration`. Field definitions will automatically generate method documentation with proper types and return values.
566
+
567
+ See the [yard-ultra_settings](https://github.com/bdurand/ultra_settings/tree/main/yard_plugin) gem for more details.
568
+
569
+ ### Using YARD Macros
570
+
571
+ If you prefer not to install the plugin gem, you can use YARD's built-in macro feature by adding documentation comments before field definitions:
572
+
573
+ ```ruby
574
+ class MyServiceConfiguration < UltraSettings::Configuration
575
+ self.fields_secret_by_default = false
576
+
577
+ # @!method host
578
+ # The hostname for the service
579
+ # @return [String, nil]
580
+ field :host, type: :string
581
+
582
+ # @!method port
583
+ # The port for the service
584
+ # @return [Integer]
585
+ field :port, type: :integer, default: 80
586
+ end
587
+ ```
588
+
589
+ This approach gives you full control over the documentation but requires more manual effort for each field.
590
+
523
591
  ## Installation
524
592
 
525
593
  Add this line to your application's Gemfile:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.8.0
1
+ 2.8.1
@@ -0,0 +1,39 @@
1
+ <% source_value = configuration.__value_from_source__(field.name, source) %>
2
+ <% overridden_by = source_overridden_by(source, current_source) %>
3
+ <div class="ultra-settings-source <%= 'ultra-settings-source-active' if current_source == source %> <%= 'ultra-settings-source-overridden' if overridden_by && !source_value.nil? %>">
4
+ <span class="ultra-settings-source-type">
5
+ <%= source_type %>
6
+ </span>
7
+
8
+ <div class="ultra-settings-source-value-container">
9
+ <% if source_name %>
10
+ <code class="ultra-settings-source-value">
11
+ <%= source_name %>
12
+ </code>
13
+ <% end %>
14
+
15
+ <%= show_defined_value(source_name, source_value, field.secret?) %>
16
+ </div>
17
+
18
+ <% if current_source == source %>
19
+ <span class="ultra-settings-source-indicator">
20
+ Currently active
21
+ </span>
22
+ <% elsif overridden_by && !source_value.nil? %>
23
+ <%= override_indicator(overridden_by) %>
24
+ <% end %>
25
+
26
+ <% if source == :settings %>
27
+ <% edit_url = UltraSettings.runtime_settings_url(name: field.runtime_setting, type: field.type, description: field.description) %>
28
+
29
+ <% if edit_url %>
30
+ <a
31
+ href="<%= html_escape(edit_url) %>"
32
+ class="ultra-settings-edit-link"
33
+ title="Edit <%= html_escape(field.runtime_setting) %>"
34
+ >
35
+ <%= edit_icon %>
36
+ </a>
37
+ <% end %>
38
+ <% end %>
39
+ </div>
data/app/application.css CHANGED
@@ -132,6 +132,10 @@ code.ultra-settings-field-data-value {
132
132
  gap: 0.5rem;
133
133
  }
134
134
 
135
+ .ultra-settings-field-description svg {
136
+ margin-top: 0.125rem;
137
+ }
138
+
135
139
  .ultra-settings-description-text {
136
140
  flex: 1;
137
141
  }
@@ -166,13 +170,19 @@ code.ultra-settings-field-data-value {
166
170
  min-width: 120px;
167
171
  }
168
172
 
173
+ .ultra-settings-source-value-container {
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 0.5rem;
177
+ flex: 1;
178
+ margin: 0 0.75rem;
179
+ }
180
+
169
181
  .ultra-settings-source-value {
170
182
  font-family: monospace;
171
183
  font-size: 0.875rem;
172
184
  color: var(--source-value-color);
173
185
  font-weight: 550;
174
- flex: 1;
175
- margin: 0 0.75rem;
176
186
  word-break: break-all;
177
187
  }
178
188
 
@@ -189,19 +199,95 @@ code.ultra-settings-field-data-value {
189
199
  font-size: 1rem;
190
200
  }
191
201
 
202
+ .ultra-settings-source-overridden {
203
+ background-color: var(--source-overridden-bg-color);
204
+ border-color: var(--source-overridden-border-color);
205
+ }
206
+
207
+ .ultra-settings-source-overridden .ultra-settings-source-type {
208
+ color: var(--source-overridden-type-color);
209
+ }
210
+
211
+ .ultra-settings-source-overridden .ultra-settings-source-value-container .ultra-settings-source-value {
212
+ color: var(--source-overridden-value-color);
213
+ }
214
+
215
+ .ultra-settings-source-override {
216
+ font-size: 0.75rem;
217
+ font-weight: 600;
218
+ color: var(--source-overridden-warning-color);
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 0.25rem;
222
+ }
223
+
224
+ .ultra-settings-source-override svg {
225
+ flex-shrink: 0;
226
+ }
227
+
228
+ .ultra-settings-source-override-text {
229
+ text-transform: uppercase;
230
+ letter-spacing: 0.025em;
231
+ }
232
+
192
233
  .ultra-settings-icon-info {
193
234
  color: var(--info-color);
194
235
  cursor: pointer;
236
+ padding: 0.25rem;
237
+ border-radius: 0.25rem;
238
+ transition: all 0.15s ease;
239
+ display: inline-flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ }
243
+
244
+ .ultra-settings-icon-info:hover {
245
+ background-color: var(--source-bg-color);
246
+ transform: scale(1.15);
247
+ }
248
+
249
+ .ultra-settings-icon-info:active {
250
+ transform: scale(1.05);
195
251
  }
196
252
 
197
253
  .ultra-settings-icon-not-set {
198
254
  color: var(--warning-color);
199
255
  cursor: pointer;
256
+ padding: 0.25rem;
257
+ border-radius: 0.25rem;
258
+ transition: all 0.15s ease;
259
+ display: inline-flex;
260
+ align-items: center;
261
+ justify-content: center;
262
+ }
263
+
264
+ .ultra-settings-icon-not-set:hover {
265
+ background-color: var(--source-bg-color);
266
+ transform: scale(1.15);
267
+ }
268
+
269
+ .ultra-settings-icon-not-set:active {
270
+ transform: scale(1.05);
200
271
  }
201
272
 
202
273
  .ultra-settings-icon-secret {
203
274
  color: var(--disabled-color);
204
275
  cursor: pointer;
276
+ padding: 0.25rem;
277
+ border-radius: 0.25rem;
278
+ transition: all 0.15s ease;
279
+ display: inline-flex;
280
+ align-items: center;
281
+ justify-content: center;
282
+ }
283
+
284
+ .ultra-settings-icon-secret:hover {
285
+ background-color: var(--source-bg-color);
286
+ transform: scale(1.15);
287
+ }
288
+
289
+ .ultra-settings-icon-secret:active {
290
+ transform: scale(1.05);
205
291
  }
206
292
 
207
293
  .ultra-settings-edit-link {
@@ -219,25 +305,54 @@ code.ultra-settings-field-data-value {
219
305
  }
220
306
 
221
307
  .ultra-settings-dialog {
222
- min-width: 20rem;
308
+ min-width: 24rem;
309
+ max-width: 90vw;
223
310
  padding: 0;
224
- border: 1px solid var(--field-border-color);
225
- border-radius: 0.375rem;
311
+ border: none;
312
+ border-radius: 0.5rem;
313
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
314
+ animation: ultra-settings-dialog-show 0.2s ease-out;
315
+ }
316
+
317
+ .ultra-settings-dialog::backdrop {
318
+ background-color: rgba(0, 0, 0, 0.5);
319
+ animation: ultra-settings-backdrop-show 0.2s ease-out;
320
+ }
321
+
322
+ @keyframes ultra-settings-dialog-show {
323
+ from {
324
+ opacity: 0;
325
+ transform: scale(0.95) translateY(-10px);
326
+ }
327
+ to {
328
+ opacity: 1;
329
+ transform: scale(1) translateY(0);
330
+ }
331
+ }
332
+
333
+ @keyframes ultra-settings-backdrop-show {
334
+ from {
335
+ opacity: 0;
336
+ }
337
+ to {
338
+ opacity: 1;
339
+ }
226
340
  }
227
341
 
228
342
  .ultra-settings-dialog-header {
229
- padding: 0.5rem;
343
+ padding: 1rem 1.25rem;
230
344
  background-color: var(--field-header-bg-color);
231
345
  color: var(--field-header-text-color);
232
346
  font-size: 1rem;
233
347
  display: flex;
234
- align-items: top;
348
+ align-items: center;
349
+ border-bottom: 1px solid var(--field-border-color);
235
350
  }
236
351
 
237
352
  .ultra-settings-dialog-title {
238
353
  flex: 1;
239
- text-align: center;
240
- font-weight: 550;
354
+ font-weight: 600;
355
+ font-size: 1.125rem;
241
356
  }
242
357
 
243
358
  .ultra-settings-dialog-close {
@@ -246,12 +361,37 @@ code.ultra-settings-field-data-value {
246
361
  color: var(--field-header-text-color);
247
362
  cursor: pointer;
248
363
  padding: 0.25rem;
364
+ border-radius: 0.25rem;
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ transition: background-color 0.15s ease;
369
+ margin-left: 0.5rem;
370
+ }
371
+
372
+ .ultra-settings-dialog-close:hover {
373
+ background-color: var(--source-bg-color);
374
+ }
375
+
376
+ .ultra-settings-dialog-close:active {
377
+ transform: scale(0.95);
249
378
  }
250
379
 
251
380
  .ultra-settings-dialog-body {
252
- padding: 1rem;
381
+ padding: 1.5rem;
253
382
  background-color: var(--background-color);
254
383
  color: var(--text-color);
384
+ max-height: 60vh;
385
+ overflow-y: auto;
386
+ }
387
+
388
+ .ultra-settings-dialog-body .ultra-settings-field-data-value {
389
+ display: block;
390
+ word-break: break-all;
391
+ white-space: pre-wrap;
392
+ font-size: 0.9375rem;
393
+ line-height: 1.6;
394
+ padding: 0.75rem 1rem;
255
395
  }
256
396
 
257
397
  /* Dropdown Styles */
@@ -28,6 +28,11 @@
28
28
  --source-border-color: #e9ecef;
29
29
  --source-active-bg-color: #e7f3ff;
30
30
  --source-active-border-color: #0d6efd;
31
+ --source-overridden-bg-color: #fff3cd;
32
+ --source-overridden-border-color: #ffc107;
33
+ --source-overridden-type-color: #4d4d4d;
34
+ --source-overridden-value-color: #2b2b2b;
35
+ --source-overridden-warning-color: #996900;
31
36
  --source-type-color: #666666;
32
37
  --source-value-color: #444444;
33
38
  --source-indicator-color: #0d6efd;
@@ -77,6 +82,11 @@
77
82
  --source-border-color: #444444;
78
83
  --source-active-bg-color: #1a3a52;
79
84
  --source-active-border-color: #0d6efd;
85
+ --source-overridden-bg-color: #3d3416;
86
+ --source-overridden-border-color: #997404;
87
+ --source-overridden-type-color: #d4d4d4;
88
+ --source-overridden-value-color: #e8e8e8;
89
+ --source-overridden-warning-color: #ffb84d;
80
90
  --source-type-color: #adb5bd;
81
91
  --source-value-color: #ced4da;
82
92
  --source-indicator-color: #6ea8fe;
@@ -55,83 +55,28 @@
55
55
  <div class="ultra-settings-field-sources">
56
56
  <% sources = configuration.__available_sources__(field.name) %>
57
57
  <% if sources.include?(:env) %>
58
- <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :env %>">
59
- <span class="ultra-settings-source-type">
60
- Environment Variable
61
- </span>
62
-
63
- <code class="ultra-settings-source-value">
64
- <%= field.env_var %>
65
- <%= show_defined_value(field.env_var, configuration.__value_from_source__(field.name, :env), field.secret?) %>
66
- </code>
67
-
68
- <% if source == :env %>
69
- <span class="ultra-settings-source-indicator">
70
- Currently active
71
- </span>
72
- <% end %>
73
- </div>
58
+ <%= render_partial "data_source", configuration: configuration, field: field, source: :env, current_source: source, source_type: "Environment Variable", source_name: field.env_var %>
74
59
  <% end %>
75
60
 
76
61
  <% if sources.include?(:settings) %>
77
- <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :settings %>">
78
- <span class="ultra-settings-source-type">Runtime Setting</span>
79
-
80
- <code class="ultra-settings-source-value">
81
- <%= field.runtime_setting %>
82
- <%= show_defined_value(field.runtime_setting, configuration.__value_from_source__(field.name, :settings), field.secret?) %>
83
- </code>
84
-
85
- <% if source == :settings %>
86
- <span class="ultra-settings-source-indicator">
87
- Currently active
88
- </span>
89
- <% end %>
90
-
91
- <% edit_url = UltraSettings.runtime_settings_url(name: field.runtime_setting, type: field.type, description: field.description) %>
92
-
93
- <% if edit_url %>
94
- <a
95
- href="<%= html_escape(edit_url) %>"
96
- class="ultra-settings-edit-link"
97
- title="Edit <%= html_escape(field.runtime_setting) %>"
98
- >
99
- <%= edit_icon %>
100
- </a>
101
- <% end %>
102
- </div>
62
+ <%= render_partial "data_source", configuration: configuration, field: field, source: :settings, current_source: source, source_type: "Runtime Setting", source_name: field.runtime_setting %>
103
63
  <% end %>
104
64
 
105
65
  <% if sources.include?(:yaml) %>
106
- <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :yaml %>">
107
- <span class="ultra-settings-source-type">Configuration File</span>
108
-
109
- <code class="ultra-settings-source-value">
110
- <%= field.yaml_key %>
111
- <%= show_defined_value(field.yaml_key, configuration.__value_from_source__(field.name, :yaml), field.secret?) %>
112
- </code>
113
-
114
- <% if source == :yaml %>
115
- <span class="ultra-settings-source-indicator">
116
- Currently active
117
- </span>
118
- <% end %>
119
- </div>
66
+ <%= render_partial "data_source", configuration: configuration, field: field, source: :yaml, current_source: source, source_type: "Configuration File", source_name: field.yaml_key %>
120
67
  <% end %>
121
68
 
122
- <% if sources.include?(:default) || source == :default %>
123
- <div class="ultra-settings-source <%= 'ultra-settings-source-active' if source == :default %>">
124
- <span class="ultra-settings-source-type">Default Value</span>
69
+ <% if sources.include?(:default) %>
70
+ <%= render_partial "data_source", configuration: configuration, field: field, source: :default, current_source: source, source_type: "Default Value", source_name: nil %>
71
+ <% end %>
125
72
 
126
- <code class="ultra-settings-source-value">
127
- <%= show_defined_value("Default Value", field.default, field.secret?) %>
128
- </code>
73
+ <% if source == :default && configuration[field.name].nil? %>
74
+ <div class="ultra-settings-source ultra-settings-source-active">
75
+ <span class="ultra-settings-source-type">Not Set</span>
129
76
 
130
- <% if source == :default %>
131
- <span class="ultra-settings-source-indicator">
132
- Currently active
133
- </span>
134
- <% end %>
77
+ <span class="ultra-settings-source-indicator">
78
+ Currently active
79
+ </span>
135
80
  </div>
136
81
  <% end %>
137
82
  </div>
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UltraSettings
4
+ class AuditDataSources
5
+ class << self
6
+ # Find environment variables that are set but have the same value as their default.
7
+ # These environment variables could potentially be removed since they're not changing behavior.
8
+ #
9
+ # @return [Array<Array<(String, Object)>>] An array of tuples containing environment variable names and their default values
10
+ def unnecessary_env_vars
11
+ env_vars_at_default = []
12
+ each_configuration do |config|
13
+ each_field_using_source(config, :env) do |field|
14
+ value = config[field.name]
15
+ default_value = default_config_value(config, field)
16
+ env_vars_at_default << [field.env_var, default_value] if default_value == value
17
+ end
18
+ end
19
+ env_vars_at_default
20
+ end
21
+
22
+ # Find runtime settings that are set but have the same value as their default.
23
+ # These runtime settings could potentially be removed since they're not changing behavior.
24
+ #
25
+ # @return [Array<Array<(String, Object)>>] An array of tuples containing runtime setting names and their default values
26
+ def unnecessary_runtime_settings
27
+ unnecessary_runtime_settings = []
28
+ each_configuration do |config|
29
+ each_field_using_source(config, :settings) do |field|
30
+ value = config[field.name]
31
+ default_value = default_config_value(config, field)
32
+ unnecessary_runtime_settings << [field.runtime_setting, default_value] if default_value == value
33
+ end
34
+ end
35
+ unnecessary_runtime_settings
36
+ end
37
+
38
+ # Find environment variables that could be moved to runtime settings.
39
+ # These are non-default environment variable values where a runtime setting is also available.
40
+ #
41
+ # @return [Array<Array<(String, String, Object)>>] An array of tuples containing environment variable name, runtime setting name, and current value
42
+ def env_vars_can_be_runtime_setting
43
+ env_vars_can_be_runtime = []
44
+ each_configuration do |config|
45
+ each_field_using_source(config, :env) do |field|
46
+ value = config[field.name]
47
+ default_value = default_config_value(config, field)
48
+ next unless field.runtime_setting && value != default_value
49
+
50
+ env_vars_can_be_runtime << [field.env_var, field.runtime_setting, value]
51
+ end
52
+ end
53
+ env_vars_can_be_runtime
54
+ end
55
+
56
+ # Find environment variables being used that don't have default values defined.
57
+ # These configurations require an environment variable to be set.
58
+ #
59
+ # @return [Array<Array<(String, Symbol, String, Object)>>] An array of tuples containing class name, field name, environment variable name, and current value
60
+ def env_vars_without_default
61
+ no_default_env_var_fields = []
62
+ each_configuration do |config|
63
+ each_field_using_source(config, :env) do |field|
64
+ value = default_config_value(config, field)
65
+ if value.nil?
66
+ no_default_env_var_fields << [config.class.name, field.name, field.env_var, config[field.name]]
67
+ end
68
+ end
69
+ end
70
+ no_default_env_var_fields
71
+ end
72
+
73
+ private
74
+
75
+ def each_configuration(&_block)
76
+ UltraSettings::Configuration.descendant_configurations.each do |config_class|
77
+ config = config_class.instance
78
+ yield config
79
+ end
80
+ end
81
+
82
+ def each_field_using_source(config, source, &_block)
83
+ config.class.fields.each do |field|
84
+ next if field.secret?
85
+ next unless config.__source__(field.name) == source
86
+
87
+ yield field
88
+ end
89
+ end
90
+
91
+ def default_config_value(config, field)
92
+ yaml_value = config.__value_from_source__(field.name, :yaml)
93
+ default_value = config.__value_from_source__(field.name, :default)
94
+ yaml_value || default_value
95
+ end
96
+ end
97
+ end
98
+ end
@@ -10,6 +10,7 @@ module UltraSettings
10
10
  @env_var_prefix = nil
11
11
  @runtime_setting_prefix = nil
12
12
  @description = nil
13
+ @descendants = []
13
14
 
14
15
  class << self
15
16
  # Set a description for the configuration. This is optional. It will be displayed
@@ -84,7 +85,8 @@ module UltraSettings
84
85
  secret: secret
85
86
  )
86
87
 
87
- class_eval <<~RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Security/Eval
88
+ caller_location = caller_locations(1, 1).first
89
+ class_eval <<~RUBY, caller_location.path, caller_location.lineno # rubocop:disable Security/Eval, Style/EvalWithLocation
88
90
  def #{name}
89
91
  __get_value__(#{name.inspect})
90
92
  end
@@ -110,7 +112,7 @@ module UltraSettings
110
112
  name = name.to_s
111
113
  return true if defined_fields.include?(name)
112
114
 
113
- if superclass <= Configuration
115
+ if superclass < Configuration
114
116
  superclass.include_field?(name)
115
117
  else
116
118
  false
@@ -369,12 +371,30 @@ module UltraSettings
369
371
  YamlConfig.new(configuration_file, yaml_config_env).to_h
370
372
  end
371
373
 
374
+ # Get all descendant configuration classes (subclasses and their subclasses, recursively).
375
+ #
376
+ # @return [Array<Class>] All classes that inherit from this class.
377
+ def descendant_configurations
378
+ @descendants ||= []
379
+ @descendants.flat_map { |subclass| [subclass] + subclass.descendant_configurations }
380
+ end
381
+
372
382
  private
373
383
 
384
+ # Hook called when this class is inherited. Tracks all descendant classes.
385
+ #
386
+ # @param subclass [Class] The subclass that is inheriting from this class.
387
+ # @return [void]
388
+ def inherited(subclass)
389
+ super
390
+ @descendants ||= []
391
+ @descendants << subclass
392
+ end
393
+
374
394
  def defined_fields
375
395
  unless defined?(@defined_fields)
376
396
  fields = {}
377
- if superclass <= Configuration
397
+ if superclass < Configuration
378
398
  fields = superclass.send(:defined_fields).dup
379
399
  end
380
400
  @defined_fields = fields
@@ -153,5 +153,44 @@ module UltraSettings
153
153
  this.closest('.ultra-settings-configuration').querySelector('.ultra-settings-dialog-close').blur();
154
154
  JAVASCRIPT
155
155
  end
156
+
157
+ def source_priority
158
+ [:env, :settings, :yaml, :default]
159
+ end
160
+
161
+ def source_overridden_by(current_source, active_source)
162
+ return nil if current_source == active_source
163
+
164
+ current_index = source_priority.index(current_source)
165
+ active_index = source_priority.index(active_source)
166
+
167
+ return nil if current_index.nil? || active_index.nil?
168
+ return nil if current_index < active_index
169
+
170
+ active_source
171
+ end
172
+
173
+ def override_indicator(overridden_by_source)
174
+ source_names = {
175
+ env: "environment variable",
176
+ settings: "runtime setting",
177
+ yaml: "configuration file",
178
+ default: "default value"
179
+ }
180
+ <<~HTML
181
+ <span class="ultra-settings-source-override" title="Overridden by #{source_names[overridden_by_source]}">
182
+ #{warning_icon(14)}
183
+ <span class="ultra-settings-source-override-text">Overridden by #{source_names[overridden_by_source]}</span>
184
+ </span>
185
+ HTML
186
+ end
187
+
188
+ def warning_icon(size = 16)
189
+ <<~HTML
190
+ <svg width="#{size}" height="#{size}" fill="currentColor" viewBox="0 0 16 16">
191
+ <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
192
+ </svg>
193
+ HTML
194
+ end
156
195
  end
157
196
  end
@@ -23,6 +23,8 @@ module UltraSettings
23
23
 
24
24
  app_config_dir = Rails.root.join(directory)
25
25
  app_config_dir.glob("**/*_configuration.rb").each do |file_path|
26
+ next unless file_path.file? && file_path.readable?
27
+
26
28
  relative_path = file_path.relative_path_from(app_config_dir).to_s
27
29
  class_name = relative_path.chomp(".rb").classify
28
30
  unless UltraSettings.added?(class_name)
@@ -32,5 +34,11 @@ module UltraSettings
32
34
  end
33
35
  end
34
36
  end
37
+
38
+ rake_tasks do
39
+ Dir.glob(File.expand_path("tasks/*.rake", __dir__)).each do |rake_file|
40
+ load rake_file
41
+ end
42
+ end
35
43
  end
36
44
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ultra_settings do
4
+ desc <<~DOC
5
+ Generates a report of environment variables used in configurations that are set to their default values.
6
+ This report can help identify environment variables that are superfluous and can be removed. It skips any
7
+ environment variables that are used for secrets.
8
+ DOC
9
+ task unnecessary_env_vars: :environment do
10
+ require_relative "utils"
11
+ require_relative "../audit_data_sources"
12
+
13
+ UltraSettings::Tasks::Utils.eager_load!
14
+ env_vars_at_default = UltraSettings::AuditDataSources.unnecessary_env_vars
15
+
16
+ output = env_vars_at_default.collect do |env_var, value|
17
+ "Environment variable #{env_var} is set to its default value: #{value.inspect}"
18
+ end
19
+ puts output
20
+ end
21
+
22
+ desc <<~DOC
23
+ Generates a report of runtime settings used in configurations that are set to their default values.
24
+ This report can help identify runtime settings that are superfluous and can be removed. It skips any
25
+ runtime settings that are used for secrets.
26
+ DOC
27
+ task unnecessary_runtime_settings: :environment do
28
+ require_relative "utils"
29
+ require_relative "../audit_data_sources"
30
+
31
+ UltraSettings::Tasks::Utils.eager_load!
32
+ unnecessary_runtime_settings = UltraSettings::AuditDataSources.unnecessary_runtime_settings
33
+
34
+ output = unnecessary_runtime_settings.collect do |runtime_setting, value|
35
+ "Runtime setting #{runtime_setting} is set to its default value: #{value.inspect}"
36
+ end
37
+ puts output
38
+ end
39
+
40
+ desc <<~DOC
41
+ Generates a report of environment variables used in configurations that can be converted to runtime settings.
42
+ This report can help identify environment variables that can be removed if the corresponding runtime settings
43
+ are set. It skips any environment variables that are used for secrets.
44
+ DOC
45
+ task env_vars_can_be_runtime_setting: :environment do
46
+ require_relative "utils"
47
+ require_relative "../audit_data_sources"
48
+
49
+ UltraSettings::Tasks::Utils.eager_load!
50
+ env_vars_can_be_runtime = UltraSettings::AuditDataSources.env_vars_can_be_runtime_setting
51
+
52
+ output = env_vars_can_be_runtime.collect do |env_var, runtime_setting, value|
53
+ "Environment variable #{env_var} can be converted to runtime setting #{runtime_setting} with value: #{value.inspect}"
54
+ end
55
+ puts output
56
+ end
57
+
58
+ desc <<~DOC
59
+ Generates a report of environment variables used in configurations that do not have a default value.
60
+ This report can help identify settings that could be set in YAML or with a default value rather than via
61
+ environment variables. If these changes are made, then the environment variables could be removed.
62
+ It skips any environment variables that are used for secrets.
63
+ DOC
64
+ task env_vars_without_default: :environment do
65
+ require_relative "utils"
66
+ require_relative "../audit_data_sources"
67
+
68
+ UltraSettings::Tasks::Utils.eager_load!
69
+ env_vars_without_default = UltraSettings::AuditDataSources.env_vars_without_default
70
+
71
+ output = env_vars_without_default.collect do |config, field, env_var, value|
72
+ "Environment variable #{env_var} used by #{config}##{field} does not have a default value (current value: #{value.inspect})"
73
+ end
74
+ puts output
75
+ end
76
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UltraSettings
4
+ module Tasks
5
+ module Utils
6
+ class << self
7
+ # Helper for eager loading a Rails application.
8
+ def eager_load!
9
+ return unless defined?(Rails.application.config.eager_load)
10
+ return if Rails.application.config.eager_load
11
+
12
+ if defined?(Rails.application.eager_load!)
13
+ Rails.application.eager_load!
14
+ elsif defined?(Rails.autoloaders.zeitwerk_enabled?) && Rails.autoloaders.zeitwerk_enabled?
15
+ Rails.autoloaders.each(&:eager_load)
16
+ else
17
+ raise "Failed to eager load application."
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -28,6 +28,8 @@ Gem::Specification.new do |spec|
28
28
  bin/
29
29
  gemfiles/
30
30
  spec/
31
+ test_app/
32
+ yard_plugin/
31
33
  ]
32
34
  spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
33
35
  `git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ultra_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.0
4
+ version: 2.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
@@ -29,6 +29,7 @@ executables: []
29
29
  extensions: []
30
30
  extra_rdoc_files: []
31
31
  files:
32
+ - AGENTS.md
32
33
  - ARCHITECTURE.md
33
34
  - CHANGELOG.md
34
35
  - MIT-LICENSE.txt
@@ -36,6 +37,7 @@ files:
36
37
  - VERSION
37
38
  - app/_config_description.html.erb
38
39
  - app/_config_list.html.erb
40
+ - app/_data_source.html.erb
39
41
  - app/_select_menu.html.erb
40
42
  - app/application.css
41
43
  - app/application.js
@@ -47,6 +49,7 @@ files:
47
49
  - app/layout_vars.css.erb
48
50
  - lib/ultra_settings.rb
49
51
  - lib/ultra_settings/application_view.rb
52
+ - lib/ultra_settings/audit_data_sources.rb
50
53
  - lib/ultra_settings/coerce.rb
51
54
  - lib/ultra_settings/config_helper.rb
52
55
  - lib/ultra_settings/configuration.rb
@@ -55,6 +58,8 @@ files:
55
58
  - lib/ultra_settings/rack_app.rb
56
59
  - lib/ultra_settings/railtie.rb
57
60
  - lib/ultra_settings/render_helper.rb
61
+ - lib/ultra_settings/tasks/audit_data_sources.rake
62
+ - lib/ultra_settings/tasks/utils.rb
58
63
  - lib/ultra_settings/uninitialized_runtime_settings.rb
59
64
  - lib/ultra_settings/version.rb
60
65
  - lib/ultra_settings/view_helper.rb
@@ -82,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
87
  - !ruby/object:Gem::Version
83
88
  version: '0'
84
89
  requirements: []
85
- rubygems_version: 3.6.9
90
+ rubygems_version: 4.0.3
86
91
  specification_version: 4
87
92
  summary: UltraSettings is a Ruby gem that provides a flexible and documented approach
88
93
  to managing application configurations from multiple sources, including environment