readwritesettings 3.0.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.
- data/.gitignore +10 -0
- data/.travis.yml +12 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +26 -0
- data/LICENSE +20 -0
- data/README.md +196 -0
- data/Rakefile +7 -0
- data/lib/readwritesettings.rb +246 -0
- data/readwritesettings.gemspec +21 -0
- data/spec/readwritesettings_spec.rb +284 -0
- data/spec/settings.rb +6 -0
- data/spec/settings.yml +28 -0
- data/spec/settings2.rb +4 -0
- data/spec/settings3.rb +4 -0
- data/spec/settings4.rb +4 -0
- data/spec/settings_empty.rb +3 -0
- data/spec/settings_empty.yml +0 -0
- data/spec/spec_helper.rb +17 -0
- metadata +108 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
settingslogic (2.0.9)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.1.3)
|
10
|
+
rake (10.0.3)
|
11
|
+
rspec (2.12.0)
|
12
|
+
rspec-core (~> 2.12.0)
|
13
|
+
rspec-expectations (~> 2.12.0)
|
14
|
+
rspec-mocks (~> 2.12.0)
|
15
|
+
rspec-core (2.12.2)
|
16
|
+
rspec-expectations (2.12.1)
|
17
|
+
diff-lcs (~> 1.1.3)
|
18
|
+
rspec-mocks (2.12.1)
|
19
|
+
|
20
|
+
PLATFORMS
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
rake
|
25
|
+
rspec
|
26
|
+
settingslogic!
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Ben Johnson of Binary Logic (binarylogic.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
# ReadWriteSettings
|
2
|
+
|
3
|
+
ReadWriteSettings is a simple configuration / settings solution that uses an ERB enabled YAML file. It has been great for
|
4
|
+
our apps, maybe you will enjoy it too. ReadWriteSettings works with Rails, Sinatra, or any Ruby project.
|
5
|
+
|
6
|
+
It is a fork of {ReadWriteSettings}[http://github.com/binarylogic/settingslogic] to support modifications and additional getting/setter methods. Hopefully can merge this fork back into ReadWriteSettings in future and have a single project again.
|
7
|
+
|
8
|
+
## Helpful links
|
9
|
+
|
10
|
+
* <b>Issues:</b> http://github.com/drnic/readwritesettings/issues
|
11
|
+
* <b>Source:</b> http://github.com/drnic/readwritesettings
|
12
|
+
* [](https://travis-ci.org/drnic/readwritesettings)
|
13
|
+
* [](https://codeclimate.com/github/drnic/readwritesettings)
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add the following to your Gemfile:
|
18
|
+
|
19
|
+
``` ruby
|
20
|
+
gem "readwritesettings"
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
### 1. Define your class
|
26
|
+
|
27
|
+
Instead of defining a Settings constant for you, that task is left to you. Simply create a class in your application
|
28
|
+
that looks like:
|
29
|
+
|
30
|
+
``` ruby
|
31
|
+
class Settings < ReadWriteSettings
|
32
|
+
source "#{Rails.root}/config/application.yml"
|
33
|
+
namespace Rails.env
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
Name it Settings, name it Config, name it whatever you want. Add as many or as few as you like. A good place to put
|
38
|
+
this file in a rails app is app/models/settings.rb
|
39
|
+
|
40
|
+
I felt adding a settings file in your app was more straightforward, less tricky, and more flexible.
|
41
|
+
|
42
|
+
Alternately, you can pass a Hash into the constructor.
|
43
|
+
|
44
|
+
``` ruby
|
45
|
+
Settings = ReadWriteSettings.new({"key" => "value"})
|
46
|
+
```
|
47
|
+
|
48
|
+
### 2. Create your settings
|
49
|
+
|
50
|
+
Notice above we specified an absolute path to our settings file called "application.yml". This is just a typical YAML file.
|
51
|
+
Also notice above that we specified a namespace for our environment. A namespace is just an optional string that corresponds
|
52
|
+
to a key in the YAML file.
|
53
|
+
|
54
|
+
Using a namespace allows us to change our configuration depending on our environment:
|
55
|
+
|
56
|
+
``` yaml
|
57
|
+
# config/application.yml
|
58
|
+
defaults: &defaults
|
59
|
+
cool:
|
60
|
+
saweet: nested settings
|
61
|
+
neat_setting: 24
|
62
|
+
awesome_setting: <%= "Did you know 5 + 5 = #{5 + 5}?" %>
|
63
|
+
|
64
|
+
development:
|
65
|
+
<<: *defaults
|
66
|
+
neat_setting: 800
|
67
|
+
|
68
|
+
test:
|
69
|
+
<<: *defaults
|
70
|
+
|
71
|
+
production:
|
72
|
+
<<: *defaults
|
73
|
+
```
|
74
|
+
|
75
|
+
_Note_: Certain Ruby/Bundler versions include a version of the Psych YAML parser which incorrectly handles merges (the `<<` in the example above.)
|
76
|
+
If your default settings seem to be overwriting your environment-specific settings, including the following lines in your config/boot.rb file may solve the problem:
|
77
|
+
|
78
|
+
``` ruby
|
79
|
+
require 'yaml'
|
80
|
+
YAML::ENGINE.yamler= 'syck'
|
81
|
+
```
|
82
|
+
|
83
|
+
### 3. Access your settings
|
84
|
+
|
85
|
+
``` ruby
|
86
|
+
>> Rails.env
|
87
|
+
=> "development"
|
88
|
+
|
89
|
+
>> Settings.cool
|
90
|
+
=> "#<ReadWriteSettings::Settings ... >"
|
91
|
+
|
92
|
+
>> Settings.cool.saweet
|
93
|
+
=> "nested settings"
|
94
|
+
|
95
|
+
>> Settings.neat_setting
|
96
|
+
=> 800
|
97
|
+
|
98
|
+
>> Settings.awesome_setting
|
99
|
+
=> "Did you know 5 + 5 = 10?"
|
100
|
+
```
|
101
|
+
|
102
|
+
You can use these settings anywhere, for example in a model:
|
103
|
+
|
104
|
+
``` ruby
|
105
|
+
class Post < ActiveRecord::Base
|
106
|
+
self.per_page = Settings.pagination.posts_per_page
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
### 4. Optional / dynamic settings
|
111
|
+
|
112
|
+
Often, you will want to handle defaults in your application logic itself, to reduce the number of settings
|
113
|
+
you need to put in your YAML file. You can access an optional setting by using Hash notation:
|
114
|
+
|
115
|
+
``` ruby
|
116
|
+
>> Settings.messaging.queue_name
|
117
|
+
=> Exception: Missing setting 'queue_name' in 'message' section in 'application.yml'
|
118
|
+
|
119
|
+
>> Settings.messaging['queue_name']
|
120
|
+
=> nil
|
121
|
+
|
122
|
+
>> Settings.messaging['queue_name'] ||= 'user_mail'
|
123
|
+
=> "user_mail"
|
124
|
+
|
125
|
+
>> Settings.messaging.queue_name
|
126
|
+
=> "user_mail"
|
127
|
+
```
|
128
|
+
|
129
|
+
Modifying our model example:
|
130
|
+
|
131
|
+
``` ruby
|
132
|
+
class Post < ActiveRecord::Base
|
133
|
+
self.per_page = Settings.posts['per_page'] || Settings.pagination.per_page
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
This would allow you to specify a custom value for per_page just for posts, or
|
138
|
+
to fall back to your default value if not specified.
|
139
|
+
|
140
|
+
### 5. Suppressing Exceptions Conditionally
|
141
|
+
|
142
|
+
Raising exceptions for missing settings helps highlight configuration problems. However, in a
|
143
|
+
Rails app it may make sense to suppress this in production and return nil for missing settings.
|
144
|
+
While it's useful to stop and highlight an error in development or test environments, this is
|
145
|
+
often not the right answer for production.
|
146
|
+
|
147
|
+
``` ruby
|
148
|
+
class Settings < ReadWriteSettings
|
149
|
+
source "#{Rails.root}/config/application.yml"
|
150
|
+
namespace Rails.env
|
151
|
+
suppress_errors Rails.env.production?
|
152
|
+
end
|
153
|
+
|
154
|
+
>> Settings.non_existent_key
|
155
|
+
=> nil
|
156
|
+
```
|
157
|
+
|
158
|
+
## Note on Sinatra / Capistrano / Vlad
|
159
|
+
|
160
|
+
Each of these frameworks uses a +set+ convention for settings, which actually defines methods
|
161
|
+
in the global Object namespace:
|
162
|
+
|
163
|
+
``` ruby
|
164
|
+
set :application, "myapp" # does "def application" globally
|
165
|
+
```
|
166
|
+
|
167
|
+
This can cause collisions with ReadWriteSettings, since those methods are global. Luckily, the
|
168
|
+
solution is to just add a call to load! in your class:
|
169
|
+
|
170
|
+
``` ruby
|
171
|
+
class Settings < ReadWriteSettings
|
172
|
+
source "#{Rails.root}/config/application.yml"
|
173
|
+
namespace Rails.env
|
174
|
+
load!
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
It's probably always safest to add load! to your class, since this guarantees settings will be
|
179
|
+
loaded at that time, rather than lazily later via method_missing.
|
180
|
+
|
181
|
+
Finally, you can reload all your settings later as well:
|
182
|
+
|
183
|
+
``` ruby
|
184
|
+
Settings.reload!
|
185
|
+
```
|
186
|
+
|
187
|
+
This is useful if you want to support changing your settings YAML without restarting your app.
|
188
|
+
|
189
|
+
## History
|
190
|
+
|
191
|
+
This project was originally created by Ben Johnson and called ReadWriteSettings. A renamed fork was created so that new gem versions could be released with new functionality, by Dr Nic Williams. The latter is not entirely thrilled about this situation; but at least he now has a versioned gem with the new shiny stuff in it.
|
192
|
+
|
193
|
+
Copyright (c) 2008-2010 {Ben Johnson}[http://github.com/binarylogic] of {Binary Logic}[http://www.binarylogic.com],
|
194
|
+
released under the MIT license. Support for optional settings and reloading by {Nate Wiger}[http://nate.wiger.org].
|
195
|
+
|
196
|
+
Copyright (c) 2013 {Dr Nic Williams}[http://github.com/drnic]
|
data/Rakefile
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "erb"
|
3
|
+
require 'open-uri'
|
4
|
+
|
5
|
+
# A simple settings solution using a YAML file. See README for more information.
|
6
|
+
class ReadWriteSettings < Hash
|
7
|
+
class MissingSetting < StandardError; end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def name # :nodoc:
|
11
|
+
self.superclass != Hash && instance.key?("name") ? instance.name : super
|
12
|
+
end
|
13
|
+
|
14
|
+
# Enables Settings.get('nested.key.name') for dynamic access
|
15
|
+
def get(key)
|
16
|
+
parts = key.split('.')
|
17
|
+
curs = self
|
18
|
+
while p = parts.shift
|
19
|
+
curs = curs.send(p)
|
20
|
+
end
|
21
|
+
curs
|
22
|
+
end
|
23
|
+
|
24
|
+
def source(value = nil)
|
25
|
+
@source ||= value
|
26
|
+
end
|
27
|
+
|
28
|
+
def namespace(value = nil)
|
29
|
+
@namespace ||= value
|
30
|
+
end
|
31
|
+
|
32
|
+
def suppress_errors(value = nil)
|
33
|
+
@suppress_errors ||= value
|
34
|
+
end
|
35
|
+
|
36
|
+
def [](key)
|
37
|
+
instance.fetch(key.to_s, nil)
|
38
|
+
end
|
39
|
+
|
40
|
+
def []=(key, val)
|
41
|
+
# Setting[:key][:key2] = 'value' for dynamic settings
|
42
|
+
val = new(val, source) if val.is_a? Hash
|
43
|
+
instance.store(key.to_s, val)
|
44
|
+
instance.create_accessor_for(key, val)
|
45
|
+
end
|
46
|
+
|
47
|
+
def load!
|
48
|
+
instance
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def reload!
|
53
|
+
@instance = nil
|
54
|
+
load!
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def instance
|
59
|
+
return @instance if @instance
|
60
|
+
@instance = new
|
61
|
+
create_accessors!
|
62
|
+
@instance
|
63
|
+
end
|
64
|
+
|
65
|
+
def method_missing(name, *args, &block)
|
66
|
+
instance.send(name, *args, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
# It would be great to DRY this up somehow, someday, but it's difficult because
|
70
|
+
# of the singleton pattern. Basically this proxies Setting.foo to Setting.instance.foo
|
71
|
+
def create_accessors!
|
72
|
+
instance.each do |key,val|
|
73
|
+
create_accessor_for(key)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def create_accessor_for(key)
|
78
|
+
return unless key.to_s =~ /^\w+$/ # could have "some-setting:" which blows up eval
|
79
|
+
instance_eval "def #{key}; instance.send(:#{key}); end"
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
# Initializes a new settings object. You can initialize an object in any of the following ways:
|
85
|
+
#
|
86
|
+
# Settings.new(:application) # will look for config/application.yml
|
87
|
+
# Settings.new("application.yaml") # will look for application.yaml
|
88
|
+
# Settings.new("/var/configs/application.yml") # will look for /var/configs/application.yml
|
89
|
+
# Settings.new(:config1 => 1, :config2 => 2)
|
90
|
+
#
|
91
|
+
# Basically if you pass a symbol it will look for that file in the configs directory of your rails app,
|
92
|
+
# if you are using this in rails. If you pass a string it should be an absolute path to your settings file.
|
93
|
+
# Then you can pass a hash, and it just allows you to access the hash via methods.
|
94
|
+
def initialize(hash_or_file = self.class.source, section = nil)
|
95
|
+
#puts "new! #{hash_or_file}"
|
96
|
+
case hash_or_file
|
97
|
+
when nil
|
98
|
+
raise Errno::ENOENT, "No file specified as ReadWriteSettings source"
|
99
|
+
when Hash
|
100
|
+
self.replace hash_or_file
|
101
|
+
else
|
102
|
+
file_contents = open(hash_or_file).read
|
103
|
+
hash = file_contents.empty? ? {} : YAML.load(ERB.new(file_contents).result).to_hash
|
104
|
+
if self.class.namespace
|
105
|
+
hash = hash[self.class.namespace] or return missing_key("Missing setting '#{self.class.namespace}' in #{hash_or_file}")
|
106
|
+
end
|
107
|
+
self.replace hash
|
108
|
+
end
|
109
|
+
@section = section || self.class.source # so end of error says "in application.yml"
|
110
|
+
create_accessors!
|
111
|
+
end
|
112
|
+
|
113
|
+
# Called for dynamically-defined keys, and also the first key deferenced at the top-level, if load! is not used.
|
114
|
+
# Otherwise, create_accessors! (called by new) will have created actual methods for each key.
|
115
|
+
def method_missing(name, *args, &block)
|
116
|
+
key = name.to_s
|
117
|
+
return missing_key("Missing setting '#{key}' in #{@section}") unless has_key? key
|
118
|
+
value = fetch(key)
|
119
|
+
create_accessor_for(key)
|
120
|
+
value.is_a?(Hash) ? self.class.new(value, "'#{key}' section in #{@section}") : value
|
121
|
+
end
|
122
|
+
|
123
|
+
def [](key)
|
124
|
+
fetch(key.to_s, nil)
|
125
|
+
end
|
126
|
+
|
127
|
+
def []=(key,val)
|
128
|
+
# Setting[:key][:key2] = 'value' for dynamic settings
|
129
|
+
val = self.class.new(val, @section) if val.is_a? Hash
|
130
|
+
store(key.to_s, val)
|
131
|
+
create_accessor_for(key, val)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Create a nested structure and set value.
|
135
|
+
# For example: set("foo.bar.tar", 123)
|
136
|
+
# Resulting ReadWriteSettings/Hash:
|
137
|
+
# { "foo" => { "bar" => { "tar" => 123 }}}
|
138
|
+
def set(nested_key, val)
|
139
|
+
target_settings_field = self
|
140
|
+
settings_key_portions = nested_key.to_s.split(".")
|
141
|
+
parent_key_portions, final_key = settings_key_portions[0..-2], settings_key_portions[-1]
|
142
|
+
parent_key_portions.each do |key_portion|
|
143
|
+
target_settings_field[key_portion] ||= ReadWriteSettings.new({})
|
144
|
+
target_settings_field = target_settings_field[key_portion]
|
145
|
+
end
|
146
|
+
target_settings_field[final_key] = val
|
147
|
+
create_accessors!
|
148
|
+
end
|
149
|
+
|
150
|
+
# Like #set, but only sets the value if the key is not already set
|
151
|
+
# Returns the existing value or the newly-set default value
|
152
|
+
def set_default(nested_key, val)
|
153
|
+
target_settings_field = self
|
154
|
+
settings_key_portions = nested_key.to_s.split(".")
|
155
|
+
parent_key_portions, final_key = settings_key_portions[0..-2], settings_key_portions[-1]
|
156
|
+
parent_key_portions.each do |key_portion|
|
157
|
+
target_settings_field[key_portion] ||= ReadWriteSettings.new({})
|
158
|
+
target_settings_field = target_settings_field[key_portion]
|
159
|
+
end
|
160
|
+
target_settings_field[final_key] ||= val
|
161
|
+
target_settings_field[final_key]
|
162
|
+
end
|
163
|
+
|
164
|
+
def nested_value(nested_key)
|
165
|
+
target_settings_field = self
|
166
|
+
settings_key_portions = nested_key.to_s.split(".")
|
167
|
+
parent_key_portions, final_key = settings_key_portions[0..-2], settings_key_portions[-1]
|
168
|
+
parent_key_portions.each do |key_portion|
|
169
|
+
target_settings_field[key_portion] ||= ReadWriteSettings.new({})
|
170
|
+
target_settings_field = target_settings_field[key_portion]
|
171
|
+
end
|
172
|
+
target_settings_field[final_key]
|
173
|
+
end
|
174
|
+
alias :exists? :nested_value
|
175
|
+
|
176
|
+
# Returns an instance of a Hash object
|
177
|
+
def to_hash
|
178
|
+
Hash[self]
|
179
|
+
end
|
180
|
+
|
181
|
+
# Convert all nested ReadWriteSettings objects to Hash objects
|
182
|
+
def to_nested_hash
|
183
|
+
inject({}) do |hash, key_value|
|
184
|
+
key, value = key_value
|
185
|
+
hash[key] = value.respond_to?(:to_nested_hash) ? value.to_nested_hash : value
|
186
|
+
hash
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def save(path)
|
191
|
+
File.open(path, "w") { |f| f << to_nested_hash.to_yaml }
|
192
|
+
end
|
193
|
+
|
194
|
+
# This handles naming collisions with Sinatra/Vlad/Capistrano. Since these use a set()
|
195
|
+
# helper that defines methods in Object, ANY method_missing ANYWHERE picks up the Vlad/Sinatra
|
196
|
+
# settings! So settings.deploy_to title actually calls Object.deploy_to (from set :deploy_to, "host"),
|
197
|
+
# rather than the app_yml['deploy_to'] hash. Jeezus.
|
198
|
+
def create_accessors!
|
199
|
+
self.each do |key,val|
|
200
|
+
create_accessor_for(key)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Use instance_eval/class_eval because they're actually more efficient than define_method{}
|
205
|
+
# http://stackoverflow.com/questions/185947/ruby-definemethod-vs-def
|
206
|
+
# http://bmorearty.wordpress.com/2009/01/09/fun-with-rubys-instance_eval-and-class_eval/
|
207
|
+
def create_accessor_for(key, val=nil)
|
208
|
+
return unless key.to_s =~ /^\w+$/ # could have "some-setting:" which blows up eval
|
209
|
+
instance_variable_set("@#{key}", val)
|
210
|
+
self.class.class_eval <<-EndEval
|
211
|
+
def #{key}
|
212
|
+
return @#{key} if @#{key}
|
213
|
+
return missing_key("Missing setting '#{key}' in #{@section}") unless has_key? '#{key}'
|
214
|
+
value = fetch('#{key}')
|
215
|
+
@#{key} = if value.is_a?(Hash)
|
216
|
+
self.class.new(value, "'#{key}' section in #{@section}")
|
217
|
+
elsif value.is_a?(Array) && value.all?{|v| v.is_a? Hash}
|
218
|
+
value.map{|v| self.class.new(v)}
|
219
|
+
else
|
220
|
+
value
|
221
|
+
end
|
222
|
+
end
|
223
|
+
EndEval
|
224
|
+
end
|
225
|
+
|
226
|
+
def symbolize_keys
|
227
|
+
|
228
|
+
inject({}) do |memo, tuple|
|
229
|
+
|
230
|
+
k = (tuple.first.to_sym rescue tuple.first) || tuple.first
|
231
|
+
|
232
|
+
v = k.is_a?(Symbol) ? send(k) : tuple.last # make sure the value is accessed the same way Settings.foo.bar works
|
233
|
+
|
234
|
+
memo[k] = v && v.respond_to?(:symbolize_keys) ? v.symbolize_keys : v #recurse for nested hashes
|
235
|
+
|
236
|
+
memo
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
def missing_key(msg)
|
242
|
+
return nil if self.class.suppress_errors
|
243
|
+
|
244
|
+
raise MissingSetting, msg
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "readwritesettings"
|
6
|
+
s.version = "3.0.1"
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Dr Nic Williams", "Ben Johnson"]
|
9
|
+
s.email = ["drnicwilliams@gmail.com", "bjohnson@binarylogic.com"]
|
10
|
+
s.homepage = "http://github.com/drnic/readwritesettings"
|
11
|
+
s.summary = %q{A simple settings solution that uses an ERB enabled YAML file and a singleton design pattern.}
|
12
|
+
s.description = %q{A simple settings solution that uses an ERB enabled YAML file and a singleton design pattern.}
|
13
|
+
|
14
|
+
s.add_development_dependency 'rake'
|
15
|
+
s.add_development_dependency 'rspec'
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
@@ -0,0 +1,284 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/spec_helper")
|
2
|
+
|
3
|
+
describe "ReadWriteSettings" do
|
4
|
+
it "should access settings" do
|
5
|
+
Settings.setting2.should == 5
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should access nested settings" do
|
9
|
+
Settings.setting1.setting1_child.should == "saweet"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should access settings in nested arrays" do
|
13
|
+
Settings.array.first.name.should == "first"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should access deep nested settings" do
|
17
|
+
Settings.setting1.deep.another.should == "my value"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should access extra deep nested settings" do
|
21
|
+
Settings.setting1.deep.child.value.should == 2
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should enable erb" do
|
25
|
+
Settings.setting3.should == 25
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should namespace settings" do
|
29
|
+
Settings2.setting1_child.should == "saweet"
|
30
|
+
Settings2.deep.another.should == "my value"
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return the namespace" do
|
34
|
+
Settings.namespace.should be_nil
|
35
|
+
Settings2.namespace.should == 'setting1'
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should distinguish nested keys" do
|
39
|
+
Settings.language.haskell.paradigm.should == 'functional'
|
40
|
+
Settings.language.smalltalk.paradigm.should == 'object oriented'
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should not collide with global methods" do
|
44
|
+
Settings3.nested.collides.does.should == 'not either'
|
45
|
+
Settings3[:nested] = 'fooey'
|
46
|
+
Settings3[:nested].should == 'fooey'
|
47
|
+
Settings3.nested.should == 'fooey'
|
48
|
+
Settings3.collides.does.should == 'not'
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should raise a helpful error message" do
|
52
|
+
e = nil
|
53
|
+
begin
|
54
|
+
Settings.missing
|
55
|
+
rescue => e
|
56
|
+
e.should be_kind_of ReadWriteSettings::MissingSetting
|
57
|
+
end
|
58
|
+
e.should_not be_nil
|
59
|
+
e.message.should =~ /Missing setting 'missing' in/
|
60
|
+
|
61
|
+
e = nil
|
62
|
+
begin
|
63
|
+
Settings.language.missing
|
64
|
+
rescue => e
|
65
|
+
e.should be_kind_of ReadWriteSettings::MissingSetting
|
66
|
+
end
|
67
|
+
e.should_not be_nil
|
68
|
+
e.message.should =~ /Missing setting 'missing' in 'language' section/
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should handle optional / dynamic settings" do
|
72
|
+
e = nil
|
73
|
+
begin
|
74
|
+
Settings.language.erlang
|
75
|
+
rescue => e
|
76
|
+
e.should be_kind_of ReadWriteSettings::MissingSetting
|
77
|
+
end
|
78
|
+
e.should_not be_nil
|
79
|
+
e.message.should =~ /Missing setting 'erlang' in 'language' section/
|
80
|
+
|
81
|
+
Settings.language['erlang'].should be_nil
|
82
|
+
Settings.language['erlang'] = 5
|
83
|
+
Settings.language['erlang'].should == 5
|
84
|
+
|
85
|
+
Settings.language['erlang'] = {'paradigm' => 'functional'}
|
86
|
+
Settings.language.erlang.paradigm.should == 'functional'
|
87
|
+
Settings.respond_to?('erlang').should be_false
|
88
|
+
|
89
|
+
Settings.reload!
|
90
|
+
Settings.language['erlang'].should be_nil
|
91
|
+
|
92
|
+
Settings.language[:erlang] ||= 5
|
93
|
+
Settings.language[:erlang].should == 5
|
94
|
+
|
95
|
+
Settings.language[:erlang] = {}
|
96
|
+
Settings.language[:erlang][:paradigm] = 'functional'
|
97
|
+
Settings.language.erlang.paradigm.should == 'functional'
|
98
|
+
|
99
|
+
Settings[:toplevel] = '42'
|
100
|
+
Settings.toplevel.should == '42'
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should raise an error on a nil source argument" do
|
104
|
+
class NoSource < ReadWriteSettings; end
|
105
|
+
e = nil
|
106
|
+
begin
|
107
|
+
NoSource.foo.bar
|
108
|
+
rescue => e
|
109
|
+
e.should be_kind_of Errno::ENOENT
|
110
|
+
end
|
111
|
+
e.should_not be_nil
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should allow suppressing errors" do
|
115
|
+
Settings4.non_existent_key.should be_nil
|
116
|
+
end
|
117
|
+
|
118
|
+
# This one edge case currently does not pass, because it requires very
|
119
|
+
# esoteric code in order to make it pass. It was judged not worth fixing,
|
120
|
+
# as it introduces significant complexity for minor gain.
|
121
|
+
# it "should handle reloading top-level settings"
|
122
|
+
# Settings[:inspect] = 'yeah baby'
|
123
|
+
# Settings.inspect.should == 'yeah baby'
|
124
|
+
# Settings.reload!
|
125
|
+
# Settings.inspect.should == 'Settings'
|
126
|
+
# end
|
127
|
+
|
128
|
+
it "should handle oddly-named settings" do
|
129
|
+
Settings.language['some-dash-setting#'] = 'dashtastic'
|
130
|
+
Settings.language['some-dash-setting#'].should == 'dashtastic'
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should handle settings with nil value" do
|
134
|
+
Settings["flag"] = true
|
135
|
+
Settings["flag"] = nil
|
136
|
+
Settings.flag.should == nil
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should handle settings with false value" do
|
140
|
+
Settings["flag"] = true
|
141
|
+
Settings["flag"] = false
|
142
|
+
Settings.flag.should == false
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should support instance usage as well" do
|
146
|
+
settings = SettingsInst.new(Settings.source)
|
147
|
+
settings.setting1.setting1_child.should == "saweet"
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should be able to get() a key with dot.notation" do
|
151
|
+
Settings.get('setting1.setting1_child').should == "saweet"
|
152
|
+
Settings.get('setting1.deep.another').should == "my value"
|
153
|
+
Settings.get('setting1.deep.child.value').should == 2
|
154
|
+
end
|
155
|
+
|
156
|
+
# If .name is not a property, delegate to superclass
|
157
|
+
it "should respond with Module.name" do
|
158
|
+
Settings2.name.should == "Settings2"
|
159
|
+
end
|
160
|
+
|
161
|
+
# If .name is called on ReadWriteSettings itself, handle appropriately
|
162
|
+
# by delegating to Hash
|
163
|
+
it "should have the parent class always respond with Module.name" do
|
164
|
+
ReadWriteSettings.name.should == 'ReadWriteSettings'
|
165
|
+
end
|
166
|
+
|
167
|
+
# If .name is a property, respond with that instead of delegating to superclass
|
168
|
+
it "should allow a name setting to be overriden" do
|
169
|
+
Settings.name.should == 'test'
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should allow symbolize_keys" do
|
173
|
+
Settings.reload!
|
174
|
+
result = Settings.language.haskell.symbolize_keys
|
175
|
+
result.class.should == Hash
|
176
|
+
result.should == {:paradigm => "functional"}
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should allow symbolize_keys on nested hashes" do
|
180
|
+
Settings.reload!
|
181
|
+
result = Settings.language.symbolize_keys
|
182
|
+
result.class.should == Hash
|
183
|
+
result.should == {
|
184
|
+
:haskell => {:paradigm => "functional"},
|
185
|
+
:smalltalk => {:paradigm => "object oriented"}
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
it "should handle empty file" do
|
190
|
+
SettingsEmpty.keys.should eql([])
|
191
|
+
end
|
192
|
+
|
193
|
+
# Put this test last or else call to .instance will load @instance,
|
194
|
+
# masking bugs.
|
195
|
+
it "should be a hash" do
|
196
|
+
Settings.send(:instance).should be_is_a(Hash)
|
197
|
+
end
|
198
|
+
|
199
|
+
describe "#to_hash" do
|
200
|
+
it "should return a new instance of a Hash object" do
|
201
|
+
Settings.to_hash.should be_kind_of(Hash)
|
202
|
+
Settings.to_hash.class.name.should == "Hash"
|
203
|
+
Settings.to_hash.object_id.should_not == Settings.object_id
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "#to_nested_hash" do
|
208
|
+
it "should convert all nested ReadWriteSettings objects to Hash objects" do
|
209
|
+
hash = Settings.to_nested_hash
|
210
|
+
hash.class.should == Hash
|
211
|
+
hash["language"].class.should == Hash
|
212
|
+
hash["language"]["haskell"].class.should == Hash
|
213
|
+
hash["language"]["haskell"]["paradigm"].class.should == String
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe "#save(file)" do
|
218
|
+
it "should save ReadWriteSettings object such that it can be reloaded later" do
|
219
|
+
Settings.reload!
|
220
|
+
Settings["extra"] = {}
|
221
|
+
Settings["extra"]["value"] = 123
|
222
|
+
Settings.extra.value.should == 123
|
223
|
+
Settings.save("/tmp/settings.yml")
|
224
|
+
|
225
|
+
later_on = ReadWriteSettings.new("/tmp/settings.yml")
|
226
|
+
later_on.extra.value.should == 123
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
describe "#set('nested.key', value)" do
|
231
|
+
it "works like []= for non-dotted keys" do
|
232
|
+
Settings.set("simple", "value")
|
233
|
+
Settings.simple.should == "value"
|
234
|
+
end
|
235
|
+
|
236
|
+
it "creates new nested structures" do
|
237
|
+
Settings.set("nested.key", "value")
|
238
|
+
Settings.nested.key.should == "value"
|
239
|
+
end
|
240
|
+
|
241
|
+
it "reuses existing structures" do
|
242
|
+
Settings.set("language.ruby.paradigm", "scripting")
|
243
|
+
Settings.language.haskell.paradigm.should == "functional"
|
244
|
+
Settings.language.ruby.paradigm.should == "scripting"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
describe "#set_default('nested.key', value)" do
|
249
|
+
it "works like #set if key 'nested.key' missing" do
|
250
|
+
Settings.set("nested", {})
|
251
|
+
Settings.set_default("nested.key", "default").should == "default"
|
252
|
+
Settings.nested.key.should == "default"
|
253
|
+
end
|
254
|
+
|
255
|
+
it "works like #set if key 'nested' missing" do
|
256
|
+
Settings.set_default("nested.key", "default").should == "default"
|
257
|
+
Settings.nested.key.should == "default"
|
258
|
+
end
|
259
|
+
|
260
|
+
it "does nothing if nested.key is set" do
|
261
|
+
Settings.set("nested.key", "value")
|
262
|
+
Settings.set_default("nested.key", "default").should == "value"
|
263
|
+
Settings.nested.key.should == "value"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
describe "#exists?('nested.key') && #value('nested.key')" do
|
268
|
+
before { Settings.reload! }
|
269
|
+
it "returns truthy if nested.key is set" do
|
270
|
+
Settings.set("nested.key", "value")
|
271
|
+
Settings.nested_value("nested.key").should == "value"
|
272
|
+
end
|
273
|
+
|
274
|
+
it "returns falsy if nested is not set" do
|
275
|
+
Settings.nested_value("nested.key").should be_nil
|
276
|
+
end
|
277
|
+
|
278
|
+
it "returns falsy if nested.key is not set" do
|
279
|
+
Settings.set("nested", {})
|
280
|
+
Settings.exists?("nested.key").should be_nil
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
data/spec/settings.rb
ADDED
data/spec/settings.yml
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
setting1:
|
2
|
+
setting1_child: saweet
|
3
|
+
deep:
|
4
|
+
another: my value
|
5
|
+
child:
|
6
|
+
value: 2
|
7
|
+
|
8
|
+
setting2: 5
|
9
|
+
setting3: <%= 5 * 5 %>
|
10
|
+
name: test
|
11
|
+
|
12
|
+
language:
|
13
|
+
haskell:
|
14
|
+
paradigm: functional
|
15
|
+
smalltalk:
|
16
|
+
paradigm: object oriented
|
17
|
+
|
18
|
+
collides:
|
19
|
+
does: not
|
20
|
+
nested:
|
21
|
+
collides:
|
22
|
+
does: not either
|
23
|
+
|
24
|
+
array:
|
25
|
+
-
|
26
|
+
name: first
|
27
|
+
-
|
28
|
+
name: second
|
data/spec/settings2.rb
ADDED
data/spec/settings3.rb
ADDED
data/spec/settings4.rb
ADDED
File without changes
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'rspec'
|
4
|
+
require 'readwritesettings'
|
5
|
+
require 'settings'
|
6
|
+
require 'settings2'
|
7
|
+
require 'settings3'
|
8
|
+
require 'settings4'
|
9
|
+
require 'settings_empty'
|
10
|
+
|
11
|
+
# Needed to test Settings3
|
12
|
+
Object.send :define_method, 'collides' do
|
13
|
+
'collision'
|
14
|
+
end
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: readwritesettings
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 3.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dr Nic Williams
|
9
|
+
- Ben Johnson
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-05-20 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rake
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rspec
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
description: A simple settings solution that uses an ERB enabled YAML file and a singleton
|
48
|
+
design pattern.
|
49
|
+
email:
|
50
|
+
- drnicwilliams@gmail.com
|
51
|
+
- bjohnson@binarylogic.com
|
52
|
+
executables: []
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files: []
|
55
|
+
files:
|
56
|
+
- .gitignore
|
57
|
+
- .travis.yml
|
58
|
+
- Gemfile
|
59
|
+
- Gemfile.lock
|
60
|
+
- LICENSE
|
61
|
+
- README.md
|
62
|
+
- Rakefile
|
63
|
+
- lib/readwritesettings.rb
|
64
|
+
- readwritesettings.gemspec
|
65
|
+
- spec/readwritesettings_spec.rb
|
66
|
+
- spec/settings.rb
|
67
|
+
- spec/settings.yml
|
68
|
+
- spec/settings2.rb
|
69
|
+
- spec/settings3.rb
|
70
|
+
- spec/settings4.rb
|
71
|
+
- spec/settings_empty.rb
|
72
|
+
- spec/settings_empty.yml
|
73
|
+
- spec/spec_helper.rb
|
74
|
+
homepage: http://github.com/drnic/readwritesettings
|
75
|
+
licenses: []
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 1.8.25
|
95
|
+
signing_key:
|
96
|
+
specification_version: 3
|
97
|
+
summary: A simple settings solution that uses an ERB enabled YAML file and a singleton
|
98
|
+
design pattern.
|
99
|
+
test_files:
|
100
|
+
- spec/readwritesettings_spec.rb
|
101
|
+
- spec/settings.rb
|
102
|
+
- spec/settings.yml
|
103
|
+
- spec/settings2.rb
|
104
|
+
- spec/settings3.rb
|
105
|
+
- spec/settings4.rb
|
106
|
+
- spec/settings_empty.rb
|
107
|
+
- spec/settings_empty.yml
|
108
|
+
- spec/spec_helper.rb
|