-
Notifications
You must be signed in to change notification settings - Fork 188
Expand file tree
/
Copy pathsettingslogic.rb
More file actions
228 lines (192 loc) · 6.94 KB
/
settingslogic.rb
File metadata and controls
228 lines (192 loc) · 6.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
require "yaml"
require "erb"
require 'open-uri'
require 'hash_deep_merge'
# A simple settings solution using a YAML file. See README for more information.
class Settingslogic < Hash
class MissingSetting < StandardError; end
class << self
def name # :nodoc:
self.superclass != Hash && instance.key?("name") ? instance.name : super
end
# Enables Settings.get('nested.key.name') for dynamic access
def get(key)
parts = key.split('.')
curs = self
while p = parts.shift
curs = curs.send(p)
end
curs
end
def source(value = nil)
if value.is_a?(Array)
@source = flatten_stacked_settings_to_hash(value)
else
@source ||= value
end
end
# If initialize was given an array of settings, use deep merge to flatten
# all settings into one hash, where the last processed setting is chosen
def flatten_stacked_settings_to_hash (array_of_settings)
resulting_hash = Hash.new
array_of_settings.each do |settings|
new_hash = nil
case settings
when Hash
new_hash = settings
when String # assume it's a filename...
begin
file_contents = open(settings).read
new_hash = file_contents.empty? ? {} : YAML.load(ERB.new(file_contents).result).to_hash
rescue
end
else
new_hash = (settings.to_hash rescue nil)
end
#continue if empty
next if new_hash.empty?
if new_hash.is_a?(Hash)
resulting_hash.deep_merge!(new_hash)
else
puts "ExtendedSettings WARN : unable to add settings object : #{ settings.inspect}"
end
end
resulting_hash
end
def namespace(value = nil)
@namespace ||= value
end
def suppress_errors(value = nil)
@suppress_errors ||= value
end
def [](key)
instance.fetch(key.to_s, nil)
end
def []=(key, val)
# Setting[:key][:key2] = 'value' for dynamic settings
val = new(val, source) if val.is_a? Hash
instance.store(key.to_s, val)
instance.create_accessor_for(key, val)
end
def load!
instance
true
end
def reload!
@instance = nil
load!
end
private
def instance
return @instance if @instance
@instance = new
create_accessors!
@instance
end
def method_missing(name, *args, &block)
instance.send(name, *args, &block)
end
# It would be great to DRY this up somehow, someday, but it's difficult because
# of the singleton pattern. Basically this proxies Setting.foo to Setting.instance.foo
def create_accessors!
instance.each do |key,val|
create_accessor_for(key)
end
end
def create_accessor_for(key)
return unless key.to_s =~ /^\w+$/ # could have "some-setting:" which blows up eval
instance_eval "def #{key}; instance.send(:#{key}); end"
end
end
# Initializes a new settings object. You can initialize an object in any of the following ways:
#
# Settings.new(:application) # will look for config/application.yml
# Settings.new("application.yaml") # will look for application.yaml
# Settings.new("/var/configs/application.yml") # will look for /var/configs/application.yml
# Settings.new(:config1 => 1, :config2 => 2)
#
# Basically if you pass a symbol it will look for that file in the configs directory of your rails app,
# if you are using this in rails. If you pass a string it should be an absolute path to your settings file.
# Then you can pass a hash, and it just allows you to access the hash via methods.
def initialize(hash_or_file = self.class.source, section = nil)
case hash_or_file
when nil
raise Errno::ENOENT, "No file specified as Settingslogic source"
when Hash
self.replace hash_or_file
else
file_contents = open(hash_or_file).read
hash = file_contents.empty? ? {} : YAML.load(ERB.new(file_contents).result).to_hash
if self.class.namespace
hash = hash[self.class.namespace] or return missing_key("Missing setting '#{self.class.namespace}' in #{hash_or_file}")
end
self.replace hash
end
@section = section || self.class.source.to_yaml # so end of error says "in application.yml"
create_accessors!
end
# Called for dynamically-defined keys, and also the first key deferenced at the top-level, if load! is not used.
# Otherwise, create_accessors! (called by new) will have created actual methods for each key.
def method_missing(name, *args, &block)
key = name.to_s
return missing_key("Missing setting '#{key}' in #{@section}") unless has_key? key
value = fetch(key)
create_accessor_for(key)
value.is_a?(Hash) ? self.class.new(value, "'#{key}' section in #{@section}") : value
end
def [](key)
fetch(key.to_s, nil)
end
def []=(key,val)
# Setting[:key][:key2] = 'value' for dynamic settings
val = self.class.new(val, @section) if val.is_a? Hash
store(key.to_s, val)
create_accessor_for(key, val)
end
# Returns an instance of a Hash object
def to_hash
Hash[self]
end
# This handles naming collisions with Sinatra/Vlad/Capistrano. Since these use a set()
# helper that defines methods in Object, ANY method_missing ANYWHERE picks up the Vlad/Sinatra
# settings! So settings.deploy_to title actually calls Object.deploy_to (from set :deploy_to, "host"),
# rather than the app_yml['deploy_to'] hash. Jeezus.
def create_accessors!
self.each do |key,val|
create_accessor_for(key)
end
end
# Use instance_eval/class_eval because they're actually more efficient than define_method{}
# http://stackoverflow.com/questions/185947/ruby-definemethod-vs-def
# http://bmorearty.wordpress.com/2009/01/09/fun-with-rubys-instance_eval-and-class_eval/
def create_accessor_for(key, val=nil)
return unless key.to_s =~ /^\w+$/ # could have "some-setting:" which blows up eval
instance_variable_set("@#{key}", val)
self.class.class_eval <<-EndEval
def #{key}
return @#{key} if @#{key}
return missing_key("Missing setting '#{key}' in #{@section}") unless has_key? '#{key}'
value = fetch('#{key}')
@#{key} = if value.is_a?(Hash)
self.class.new(value, "'#{key}' section in #{@section}")
elsif value.is_a?(Array) && value.all?{|v| v.is_a? Hash}
value.map{|v| self.class.new(v)}
else
value
end
end
EndEval
end
def symbolize_keys
inject({}) do |memo, tuple|
k = (tuple.first.to_sym rescue tuple.first) || tuple.first
v = k.is_a?(Symbol) ? send(k) : tuple.last # make sure the value is accessed the same way Settings.foo.bar works
memo[k] = v && v.respond_to?(:symbolize_keys) ? v.symbolize_keys : v #recurse for nested hashes
memo
end
end
def missing_key(msg)
return nil if self.class.suppress_errors
raise MissingSetting, msg
end
end