const_conf 0.0.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 +7 -0
- data/Gemfile +5 -0
- data/LICENSE +19 -0
- data/README.md +789 -0
- data/Rakefile +35 -0
- data/const_conf.gemspec +35 -0
- data/lib/const_conf/dir_plugin.rb +175 -0
- data/lib/const_conf/env_dir_extension.rb +83 -0
- data/lib/const_conf/errors.rb +93 -0
- data/lib/const_conf/file_plugin.rb +33 -0
- data/lib/const_conf/json_plugin.rb +12 -0
- data/lib/const_conf/railtie.rb +13 -0
- data/lib/const_conf/setting.rb +382 -0
- data/lib/const_conf/setting_accessor.rb +103 -0
- data/lib/const_conf/tree.rb +265 -0
- data/lib/const_conf/version.rb +8 -0
- data/lib/const_conf/yaml_plugin.rb +30 -0
- data/lib/const_conf.rb +401 -0
- data/spec/assets/.env/API_KEY +1 -0
- data/spec/assets/config.json +3 -0
- data/spec/assets/config.yml +1 -0
- data/spec/assets/config_env.yml +7 -0
- data/spec/const_conf/dir_plugin_spec.rb +249 -0
- data/spec/const_conf/env_dir_extension_spec.rb +59 -0
- data/spec/const_conf/file_plugin_spec.rb +173 -0
- data/spec/const_conf/json_plugin_spec.rb +64 -0
- data/spec/const_conf/setting_accessor_spec.rb +114 -0
- data/spec/const_conf/setting_spec.rb +628 -0
- data/spec/const_conf/tree_spec.rb +185 -0
- data/spec/const_conf/yaml_plugin_spec.rb +84 -0
- data/spec/const_conf_spec.rb +216 -0
- data/spec/spec_helper.rb +140 -0
- metadata +240 -0
@@ -0,0 +1 @@
|
|
1
|
+
sk-1234567890abcdef1234567890abcdef
|
@@ -0,0 +1 @@
|
|
1
|
+
hello: world
|
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConstConf::DirPlugin, protect_env: true do
|
4
|
+
let(:temp_dir) { Dir.mktmpdir }
|
5
|
+
let(:config_dir) { File.join(temp_dir, 'myapp') }
|
6
|
+
let(:file_in_dir) { File.join(config_dir, 'config.yaml') }
|
7
|
+
let(:nonexistent_file) { File.join(config_dir, 'nonexistent.txt') }
|
8
|
+
|
9
|
+
before do
|
10
|
+
# Create a test config directory and file
|
11
|
+
FileUtils.mkdir_p(config_dir)
|
12
|
+
File.write(file_in_dir, "key: value\nother_key: other_value\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
FileUtils.rm_rf(temp_dir)
|
17
|
+
end
|
18
|
+
|
19
|
+
describe ConstConf::DirPlugin::ConfigDir do
|
20
|
+
let(:config_dir_instance) do
|
21
|
+
described_class.new('myapp', root_path: temp_dir, env_var: nil, env_var_name: nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#initialize' do
|
25
|
+
it 'initializes with name and root path' do
|
26
|
+
expect(config_dir_instance).to be_a(described_class)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'raises ArgumentError when both env_var and env_var_name are provided' do
|
30
|
+
expect {
|
31
|
+
described_class.new('myapp', root_path: temp_dir, env_var: 'value', env_var_name: 'VAR_NAME')
|
32
|
+
}.to raise_error(ArgumentError)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'uses env_var_name to lookup environment variable value' do
|
36
|
+
ENV['CUSTOM_HOME'] = temp_dir
|
37
|
+
config_dir_with_env = described_class.new('myapp', env_var: nil, env_var_name: 'CUSTOM_HOME')
|
38
|
+
expect(config_dir_with_env.to_s).to include(temp_dir)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#to_s' do
|
43
|
+
it 'returns the directory path as string' do
|
44
|
+
result = config_dir_instance.to_s
|
45
|
+
expect(result).to include('myapp')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#join' do
|
50
|
+
it 'joins path with directory path' do
|
51
|
+
joined_path = config_dir_instance.join('config.yaml')
|
52
|
+
expect(joined_path.to_s).to eq file_in_dir
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#read' do
|
57
|
+
context 'when file exists' do
|
58
|
+
it 'reads file content as string' do
|
59
|
+
result = config_dir_instance.read('config.yaml')
|
60
|
+
expect(result).to eq "key: value\nother_key: other_value\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'handles file with block' do
|
64
|
+
result = config_dir_instance.read('config.yaml') do |file|
|
65
|
+
file.read
|
66
|
+
end
|
67
|
+
expect(result).to eq "key: value\nother_key: other_value\n"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'when file does not exist' do
|
72
|
+
it 'returns nil when file does not exist and no default or required' do
|
73
|
+
result = config_dir_instance.read('nonexistent.txt')
|
74
|
+
expect(result).to be_nil
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'returns default value when file does not exist but default is provided' do
|
78
|
+
result = config_dir_instance.read('nonexistent.txt', default: 'default_content')
|
79
|
+
expect(result).to eq 'default_content'
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'raises RequiredValueNotConfigured when file required and does not exist' do
|
83
|
+
expect {
|
84
|
+
config_dir_instance.read('nonexistent.txt', required: true)
|
85
|
+
}.to raise_error(ConstConf::RequiredValueNotConfigured)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'yields StringIO with default when block given and file does not exist' do
|
89
|
+
result = config_dir_instance.read('nonexistent.txt', default: 'default_content') do |io|
|
90
|
+
io.read
|
91
|
+
end
|
92
|
+
expect(result).to eq 'default_content'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '#derive_directory_path' do
|
98
|
+
it 'combines root path and name' do
|
99
|
+
path = config_dir_instance.send(:derive_directory_path, 'myapp', temp_dir)
|
100
|
+
expect(path.to_s).to include('myapp')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#default_root_path' do
|
105
|
+
it 'returns default XDG config path from HOME' do
|
106
|
+
ENV['HOME'] = temp_dir
|
107
|
+
path = config_dir_instance.send(:default_root_path)
|
108
|
+
expect(path.to_s).to include('.config')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '#dir' do
|
114
|
+
let(:instance) { double('Config').extend(described_class) }
|
115
|
+
|
116
|
+
context 'with existing file in directory' do
|
117
|
+
it 'reads file content from directory' do
|
118
|
+
result = instance.dir('myapp', 'config.yaml', env_var: temp_dir)
|
119
|
+
expect(result).to eq "key: value\nother_key: other_value\n"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'with non-existent file' do
|
124
|
+
it 'returns default when provided' do
|
125
|
+
result = instance.dir('myapp', 'nonexistent.txt', env_var: temp_dir, default: 'default_value')
|
126
|
+
expect(result).to eq 'default_value'
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'returns nil when no default and file does not exist' do
|
130
|
+
result = instance.dir('myapp', 'nonexistent.txt', env_var: temp_dir)
|
131
|
+
expect(result).to be_nil
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'raises error when required is true and file does not exist' do
|
135
|
+
expect {
|
136
|
+
instance.dir('myapp', 'nonexistent.txt', env_var: temp_dir, required: true)
|
137
|
+
}.to raise_error(ConstConf::RequiredValueNotConfigured)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe 'integration with ConstConf settings' do
|
143
|
+
let(:module_with_dir_plugin) do
|
144
|
+
Module.new do
|
145
|
+
include ConstConf
|
146
|
+
plugin ConstConf::DirPlugin
|
147
|
+
|
148
|
+
description 'Module with DirPlugin'
|
149
|
+
|
150
|
+
# Test basic dir usage
|
151
|
+
CONFIG_FILE = set do
|
152
|
+
description 'Configuration from directory file'
|
153
|
+
default dir('myapp', 'config.yaml', env_var: temp_dir)
|
154
|
+
decode { |s| YAML.load(s) }
|
155
|
+
end
|
156
|
+
|
157
|
+
# Test required directory file
|
158
|
+
REQUIRED_DIR_FILE = set do
|
159
|
+
description 'Required file in directory'
|
160
|
+
default dir('myapp', 'nonexistent.txt', env_var: temp_dir, required: false)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
before do
|
166
|
+
# Create the module with dynamic eval to simulate real usage
|
167
|
+
eval <<~RUBY
|
168
|
+
if Object.const_defined?(:ConstConfTestModuleWithDir)
|
169
|
+
Object.send(:remove_const, :ConstConfTestModuleWithDir)
|
170
|
+
end
|
171
|
+
module ConstConfTestModuleWithDir
|
172
|
+
include ConstConf
|
173
|
+
plugin ConstConf::DirPlugin
|
174
|
+
|
175
|
+
description 'Module with DirPlugin'
|
176
|
+
|
177
|
+
CONFIG_FILE = set do
|
178
|
+
description 'Configuration from directory file'
|
179
|
+
default dir('myapp', 'config.yaml', env_var: '#{temp_dir}')
|
180
|
+
decode { |s| YAML.load(s) }
|
181
|
+
end
|
182
|
+
end
|
183
|
+
RUBY
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'can read files from directories' do
|
187
|
+
# This will test the actual integration
|
188
|
+
expect(ConstConfTestModuleWithDir::CONFIG_FILE).to be_a(Hash)
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'XDG-compliant directory structure' do
|
192
|
+
before do
|
193
|
+
eval <<~RUBY
|
194
|
+
if Object.const_defined?(:ConstConfXDGModule)
|
195
|
+
Object.send(:remove_const, :ConstConfXDGModule)
|
196
|
+
end
|
197
|
+
module ConstConfXDGModule
|
198
|
+
include ConstConf
|
199
|
+
plugin ConstConf::DirPlugin
|
200
|
+
|
201
|
+
description 'XDG Compliant Module'
|
202
|
+
|
203
|
+
XDG_CONFIG_HOME = set do
|
204
|
+
description 'XDG Config HOME'
|
205
|
+
prefix ''
|
206
|
+
end
|
207
|
+
|
208
|
+
API_KEY = set do
|
209
|
+
description 'API Key from XDG config'
|
210
|
+
default dir('myapp', 'api_key.txt', env_var: ConstConfXDGModule::XDG_CONFIG_HOME)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
RUBY
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'handles XDG-compliant directory structure' do
|
217
|
+
# This tests the XDG pattern from the example
|
218
|
+
expect { ConstConfXDGModule::API_KEY }.not_to raise_error
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe 'error handling' do
|
224
|
+
let(:instance) { double('Config').extend(described_class) }
|
225
|
+
|
226
|
+
it 'handles directory operations gracefully' do
|
227
|
+
# Test that we can handle edge cases in directory operations
|
228
|
+
expect {
|
229
|
+
instance.dir('nonexistent', 'file.txt', env_var: '/nonexistent/path')
|
230
|
+
}.not_to raise_error
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
describe 'complex scenarios' do
|
235
|
+
let(:nested_config_dir) { File.join(temp_dir, 'app', 'config') }
|
236
|
+
let(:nested_file) { File.join(nested_config_dir, 'settings.yml') }
|
237
|
+
|
238
|
+
before do
|
239
|
+
FileUtils.mkdir_p(nested_config_dir)
|
240
|
+
File.write(nested_file, "nested: true\n")
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'works with nested directory structures' do
|
244
|
+
instance = described_class::ConfigDir.new('app/config', root_path: temp_dir, env_var: nil, env_var_name: nil)
|
245
|
+
result = instance.read('settings.yml')
|
246
|
+
expect(result).to eq "nested: true\n"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConstConf::EnvDirExtension do
|
4
|
+
module TestConfig
|
5
|
+
include ConstConf
|
6
|
+
|
7
|
+
description 'Test Configuration'
|
8
|
+
|
9
|
+
module EnvDir
|
10
|
+
extend ConstConf::EnvDirExtension
|
11
|
+
description 'All variables loaded from .env directory'
|
12
|
+
|
13
|
+
load_dotenv_dir('spec/assets/.env/*') {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#load_dotenv_dir' do
|
18
|
+
it 'creates constants with proper naming and metadata including descriptions' do
|
19
|
+
# Verify the setting exists and has correct properties
|
20
|
+
expect(TestConfig::EnvDir::API_KEY).to eq "sk-1234567890abcdef1234567890abcdef"
|
21
|
+
|
22
|
+
# Test that it's marked as sensitive (since it's a secret)
|
23
|
+
setting = TestConfig::EnvDir.settings['API_KEY']
|
24
|
+
expect(setting.sensitive?).to be true
|
25
|
+
|
26
|
+
# Test that it has proper description from the file
|
27
|
+
expect(setting.description).to include("Value of \"API_KEY\" from")
|
28
|
+
expect(TestConfig.settings.size).to eq 0
|
29
|
+
expect(TestConfig::EnvDir.settings.size).to eq 1
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'handles duplicate definitions gracefully with proper error handling' do
|
33
|
+
module TestConfigWithManualSetting
|
34
|
+
include ConstConf
|
35
|
+
|
36
|
+
description 'Test Configuration'
|
37
|
+
|
38
|
+
# Manually define first (should take precedence)
|
39
|
+
API_KEY = set do
|
40
|
+
description 'Manually defined API_KEY'
|
41
|
+
prefix ''
|
42
|
+
|
43
|
+
default "manual_value"
|
44
|
+
end
|
45
|
+
|
46
|
+
module EnvDir
|
47
|
+
extend ConstConf::EnvDirExtension
|
48
|
+
description 'All variables loaded from .env directory'
|
49
|
+
|
50
|
+
load_dotenv_dir('spec/assets/.env/*') {}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# Should use the manual definition, not the file content
|
54
|
+
expect(TestConfigWithManualSetting::API_KEY).to eq "manual_value"
|
55
|
+
expect(TestConfigWithManualSetting.settings.size).to eq 1
|
56
|
+
expect(TestConfigWithManualSetting::EnvDir.settings.size).to eq 0
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConstConf::FilePlugin do
|
4
|
+
let(:temp_dir) { Dir.mktmpdir }
|
5
|
+
let(:config_file) { File.join(temp_dir, 'config.txt') }
|
6
|
+
let(:nonexistent_file) { File.join(temp_dir, 'nonexistent.txt') }
|
7
|
+
|
8
|
+
before do
|
9
|
+
eval %{
|
10
|
+
if Object.const_defined?(:ConstConfTestModule)
|
11
|
+
Object.send(:remove_const, :ConstConfTestModule)
|
12
|
+
end
|
13
|
+
module ::ConstConfTestModule
|
14
|
+
include ConstConf
|
15
|
+
|
16
|
+
description 'Outer configuration namespace'
|
17
|
+
end
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
let :instance do
|
23
|
+
double('Config').extend(described_class)
|
24
|
+
end
|
25
|
+
|
26
|
+
before do
|
27
|
+
# Create a test config file with content
|
28
|
+
File.write(config_file, "test_content\nwith_newlines")
|
29
|
+
end
|
30
|
+
|
31
|
+
after do
|
32
|
+
FileUtils.rm_rf(temp_dir)
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#file' do
|
36
|
+
context 'when file exists' do
|
37
|
+
it 'reads the file content as a string' do
|
38
|
+
result = instance.file(config_file)
|
39
|
+
expect(result).to eq "test_content\nwith_newlines"
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'handles files with UTF-8 encoding' do
|
43
|
+
utf8_file = File.join(temp_dir, 'utf8.txt')
|
44
|
+
File.write(utf8_file, "café 🚀\n")
|
45
|
+
|
46
|
+
result = instance.file(utf8_file)
|
47
|
+
expect(result).to eq "café 🚀\n"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when file does not exist' do
|
52
|
+
it 'returns nil when file does not exist and required is false' do
|
53
|
+
result = instance.file(nonexistent_file, required: false)
|
54
|
+
expect(result).to be_nil
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'raises RequiredValueNotConfigured when file is required and does not exist' do
|
58
|
+
expect {
|
59
|
+
instance.file(nonexistent_file, required: true)
|
60
|
+
}.to raise_error(ConstConf::RequiredValueNotConfigured)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'with custom configuration module' do
|
65
|
+
before do
|
66
|
+
eval %{
|
67
|
+
module ConstConfTestModule::ModuleWithFilePlugin
|
68
|
+
include ConstConf
|
69
|
+
plugin ConstConf::FilePlugin
|
70
|
+
|
71
|
+
description 'Module with FilePlugin'
|
72
|
+
|
73
|
+
CONFIG_VALUE = set do
|
74
|
+
description 'Configuration from file'
|
75
|
+
default file("#{config_file}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'can be used in a ConstConf module' do
|
82
|
+
expect(ConstConfTestModule::ModuleWithFilePlugin::CONFIG_VALUE).to eq "test_content\nwith_newlines"
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'handles required files properly' do
|
86
|
+
expect {
|
87
|
+
eval %{
|
88
|
+
module ConstConfTestModule::ModuleWithRequired
|
89
|
+
include ConstConf
|
90
|
+
plugin ConstConf::FilePlugin
|
91
|
+
|
92
|
+
description 'Module with Required'
|
93
|
+
|
94
|
+
REQUIRED_CONFIG = set do
|
95
|
+
description 'Required config from file'
|
96
|
+
default file("#{nonexistent_file}", required: true)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
}
|
100
|
+
}.to raise_error(ConstConf::RequiredValueNotConfigured)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe 'integration with ConstConf settings' do
|
106
|
+
before do
|
107
|
+
eval %{
|
108
|
+
module ConstConfTestModule::ConfigModule
|
109
|
+
include ConstConf
|
110
|
+
plugin ConstConf::FilePlugin
|
111
|
+
|
112
|
+
description 'Some ConfigModule'
|
113
|
+
|
114
|
+
# Test file reading in setting definition
|
115
|
+
API_KEY = set do
|
116
|
+
description 'API key from file'
|
117
|
+
default file("#{config_file}")
|
118
|
+
decode(&:chomp)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Test with required file
|
122
|
+
REQUIRED_CONFIG = set do
|
123
|
+
description 'Required config file'
|
124
|
+
default file("#{nonexistent_file}", required: false)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'properly reads file content' do
|
131
|
+
expect(ConstConfTestModule::ConfigModule::API_KEY).to eq "test_content\nwith_newlines"
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'handles decoding operations on file content' do
|
135
|
+
# The decode(&:chomp) should strip trailing whitespace/newlines
|
136
|
+
expect(ConstConfTestModule::ConfigModule::API_KEY).to eq "test_content\nwith_newlines"
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'handles missing files gracefully when not required' do
|
140
|
+
expect(ConstConfTestModule::ConfigModule::REQUIRED_CONFIG).to be_nil
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe 'error handling' do
|
145
|
+
it 'properly raises exceptions for invalid file paths' do
|
146
|
+
# This would test edge cases in the underlying File.read behavior
|
147
|
+
expect {
|
148
|
+
instance.file('/nonexistent/directory/file.txt')
|
149
|
+
}.not_to raise_error
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'with directory-based setup' do
|
154
|
+
let(:config_dir) { Dir.mktmpdir }
|
155
|
+
|
156
|
+
after do
|
157
|
+
FileUtils.rm_rf(config_dir)
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'works correctly with different file operations' do
|
161
|
+
# Create a nested structure for testing
|
162
|
+
subdir = File.join(config_dir, 'subdir')
|
163
|
+
FileUtils.mkdir_p(subdir)
|
164
|
+
|
165
|
+
test_file = File.join(subdir, 'test.txt')
|
166
|
+
File.write(test_file, 'nested_content')
|
167
|
+
|
168
|
+
# This tests that the plugin can be used in different contexts
|
169
|
+
result = instance.file(test_file)
|
170
|
+
expect(result).to eq 'nested_content'
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConstConf::JSONPlugin do
|
4
|
+
let(:json_file) { asset('config.json') }
|
5
|
+
|
6
|
+
describe '#json' do
|
7
|
+
context 'when file exists' do
|
8
|
+
it 'reads and parses JSON content as a hash' do
|
9
|
+
instance = double('Config').extend(described_class)
|
10
|
+
result = instance.json(json_file)
|
11
|
+
expect(result.hello).to eq 'world'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'handles JSON with custom object_class' do
|
15
|
+
instance = double('Config').extend(described_class)
|
16
|
+
foobar_class = Class.new(JSON::GenericObject)
|
17
|
+
result = instance.json(json_file, object_class: foobar_class)
|
18
|
+
expect(result).to be_a(foobar_class)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when file does not exist' do
|
23
|
+
let(:nonexistent_file) { asset('nonexistent.json') }
|
24
|
+
|
25
|
+
it 'returns nil when file does not exist and required is false' do
|
26
|
+
instance = double('Config').extend(described_class)
|
27
|
+
result = instance.json(nonexistent_file, required: false)
|
28
|
+
expect(result).to be_nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'raises RequiredValueNotConfigured when file is required and does not exist' do
|
32
|
+
instance = double('Config').extend(described_class)
|
33
|
+
expect {
|
34
|
+
instance.json(nonexistent_file, required: true)
|
35
|
+
}.to raise_error(ConstConf::RequiredValueNotConfigured)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'with custom configuration module' do
|
40
|
+
before do
|
41
|
+
eval %{
|
42
|
+
if Object.const_defined?(:ConstConfTestModuleWithJSON)
|
43
|
+
Object.send(:remove_const, :ConstConfTestModuleWithJSON)
|
44
|
+
end
|
45
|
+
module ::ConstConfTestModuleWithJSON
|
46
|
+
include ConstConf
|
47
|
+
plugin ConstConf::JSONPlugin
|
48
|
+
|
49
|
+
description 'Module with JSONPlugin'
|
50
|
+
|
51
|
+
CONFIG_VALUE = set do
|
52
|
+
description 'Configuration from JSON file'
|
53
|
+
default json("#{json_file}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'can be used in a ConstConf module' do
|
60
|
+
expect(ConstConfTestModuleWithJSON::CONFIG_VALUE.hello).to eq 'world'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConstConf::SettingAccessor do
|
4
|
+
let(:klass) do
|
5
|
+
Class.new do
|
6
|
+
extend ConstConf::SettingAccessor
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.setting_accessor' do
|
11
|
+
context 'with default value' do
|
12
|
+
before do
|
13
|
+
klass.setting_accessor :foobar, 'default_value'
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns default value when not set' do
|
17
|
+
instance = klass.new
|
18
|
+
expect(instance.foobar).to eq 'default_value'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns set value when explicitly set' do
|
22
|
+
instance = klass.new
|
23
|
+
instance.foobar 'explicit_value'
|
24
|
+
expect(instance.foobar).to eq 'explicit_value'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'with block default' do
|
29
|
+
before do
|
30
|
+
klass.setting_accessor :foobar do
|
31
|
+
'block_default'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'evaluates block for default value' do
|
36
|
+
instance = klass.new
|
37
|
+
expect(instance.foobar).to eq 'block_default'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'returns set value when explicitly set' do
|
41
|
+
instance = klass.new
|
42
|
+
instance.foobar 'explicit_value'
|
43
|
+
expect(instance.foobar).to eq 'explicit_value'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'with arg_block' do
|
48
|
+
before do
|
49
|
+
klass.setting_accessor :foobar
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'foo' do
|
53
|
+
instance = klass.new
|
54
|
+
expect(instance.foobar).to be nil
|
55
|
+
instance.foobar do
|
56
|
+
:bar
|
57
|
+
end
|
58
|
+
expect(instance.foobar).to be_a Proc
|
59
|
+
expect(instance.foobar.()).to be :bar
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'setter mode behavior' do
|
64
|
+
before do
|
65
|
+
klass.setting_accessor :required_setting, nil
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'raises ArgumentError when in setter_mode and value is nil' do
|
69
|
+
# This requires setting up thread-local setter_mode
|
70
|
+
expect do
|
71
|
+
instance = klass.new
|
72
|
+
# Simulate setter mode behavior
|
73
|
+
allow(klass).to receive(:setter_mode).and_return(true)
|
74
|
+
instance.required_setting
|
75
|
+
end.to raise_error(ArgumentError, /need an argument/)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'with positional argument' do
|
80
|
+
before do
|
81
|
+
klass.setting_accessor :foobar, 'default'
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'accepts positional argument for setting value' do
|
85
|
+
instance = klass.new
|
86
|
+
# This is a bit tricky to test since the method signature is complex
|
87
|
+
expect(instance.foobar('explicit')).to eq 'explicit'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'with block argument' do
|
92
|
+
before do
|
93
|
+
klass.setting_accessor :foobar, 'default'
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'accepts block argument for setting value' do
|
97
|
+
instance = klass.new
|
98
|
+
result = instance.foobar { 'block_value' }
|
99
|
+
expect(result).to be_a Proc
|
100
|
+
expect(instance.foobar.()).to eq 'block_value'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe 'thread_local behavior' do
|
106
|
+
it 'manages thread-local setter_mode state' do
|
107
|
+
expect(klass.setter_mode).to be_falsey
|
108
|
+
klass.enable_setter_mode do
|
109
|
+
expect(klass.setter_mode).to be_truthy
|
110
|
+
end
|
111
|
+
expect(klass.setter_mode).to be_falsey
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|