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.
@@ -0,0 +1 @@
1
+ sk-1234567890abcdef1234567890abcdef
@@ -0,0 +1,3 @@
1
+ {
2
+ "hello": "world"
3
+ }
@@ -0,0 +1 @@
1
+ hello: world
@@ -0,0 +1,7 @@
1
+ development: &development
2
+ hello: world
3
+
4
+ test: *development
5
+
6
+ production:
7
+ hello: outer 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