diff --git a/README.rdoc b/README.rdoc index 4be7030..594aa63 100644 --- a/README.rdoc +++ b/README.rdoc @@ -40,6 +40,13 @@ this file in a rails app is app/models/settings.rb I felt adding a settings file in your app was more straightforward, less tricky, and more flexible. +If multiple files are passed on the source line, comma-separated, they will be loaded in order, with settings in later files overriding any existing keys. This allows you to, for instance, maintain a global settings file in source control, while allowing each developer to override individual settings as needed. Files that are specified but which do not exist will simply be ignored. Thus you can safely do the following without requiring the presence of application_local.yml: + + class Settings < Settingslogic + source "#{Rails.root}/config/application.yml", "#{Rails.root}/config/application_local.yml" + namespace Rails.env + end + === 2. Create your settings Notice above we specified an absolute path to our settings file called "application.yml". This is just a typical YAML file. diff --git a/lib/settingslogic.rb b/lib/settingslogic.rb index 022b2f2..072840e 100644 --- a/lib/settingslogic.rb +++ b/lib/settingslogic.rb @@ -1,9 +1,23 @@ 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 MissingSetting < StandardError; end + class InvalidSettingsFile < StandardError; end class << self def name # :nodoc: @@ -20,11 +34,12 @@ def get(key) curs end - def source(value = nil) - if value.nil? - @source + def source(*value) + #puts "source! #{value}" + if value.nil? || value.empty? + @sources else - @source = value + @sources= value end end @@ -94,24 +109,60 @@ def create_accessor_for(key) # 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) - #puts "new! #{hash_or_file}" - case hash_or_file + 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 - else - hash = YAML.load(ERB.new(File.read(hash_or_file)).result).to_hash - if self.class.namespace - hash = hash[self.class.namespace] or raise MissingSetting, "Missing setting '#{self.class.namespace}' in #{hash_or_file}" + 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) diff --git a/spec/settings.yml b/spec/settings.yml index 6072d98..f20c335 100644 --- a/spec/settings.yml +++ b/spec/settings.yml @@ -6,9 +6,14 @@ setting1: value: 2 setting2: 5 + setting3: <%= 5 * 5 %> name: test +going: + going: + and: going + language: haskell: paradigm: functional @@ -19,4 +24,4 @@ collides: does: not nested: collides: - does: not either \ No newline at end of file + does: not either diff --git a/spec/settings4.rb b/spec/settings4.rb new file mode 100644 index 0000000..9bc5ca8 --- /dev/null +++ b/spec/settings4.rb @@ -0,0 +1,15 @@ +class Settings4 < Settingslogic + source "#{File.dirname(__FILE__)}/settings.yml", "#{File.dirname(__FILE__)}/settings_local.yml" +end + +class Settings4a < Settingslogic + source "#{File.dirname(__FILE__)}/settings.yml", "#{File.dirname(__FILE__)}/settings_local_missing.yml", "#{File.dirname(__FILE__)}/settings_invalid.yml" +end + +class Settings4b < Settingslogic + source "#{File.dirname(__FILE__)}/settings_local_missing.yml" +end + +class Settings4c < Settingslogic + source "#{File.dirname(__FILE__)}/settings_invalid.yml" +end diff --git a/spec/settings_invalid.yml b/spec/settings_invalid.yml new file mode 100644 index 0000000..c073e0d --- /dev/null +++ b/spec/settings_invalid.yml @@ -0,0 +1,3 @@ +setting1: invalid_value + when: nesting + \ No newline at end of file diff --git a/spec/settings_local.yml b/spec/settings_local.yml new file mode 100644 index 0000000..a733dd8 --- /dev/null +++ b/spec/settings_local.yml @@ -0,0 +1,6 @@ +setting2: 10 + +going: + going: + and: gone + diff --git a/spec/settingslogic_spec.rb b/spec/settingslogic_spec.rb index e95a0e6..2c50254 100644 --- a/spec/settingslogic_spec.rb +++ b/spec/settingslogic_spec.rb @@ -44,6 +44,34 @@ Settings3.collides.does.should == 'not' end + it "should override with local settings" do + Settings4.setting2.should == 10 + end + + it "should override with local nested settings" do + Settings4.going.going.and.should == "gone" + end + + it "should not raise error for missing or invalid additional files" do + Settings4a.setting1.setting1_child.should == "saweet" + end + + it "should raise an error for a missing initial file" do + begin + Settings4b.setting1 + rescue => e + e.should be_kind_of Settingslogic::InvalidSettingsFile + end + end + + it "should raise an error for an invalid initial file" do + begin + Settings4c.setting1 + rescue => e + e.should be_kind_of Settingslogic::InvalidSettingsFile + end + end + it "should raise a helpful error message" do e = nil begin diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0a3f64a..3ec675b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,4 @@ -require 'spec' +require 'rspec' require 'rubygems' require 'ruby-debug' if RUBY_VERSION < '1.9' # ruby-debug does not work on 1.9.1 yet @@ -8,11 +8,12 @@ require 'settings' require 'settings2' require 'settings3' +require 'settings4' # Needed to test Settings3 Object.send :define_method, 'collides' do 'collision' end -Spec::Runner.configure do |config| +RSpec.configure do |config| end