blueprint_config 1.3.1 → 1.5.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 +4 -4
- data/.idea/blue_config.iml +33 -0
- data/README.md +403 -7
- data/lib/blueprint_config/backend/active_record.rb +8 -0
- data/lib/blueprint_config/backend/credentials.rb +29 -10
- data/lib/blueprint_config/backend/env.rb +12 -0
- data/lib/blueprint_config/backend/memory.rb +50 -0
- data/lib/blueprint_config/backend/yaml.rb +4 -0
- data/lib/blueprint_config/configuration.rb +33 -0
- data/lib/blueprint_config/options_hash.rb +20 -0
- data/lib/blueprint_config/setting.rb +1 -1
- data/lib/blueprint_config/version.rb +1 -1
- data/lib/blueprint_config.rb +13 -0
- data/spec/backend_source_tracking_spec.rb +130 -0
- data/spec/blueprint_config/backend/memory_spec.rb +181 -0
- data/spec/configuration_integration_spec.rb +246 -0
- data/spec/configuration_memory_methods_spec.rb +145 -0
- data/spec/memory_integration_spec.rb +191 -0
- data/spec/options_hash_with_sources_spec.rb +132 -0
- data/spec/spec_helper.rb +3 -0
- metadata +15 -2
@@ -108,5 +108,25 @@ module BlueprintConfig
|
|
108
108
|
def deep_merge?(other)
|
109
109
|
other.is_a?(self.class)
|
110
110
|
end
|
111
|
+
|
112
|
+
def with_sources
|
113
|
+
result = {}
|
114
|
+
each do |key, value|
|
115
|
+
if value.is_a?(BlueprintConfig::OptionsHash)
|
116
|
+
result[key] = value.with_sources
|
117
|
+
elsif value.is_a?(BlueprintConfig::OptionsArray)
|
118
|
+
result[key] = {
|
119
|
+
value: value.to_a,
|
120
|
+
source: @__sources[key]
|
121
|
+
}
|
122
|
+
else
|
123
|
+
result[key] = {
|
124
|
+
value: value,
|
125
|
+
source: @__sources[key]
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
result
|
130
|
+
end
|
111
131
|
end
|
112
132
|
end
|
@@ -6,7 +6,7 @@ module BlueprintConfig
|
|
6
6
|
class Setting < ::ActiveRecord::Base
|
7
7
|
self.inheritance_column = nil
|
8
8
|
|
9
|
-
enum type
|
9
|
+
enum :type, { section: 0, string: 1, integer: 2, boolean: 3, json: 4, selection: 5, set: 6 }
|
10
10
|
|
11
11
|
def parsed_json_value
|
12
12
|
parsed = begin
|
data/lib/blueprint_config.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'blueprint_config/version'
|
4
4
|
require 'blueprint_config/backend/env'
|
5
5
|
require 'blueprint_config/backend/yaml'
|
6
|
+
require 'blueprint_config/backend/memory'
|
6
7
|
require 'blueprint_config/configuration'
|
7
8
|
require 'blueprint_config/backend_collection'
|
8
9
|
|
@@ -59,6 +60,11 @@ BlueprintConfig.before_initialize ||= proc do
|
|
59
60
|
backends.use :credentials, BlueprintConfig::Backend::Credentials.new
|
60
61
|
backends.use :env, BlueprintConfig::Backend::ENV.new(BlueprintConfig.env_backend_options)
|
61
62
|
backends.use :app_local, BlueprintConfig::Backend::YAML.new('config/app.local.yml')
|
63
|
+
|
64
|
+
# Memory backend with highest priority for test environment (added last)
|
65
|
+
if defined?(Rails) && Rails.env.test?
|
66
|
+
backends.use :memory, BlueprintConfig::Backend::Memory.new
|
67
|
+
end
|
62
68
|
end
|
63
69
|
end
|
64
70
|
|
@@ -71,5 +77,12 @@ BlueprintConfig.after_initialize ||= proc do
|
|
71
77
|
else
|
72
78
|
backends.push :db, ar_backend
|
73
79
|
end
|
80
|
+
|
81
|
+
# Ensure memory backend is always last (highest priority) in test environment
|
82
|
+
if defined?(Rails) && Rails.env.test? && backends[:memory]
|
83
|
+
memory_backend = backends[:memory]
|
84
|
+
backends.delete(:memory)
|
85
|
+
backends.push :memory, memory_backend
|
86
|
+
end
|
74
87
|
end
|
75
88
|
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'blueprint_config'
|
5
|
+
require 'blueprint_config/backend/yaml'
|
6
|
+
require 'blueprint_config/backend/credentials'
|
7
|
+
require 'blueprint_config/backend/env'
|
8
|
+
require 'blueprint_config/backend/active_record'
|
9
|
+
require 'blueprint_config/backend/memory'
|
10
|
+
|
11
|
+
describe 'Backend source tracking' do
|
12
|
+
describe BlueprintConfig::Backend::YAML do
|
13
|
+
let(:yaml_backend) { described_class.new('config/test.yml') }
|
14
|
+
|
15
|
+
describe '#source' do
|
16
|
+
it 'includes the file path' do
|
17
|
+
expect(yaml_backend.source).to eq('BlueprintConfig::Backend::YAML(config/test.yml)')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe BlueprintConfig::Backend::Credentials do
|
23
|
+
let(:credentials_backend) { described_class.new }
|
24
|
+
|
25
|
+
describe '#source' do
|
26
|
+
context 'without Rails' do
|
27
|
+
before do
|
28
|
+
hide_const('Rails')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'returns just the class name' do
|
32
|
+
expect(credentials_backend.source).to eq('BlueprintConfig::Backend::Credentials')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with Rails in development' do
|
37
|
+
before do
|
38
|
+
unless defined?(Rails)
|
39
|
+
module Rails
|
40
|
+
def self.env
|
41
|
+
ActiveSupport::StringInquirer.new('development')
|
42
|
+
end
|
43
|
+
def self.root
|
44
|
+
Pathname.new('/app')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
after do
|
51
|
+
Object.send(:remove_const, :Rails) if defined?(Rails)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'shows environment info when no env-specific file exists' do
|
55
|
+
allow(File).to receive(:exist?).with('config/credentials/development.yml.enc').and_return(false)
|
56
|
+
expect(credentials_backend.source).to eq('BlueprintConfig::Backend::Credentials(global)')
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'shows merged info when env-specific file exists' do
|
60
|
+
allow(File).to receive(:exist?).with('config/credentials/development.yml.enc').and_return(true)
|
61
|
+
expect(credentials_backend.source).to eq('BlueprintConfig::Backend::Credentials(global + development)')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe BlueprintConfig::Backend::ENV do
|
68
|
+
describe '#source' do
|
69
|
+
it 'returns class name when no options' do
|
70
|
+
env_backend = described_class.new
|
71
|
+
expect(env_backend.source).to eq('BlueprintConfig::Backend::ENV')
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'includes whitelist_keys when configured' do
|
75
|
+
env_backend = described_class.new(whitelist_keys: ['API_KEY', 'SECRET'])
|
76
|
+
expect(env_backend.source).to eq('BlueprintConfig::Backend::ENV(whitelist_keys: API_KEY, SECRET)')
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'includes whitelist_prefixes when configured' do
|
80
|
+
env_backend = described_class.new(whitelist_prefixes: ['APP_', 'MYAPP_'])
|
81
|
+
expect(env_backend.source).to eq('BlueprintConfig::Backend::ENV(whitelist_prefixes: APP_, MYAPP_)')
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'includes both options when configured' do
|
85
|
+
env_backend = described_class.new(
|
86
|
+
whitelist_keys: ['API_KEY'],
|
87
|
+
whitelist_prefixes: ['APP_']
|
88
|
+
)
|
89
|
+
expect(env_backend.source).to eq('BlueprintConfig::Backend::ENV(whitelist_keys: API_KEY, whitelist_prefixes: APP_)')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe BlueprintConfig::Backend::ActiveRecord do
|
95
|
+
let(:ar_backend) { described_class.new }
|
96
|
+
|
97
|
+
describe '#source' do
|
98
|
+
context 'when table exists' do
|
99
|
+
before do
|
100
|
+
allow(ar_backend).to receive(:table_exist?).and_return(true)
|
101
|
+
ar_backend.instance_variable_set(:@configured, true)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'shows settings table' do
|
105
|
+
expect(ar_backend.source).to eq('BlueprintConfig::Backend::ActiveRecord(settings table)')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'when not configured' do
|
110
|
+
before do
|
111
|
+
ar_backend.instance_variable_set(:@configured, false)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'shows not available' do
|
115
|
+
expect(ar_backend.source).to eq('BlueprintConfig::Backend::ActiveRecord(not available)')
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe BlueprintConfig::Backend::Memory do
|
122
|
+
let(:memory_backend) { described_class.new }
|
123
|
+
|
124
|
+
describe '#source' do
|
125
|
+
it 'returns the class name' do
|
126
|
+
expect(memory_backend.source).to eq('BlueprintConfig::Backend::Memory')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'blueprint_config/backend/memory'
|
4
|
+
|
5
|
+
describe BlueprintConfig::Backend::Memory do
|
6
|
+
let(:backend) { described_class.new }
|
7
|
+
|
8
|
+
describe '#load_keys' do
|
9
|
+
it 'returns empty hash initially' do
|
10
|
+
expect(backend.load_keys).to eq({})
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns stored values as nested structure' do
|
14
|
+
backend.set('foo', 'bar')
|
15
|
+
expect(backend.load_keys).to eq({ foo: 'bar' })
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'returns a copy of the store' do
|
19
|
+
backend.set('foo', 'bar')
|
20
|
+
loaded = backend.load_keys
|
21
|
+
loaded[:baz] = 'qux'
|
22
|
+
expect(backend.load_keys).to eq({ foo: 'bar' })
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#set' do
|
27
|
+
context 'with simple key-value' do
|
28
|
+
it 'stores the value' do
|
29
|
+
backend.set('foo', 'bar')
|
30
|
+
expect(backend.load_keys[:foo]).to eq('bar')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'converts key to string' do
|
34
|
+
backend.set(:foo, 'bar')
|
35
|
+
expect(backend.load_keys[:foo]).to eq('bar')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'overwrites existing value' do
|
39
|
+
backend.set('foo', 'bar')
|
40
|
+
backend.set('foo', 'baz')
|
41
|
+
expect(backend.load_keys[:foo]).to eq('baz')
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'handles numeric values' do
|
45
|
+
backend.set('port', 3000)
|
46
|
+
expect(backend.load_keys[:port]).to eq(3000)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'handles boolean values' do
|
50
|
+
backend.set('enabled', true)
|
51
|
+
expect(backend.load_keys[:enabled]).to be true
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'handles nil values' do
|
55
|
+
backend.set('empty', nil)
|
56
|
+
expect(backend.load_keys[:empty]).to be nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with nested hash value' do
|
61
|
+
it 'creates nested structure from single level hash' do
|
62
|
+
backend.set('smtp', { server: 'localhost', port: 1025 })
|
63
|
+
expect(backend.load_keys).to eq({
|
64
|
+
smtp: {
|
65
|
+
server: 'localhost',
|
66
|
+
port: 1025
|
67
|
+
}
|
68
|
+
})
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'creates nested structure from deeply nested hash' do
|
72
|
+
backend.set('app', {
|
73
|
+
mail: {
|
74
|
+
smtp: {
|
75
|
+
settings: {
|
76
|
+
address: '127.0.0.1',
|
77
|
+
port: 587
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
})
|
82
|
+
expect(backend.load_keys).to eq({
|
83
|
+
app: {
|
84
|
+
mail: {
|
85
|
+
smtp: {
|
86
|
+
settings: {
|
87
|
+
address: '127.0.0.1',
|
88
|
+
port: 587
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
})
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'handles empty parent key' do
|
97
|
+
backend.set('', { foo: 'bar', baz: 'qux' })
|
98
|
+
expect(backend.load_keys).to eq({
|
99
|
+
foo: 'bar',
|
100
|
+
baz: 'qux'
|
101
|
+
})
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'merges with existing values' do
|
105
|
+
backend.set('app.name', 'MyApp')
|
106
|
+
backend.set('app', { version: '1.0' })
|
107
|
+
expect(backend.load_keys).to include(
|
108
|
+
app: {
|
109
|
+
name: 'MyApp',
|
110
|
+
version: '1.0'
|
111
|
+
}
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'handles mixed types in nested hash' do
|
116
|
+
backend.set('config', {
|
117
|
+
string: 'value',
|
118
|
+
number: 42,
|
119
|
+
boolean: true,
|
120
|
+
null: nil,
|
121
|
+
nested: { key: 'value' }
|
122
|
+
})
|
123
|
+
|
124
|
+
expect(backend.load_keys).to eq({
|
125
|
+
config: {
|
126
|
+
string: 'value',
|
127
|
+
number: 42,
|
128
|
+
boolean: true,
|
129
|
+
null: nil,
|
130
|
+
nested: {
|
131
|
+
key: 'value'
|
132
|
+
}
|
133
|
+
}
|
134
|
+
})
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'with dotted keys' do
|
139
|
+
it 'creates nested structure from dotted keys' do
|
140
|
+
backend.set('smtp.server', 'mail.example.com')
|
141
|
+
backend.set('smtp.port', 587)
|
142
|
+
expect(backend.load_keys).to eq({
|
143
|
+
smtp: {
|
144
|
+
server: 'mail.example.com',
|
145
|
+
port: 587
|
146
|
+
}
|
147
|
+
})
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe '#clear' do
|
153
|
+
it 'removes all stored values' do
|
154
|
+
backend.set('foo', 'bar')
|
155
|
+
backend.set('baz', 'qux')
|
156
|
+
backend.clear
|
157
|
+
expect(backend.load_keys).to eq({})
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'allows setting new values after clear' do
|
161
|
+
backend.set('foo', 'bar')
|
162
|
+
backend.clear
|
163
|
+
backend.set('new', 'value')
|
164
|
+
expect(backend.load_keys).to eq({ new: 'value' })
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe '#fresh?' do
|
169
|
+
it 'always returns true' do
|
170
|
+
expect(backend.fresh?).to be true
|
171
|
+
backend.set('foo', 'bar')
|
172
|
+
expect(backend.fresh?).to be true
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe '#source' do
|
177
|
+
it 'returns the class name' do
|
178
|
+
expect(backend.source).to eq('BlueprintConfig::Backend::Memory')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'blueprint_config'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
describe 'Configuration integration with all features' do
|
9
|
+
let(:temp_dir) { Dir.mktmpdir }
|
10
|
+
let(:app_yml_path) { File.join(temp_dir, 'config', 'app.yml') }
|
11
|
+
let(:app_local_yml_path) { File.join(temp_dir, 'config', 'app.local.yml') }
|
12
|
+
|
13
|
+
before do
|
14
|
+
# Setup directory structure
|
15
|
+
FileUtils.mkdir_p(File.join(temp_dir, 'config'))
|
16
|
+
|
17
|
+
# Mock BlueprintConfig root
|
18
|
+
allow(BlueprintConfig).to receive(:root).and_return(temp_dir)
|
19
|
+
allow(BlueprintConfig).to receive(:env).and_return('test')
|
20
|
+
|
21
|
+
# Create YAML files
|
22
|
+
File.write(app_yml_path, <<~YAML)
|
23
|
+
default:
|
24
|
+
app:
|
25
|
+
name: MyApp
|
26
|
+
version: 1.0
|
27
|
+
smtp:
|
28
|
+
server: smtp.example.com
|
29
|
+
port: 587
|
30
|
+
test:
|
31
|
+
app:
|
32
|
+
name: MyApp Test
|
33
|
+
smtp:
|
34
|
+
port: 1025
|
35
|
+
YAML
|
36
|
+
|
37
|
+
File.write(app_local_yml_path, <<~YAML)
|
38
|
+
default:
|
39
|
+
debug:
|
40
|
+
verbose: true
|
41
|
+
smtp:
|
42
|
+
server: localhost
|
43
|
+
YAML
|
44
|
+
|
45
|
+
# Mock Rails for credentials
|
46
|
+
unless defined?(Rails)
|
47
|
+
module Rails
|
48
|
+
def self.env
|
49
|
+
ActiveSupport::StringInquirer.new('test')
|
50
|
+
end
|
51
|
+
def self.application
|
52
|
+
Struct.new(:credentials).new(
|
53
|
+
OpenStruct.new(
|
54
|
+
database: { password: 'secret123' },
|
55
|
+
api: { token: 'api_key_123' }
|
56
|
+
)
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Set some ENV variables
|
63
|
+
ENV['APP_HOST'] = 'test.example.com'
|
64
|
+
ENV['APP_FEATURE_NEW_UI'] = 'true'
|
65
|
+
|
66
|
+
# Initialize configuration
|
67
|
+
BlueprintConfig.instance.instance_variable_set(:@backends, nil)
|
68
|
+
BlueprintConfig.instance.instance_variable_set(:@config, nil)
|
69
|
+
BlueprintConfig.before_initialize.call
|
70
|
+
BlueprintConfig.after_initialize.call
|
71
|
+
BlueprintConfig.define_shortcut
|
72
|
+
end
|
73
|
+
|
74
|
+
after do
|
75
|
+
FileUtils.rm_rf(temp_dir)
|
76
|
+
ENV.delete('APP_HOST')
|
77
|
+
ENV.delete('APP_FEATURE_NEW_UI')
|
78
|
+
|
79
|
+
# Clean up memory backend
|
80
|
+
if defined?(AppConfig) && AppConfig.respond_to?(:clear_memory!)
|
81
|
+
AppConfig.clear_memory!
|
82
|
+
end
|
83
|
+
|
84
|
+
Object.send(:remove_const, :Rails) if defined?(Rails)
|
85
|
+
Object.send(:remove_const, :AppConfig) if defined?(AppConfig)
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'configuration loading and precedence' do
|
89
|
+
it 'loads values from all backends' do
|
90
|
+
# From app.yml default section
|
91
|
+
expect(AppConfig.app.version).to eq(1.0)
|
92
|
+
|
93
|
+
# From app.yml test section (overrides default)
|
94
|
+
expect(AppConfig.app.name).to eq('MyApp Test')
|
95
|
+
|
96
|
+
# From credentials
|
97
|
+
expect(AppConfig.database.password).to eq('secret123')
|
98
|
+
expect(AppConfig.api.token).to eq('api_key_123')
|
99
|
+
|
100
|
+
# From app.local.yml (highest file priority)
|
101
|
+
expect(AppConfig.debug.verbose).to be true
|
102
|
+
expect(AppConfig.smtp.server).to eq('localhost')
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'respects configuration precedence' do
|
106
|
+
# app.yml sets port to 587, test section overrides to 1025
|
107
|
+
expect(AppConfig.smtp.port).to eq(1025)
|
108
|
+
|
109
|
+
# app.local.yml overrides server from app.yml
|
110
|
+
expect(AppConfig.smtp.server).to eq('localhost')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe 'memory backend in test environment' do
|
115
|
+
it 'allows setting values that override all other sources' do
|
116
|
+
# Original value from app.local.yml
|
117
|
+
expect(AppConfig.smtp.server).to eq('localhost')
|
118
|
+
|
119
|
+
# Override with memory backend
|
120
|
+
AppConfig.set('smtp.server', 'memory.example.com')
|
121
|
+
expect(AppConfig.smtp.server).to eq('memory.example.com')
|
122
|
+
|
123
|
+
# Clear memory
|
124
|
+
AppConfig.clear_memory!
|
125
|
+
expect(AppConfig.smtp.server).to eq('localhost')
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'supports setting nested values with hash' do
|
129
|
+
AppConfig.set(
|
130
|
+
features: {
|
131
|
+
new_ui: true,
|
132
|
+
beta: false,
|
133
|
+
limits: {
|
134
|
+
max_users: 100,
|
135
|
+
max_projects: 10
|
136
|
+
}
|
137
|
+
}
|
138
|
+
)
|
139
|
+
|
140
|
+
expect(AppConfig.features.new_ui).to be true
|
141
|
+
expect(AppConfig.features.beta).to be false
|
142
|
+
expect(AppConfig.features.limits.max_users).to eq(100)
|
143
|
+
expect(AppConfig.features.limits.max_projects).to eq(10)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe 'source tracking' do
|
148
|
+
it 'tracks sources for values from different backends' do
|
149
|
+
expect(AppConfig.config.source(:app, :name)).to include('YAML(config/app.yml)')
|
150
|
+
expect(AppConfig.config.source(:database, :password)).to include('Credentials')
|
151
|
+
expect(AppConfig.config.source(:debug, :verbose)).to include('YAML(config/app.local.yml)')
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'shows memory backend source when value is overridden' do
|
155
|
+
AppConfig.set('test.value', 'from memory')
|
156
|
+
expect(AppConfig.config.source(:test, :value)).to include('Memory')
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe 'with_sources method' do
|
161
|
+
it 'returns all values with their sources' do
|
162
|
+
result = AppConfig.config.with_sources
|
163
|
+
|
164
|
+
# Check structure
|
165
|
+
expect(result).to be_a(Hash)
|
166
|
+
expect(result[:app][:name]).to be_a(Hash)
|
167
|
+
expect(result[:app][:name]).to have_key(:value)
|
168
|
+
expect(result[:app][:name]).to have_key(:source)
|
169
|
+
|
170
|
+
# Check values and sources
|
171
|
+
expect(result[:app][:name][:value]).to eq('MyApp Test')
|
172
|
+
expect(result[:app][:name][:source]).to include('YAML')
|
173
|
+
|
174
|
+
expect(result[:database][:password][:value]).to eq('secret123')
|
175
|
+
expect(result[:database][:password][:source]).to include('Credentials')
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'includes memory backend sources' do
|
179
|
+
AppConfig.set('dynamic.setting', 'test value')
|
180
|
+
result = AppConfig.config.with_sources
|
181
|
+
|
182
|
+
expect(result[:dynamic][:setting][:value]).to eq('test value')
|
183
|
+
expect(result[:dynamic][:setting][:source]).to include('Memory')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe 'configuration access methods' do
|
188
|
+
it 'supports member access syntax' do
|
189
|
+
expect(AppConfig.app.name).to eq('MyApp Test')
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'supports hash access syntax' do
|
193
|
+
expect(AppConfig[:app][:name]).to eq('MyApp Test')
|
194
|
+
expect(AppConfig['app']['name']).to eq('MyApp Test')
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'supports dig method' do
|
198
|
+
expect(AppConfig.dig(:app, :name)).to eq('MyApp Test')
|
199
|
+
expect(AppConfig.dig(:non, :existent)).to be_nil
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'supports bang methods for missing keys' do
|
203
|
+
expect { AppConfig.missing! }.to raise_error(KeyError, /missing/)
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'supports question methods for checking existence' do
|
207
|
+
expect(AppConfig.app?).to be true
|
208
|
+
expect(AppConfig.missing?).to be false
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
describe 'convenience methods' do
|
213
|
+
it 'supports AppConfig.to_h' do
|
214
|
+
result = AppConfig.to_h
|
215
|
+
expect(result).to be_a(Hash)
|
216
|
+
expect(result[:app][:name]).to eq('MyApp Test')
|
217
|
+
expect(result[:database][:password]).to eq('secret123')
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'supports AppConfig.with_sources' do
|
221
|
+
result = AppConfig.with_sources
|
222
|
+
expect(result).to be_a(Hash)
|
223
|
+
expect(result[:app][:name]).to have_key(:value)
|
224
|
+
expect(result[:app][:name]).to have_key(:source)
|
225
|
+
expect(result[:app][:name][:value]).to eq('MyApp Test')
|
226
|
+
expect(result[:app][:name][:source]).to include('YAML')
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
describe 'backend listing' do
|
231
|
+
it 'can list all loaded backends' do
|
232
|
+
backends = []
|
233
|
+
AppConfig.backends.each { |b| backends << b.class.name }
|
234
|
+
|
235
|
+
expect(backends).to include('BlueprintConfig::Backend::YAML')
|
236
|
+
expect(backends).to include('BlueprintConfig::Backend::Credentials')
|
237
|
+
expect(backends).to include('BlueprintConfig::Backend::Memory')
|
238
|
+
end
|
239
|
+
|
240
|
+
it 'can access specific backends' do
|
241
|
+
expect(AppConfig.backends[:memory]).to be_a(BlueprintConfig::Backend::Memory)
|
242
|
+
expect(AppConfig.backends[:app]).to be_a(BlueprintConfig::Backend::YAML)
|
243
|
+
expect(AppConfig.backends[:credentials]).to be_a(BlueprintConfig::Backend::Credentials)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|