Skip to content

Commit 105c407

Browse files
Yoshijipkuczynski
authored andcommitted
Adds the option fail_on_missing (default false) (#182)
1 parent effa31b commit 105c407

5 files changed

Lines changed: 123 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
### New features
66

7-
...
7+
* `Config#fail_on_missing` option (default `false`) to raise a `KeyError` exception when accessing a non-existing key
8+
* Add ability to test if a value was set for a given key with `key?` and `has_key?`
89

910
## 1.5.1
1011

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,49 @@ between the schema and your config.
299299

300300
Check [dry-validation](https://github.com/dry-rb/dry-validation) for more details.
301301

302+
### Missing keys
303+
304+
For an example settings file:
305+
306+
```yaml
307+
size: 1
308+
server: google.com
309+
```
310+
311+
You can test if a value was set for a given key using `key?` and its alias `has_key?`:
312+
313+
```ruby
314+
Settings.key?(:path)
315+
# => false
316+
Settings.key?(:server)
317+
# => true
318+
```
319+
320+
By default, accessing to a missing key returns `nil`:
321+
322+
```ruby
323+
Settings.key?(:path)
324+
# => false
325+
Settings.path
326+
# => nil
327+
```
328+
329+
This is not "typo-safe". To solve this problem, you can configure the `fail_on_missing` option:
330+
331+
```ruby
332+
Config.setup do |config|
333+
config.fail_on_missing = true
334+
# ...
335+
end
336+
```
337+
338+
So it will raise a `KeyError` when accessing a non-existing key (similar to `Hash#fetch` behaviour):
339+
340+
```ruby
341+
Settings.path
342+
# => raises KeyError: key not found: :path
343+
```
344+
302345
### Environment variables
303346

304347
See section below for more details.

lib/config.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ module Config
1515
# Ensures the setup only gets run once
1616
@@_ran_once = false
1717

18-
mattr_accessor :const_name, :use_env, :env_prefix, :env_separator, :env_converter, :env_parse_values
18+
mattr_accessor :const_name, :use_env, :env_prefix, :env_separator,
19+
:env_converter, :env_parse_values, :fail_on_missing
1920
@@const_name = 'Settings'
2021
@@use_env = false
2122
@@env_prefix = @@const_name
2223
@@env_separator = '.'
2324
@@env_converter = :downcase
2425
@@env_parse_values = true
26+
@@fail_on_missing = false
2527

2628
# deep_merge options
2729
mattr_accessor :knockout_prefix, :overwrite_arrays

lib/config/options.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,19 @@ def []=(param, value)
153153
end
154154
end
155155

156+
delegate :key?, :has_key?, to: :table
157+
158+
def method_missing(method_name, *args)
159+
if Config.fail_on_missing && method_name !~ /.*(?==\z)/m
160+
raise KeyError, "key not found: #{method_name.inspect}" unless key?(method_name)
161+
end
162+
super
163+
end
164+
165+
def respond_to_missing?(*args)
166+
super
167+
end
168+
156169
protected
157170

158171
def descend_array(array)

spec/options_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,66 @@
114114
end
115115
end
116116

117+
context 'when fail_on_missing option' do
118+
before { Config.reset }
119+
120+
context 'is set to true' do
121+
before { Config.setup { |cfg| cfg.fail_on_missing = true } }
122+
123+
it 'should raise an error when accessing a missing key' do
124+
config = Config.load_files("#{fixture_path}/empty1.yml")
125+
126+
expect { config.not_existing_method }.to raise_error(KeyError)
127+
expect { config[:not_existing_method] }.to raise_error(KeyError)
128+
end
129+
130+
it 'should raise an error when accessing a removed key' do
131+
config = Config.load_files("#{fixture_path}/empty1.yml")
132+
133+
config.tmp_existing = 1337
134+
expect(config.tmp_existing).to eq(1337)
135+
136+
config.delete_field(:tmp_existing)
137+
expect { config.tmp_existing }.to raise_error(KeyError)
138+
expect { config[:tmp_existing] }.to raise_error(KeyError)
139+
end
140+
end
141+
142+
context 'is set to false' do
143+
before { Config.setup { |cfg| cfg.fail_on_missing = false } }
144+
145+
it 'should return nil when accessing a missing key' do
146+
config = Config.load_files("#{fixture_path}/empty1.yml")
147+
148+
expect(config.not_existing_method).to eq(nil)
149+
expect(config[:not_existing_method]).to eq(nil)
150+
end
151+
end
152+
end
153+
154+
context '#key? and #has_key? methods' do
155+
let(:config) {
156+
config = Config.load_files("#{fixture_path}/empty1.yml")
157+
config.existing = nil
158+
config.send('complex_value=', nil)
159+
config.send('even_more_complex_value==', nil)
160+
config.nested = Config.load_files("#{fixture_path}/empty2.yml")
161+
config.nested.existing = nil
162+
config
163+
}
164+
165+
it 'should test if a value exists for a given key' do
166+
expect(config.key?(:not_existing)).to eq(false)
167+
expect(config.key?(:complex_value)).to eq(true)
168+
expect(config.key?('even_more_complex_value='.to_sym)).to eq(true)
169+
expect(config.key?(:nested)).to eq(true)
170+
expect(config.nested.key?(:not_existing)).to eq(false)
171+
expect(config.nested.key?(:existing)).to eq(true)
172+
end
173+
174+
it 'should be sensible to key\'s class' do
175+
expect(config.key?(:existing)).to eq(true)
176+
expect(config.key?('existing')).to eq(false)
177+
end
178+
end
117179
end

0 commit comments

Comments
 (0)