-
Notifications
You must be signed in to change notification settings - Fork 188
Expand file tree
/
Copy pathsettingslogic.rb
More file actions
212 lines (189 loc) · 6.65 KB
/
settingslogic.rb
File metadata and controls
212 lines (189 loc) · 6.65 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
require "yaml"
require "erb"
class Hash
def deep_merge!(other_hash)
other_hash.each_pair do |k,v|
tv = self[k]
self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge!(v) : v
end
self
end
def deep_delete_nil
delete_if{|k, v| v.nil? or v.instance_of?(Hash) && v.deep_delete_nil.empty?}
end
end
# A simple settings solution using a YAML file. See README for more information.
class Settingslogic < Hash
class MissingSetting < StandardError; end
class InvalidSettingsFile < StandardError; end
class << self
def name # :nodoc:
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)
#puts "source! #{value}"
if value.nil? || value.empty?
@sources
else
@sources= value
end
end
def namespace(value = nil)
if value.nil?
@namespace
else
@namespace = value
end
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_or_array = self.class.source, section = nil)
#puts "new! #{hash_or_file_or_array.inspect} (section: #{section})"
case hash_or_file_or_array
when nil
raise Errno::ENOENT, "No file specified as Settingslogic source"
when Hash
self.replace hash_or_file_or_array
when Array
hash = {}
ignore_load_error = false
hash_or_file_or_array.each_with_index do |filename, n|
#puts "loading from #{filename}"
ignore_load_error = (n!=0)
hash.deep_merge!(load_into_hash(filename, ignore_load_error).deep_delete_nil)
end
self.replace hash
else
hash = load_into_hash(hash_or_file_or_array)
self.replace hash
end
@section = section || self.class.source # so end of error says "in application.yml"
if @section.is_a?(Array)
@section = @section.first # TODO: is there a better way to preserve which file was used?
end
create_accessors!
end
def load_into_hash(file, ignore_on_error=false)
unless FileTest.exist?(file)
if ignore_on_error
return {}
else
raise InvalidSettingsFile, file
end
end
#puts "\n\nloading into hash from #{file} (namespace: #{self.class.namespace}) (ignore_error: #{ignore_on_error})"
begin
hash = YAML.load(ERB.new(File.read(file)).result).to_hash
rescue Exception => ex
#puts ex.inspect
#puts "ignoring? #{ignore_on_error}"
if ignore_on_error
return {}
else
raise InvalidSettingsFile, file
end
end
if self.class.namespace
hash = hash[self.class.namespace] or raise MissingSetting, "Missing setting '#{self.class.namespace}' in #{file}"
end
hash
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
raise MissingSetting, "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
# 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) if val
self.class.class_eval <<-EndEval
def #{key}
return @#{key} if @#{key}
raise MissingSetting, "Missing setting '#{key}' in #{@section}" unless has_key? '#{key}'
value = fetch('#{key}')
@#{key} = value.is_a?(Hash) ? self.class.new(value, "'#{key}' section in #{@section}") : value
end
EndEval
end
end