From 6495fc8b5ecc07e65923059c2f494fd15a7a6101 Mon Sep 17 00:00:00 2001 From: Greg Haygood Date: Sat, 16 Jul 2011 09:47:52 -0400 Subject: [PATCH 1/5] Add support for multiple files in order to support overrides; Update for rspec2 change --- README.rdoc | 7 +++++++ lib/settingslogic.rb | 42 +++++++++++++++++++++++++++----------- spec/settings4.rb | 3 +++ spec/settings_local.yml | 1 + spec/settingslogic_spec.rb | 4 ++++ spec/spec_helper.rb | 5 +++-- 6 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 spec/settings4.rb create mode 100644 spec/settings_local.yml 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..402413a 100644 --- a/lib/settingslogic.rb +++ b/lib/settingslogic.rb @@ -20,11 +20,12 @@ def get(key) curs end - def source(value = nil) - if value.nil? - @source + def source(*value) + #puts "source! #{value}" + if value.nil? or value == [] + @sources else - @source = value + @sources= value end end @@ -94,24 +95,41 @@ 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 = {} + hash_or_file_or_array.each do |filename| + #puts "loading from #{filename}" + hash.merge!(load_into_hash(filename)) 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) + return {} unless FileTest.exist?(file) + #puts "loading into hash from #{file} (namespace: #{self.class.namespace})" + hash = YAML.load(ERB.new(File.read(file)).result).to_hash + 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/settings4.rb b/spec/settings4.rb new file mode 100644 index 0000000..a0213bd --- /dev/null +++ b/spec/settings4.rb @@ -0,0 +1,3 @@ +class Settings4 < Settingslogic + source "#{File.dirname(__FILE__)}/settings.yml", "#{File.dirname(__FILE__)}/settings_local.yml", "#{File.dirname(__FILE__)}/settings_local2.yml" +end diff --git a/spec/settings_local.yml b/spec/settings_local.yml new file mode 100644 index 0000000..0e25d15 --- /dev/null +++ b/spec/settings_local.yml @@ -0,0 +1 @@ +setting2: 10 diff --git a/spec/settingslogic_spec.rb b/spec/settingslogic_spec.rb index e95a0e6..7ed7e2a 100644 --- a/spec/settingslogic_spec.rb +++ b/spec/settingslogic_spec.rb @@ -44,6 +44,10 @@ Settings3.collides.does.should == 'not' end + it "should override with local settings" do + Settings4.setting2.should == 10 + 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 From fa2d4840a28b613f341ae4068ba20427e67defba Mon Sep 17 00:00:00 2001 From: Brewster Date: Wed, 10 Aug 2011 18:41:33 -0700 Subject: [PATCH 2/5] added deep_merge! and deep_delete_blank hash methods --- lib/settingslogic.rb | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/settingslogic.rb b/lib/settingslogic.rb index 402413a..e9ceb2b 100644 --- a/lib/settingslogic.rb +++ b/lib/settingslogic.rb @@ -1,6 +1,19 @@ 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_blank + delete_if{|k, v| v.blank? or v.instance_of?(Hash) && v.deep_delete_blank.blank?} + end +end + # A simple settings solution using a YAML file. See README for more information. class Settingslogic < Hash class MissingSetting < StandardError; end @@ -22,7 +35,7 @@ def get(key) def source(*value) #puts "source! #{value}" - if value.nil? or value == [] + if value.blank? @sources else @sources= value @@ -106,7 +119,7 @@ def initialize(hash_or_file_or_array = self.class.source, section = nil) hash = {} hash_or_file_or_array.each do |filename| #puts "loading from #{filename}" - hash.merge!(load_into_hash(filename)) + hash.deep_merge!(load_into_hash(filename).deep_delete_blank) end self.replace hash else From 1fc13bdd1ca8237a076c040a1a7aa391379fbbc5 Mon Sep 17 00:00:00 2001 From: Brewster Date: Wed, 10 Aug 2011 19:00:10 -0700 Subject: [PATCH 3/5] changed deep_delete_blank to only delete if nil --- lib/settingslogic.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/settingslogic.rb b/lib/settingslogic.rb index e9ceb2b..2f9c4eb 100644 --- a/lib/settingslogic.rb +++ b/lib/settingslogic.rb @@ -9,8 +9,8 @@ def deep_merge!(other_hash) end self end - def deep_delete_blank - delete_if{|k, v| v.blank? or v.instance_of?(Hash) && v.deep_delete_blank.blank?} + def deep_delete_nil + delete_if{|k, v| v.nil? or v.instance_of?(Hash) && v.deep_delete_blank.empty?} end end @@ -119,7 +119,7 @@ def initialize(hash_or_file_or_array = self.class.source, section = nil) hash = {} hash_or_file_or_array.each do |filename| #puts "loading from #{filename}" - hash.deep_merge!(load_into_hash(filename).deep_delete_blank) + hash.deep_merge!(load_into_hash(filename).deep_delete_nil) end self.replace hash else From 996112d8a7a4b91b873c46194e96c3a04773dedc Mon Sep 17 00:00:00 2001 From: Greg Haygood Date: Wed, 10 Aug 2011 23:06:04 -0400 Subject: [PATCH 4/5] add spec for deeply nested merges --- lib/settingslogic.rb | 6 +++--- spec/settings.yml | 7 ++++++- spec/settings_local.yml | 5 +++++ spec/settingslogic_spec.rb | 4 ++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/settingslogic.rb b/lib/settingslogic.rb index 2f9c4eb..3dcc0e7 100644 --- a/lib/settingslogic.rb +++ b/lib/settingslogic.rb @@ -5,12 +5,12 @@ 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 + 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_blank.empty?} + delete_if{|k, v| v.nil? or v.instance_of?(Hash) && v.deep_delete_nil.empty?} end end @@ -35,7 +35,7 @@ def get(key) def source(*value) #puts "source! #{value}" - if value.blank? + if value.nil? || value.empty? @sources else @sources= value 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/settings_local.yml b/spec/settings_local.yml index 0e25d15..a733dd8 100644 --- a/spec/settings_local.yml +++ b/spec/settings_local.yml @@ -1 +1,6 @@ setting2: 10 + +going: + going: + and: gone + diff --git a/spec/settingslogic_spec.rb b/spec/settingslogic_spec.rb index 7ed7e2a..f947839 100644 --- a/spec/settingslogic_spec.rb +++ b/spec/settingslogic_spec.rb @@ -48,6 +48,10 @@ Settings4.setting2.should == 10 end + it "should override with local nested settings" do + Settings4.going.going.and.should == "gone" + end + it "should raise a helpful error message" do e = nil begin From 0408b4d9ef6148e6c1293dd25ee2ba288b6ba575 Mon Sep 17 00:00:00 2001 From: Greg Haygood Date: Sun, 14 Aug 2011 11:21:53 -0400 Subject: [PATCH 5/5] catch errors for missing and invalid settings files --- lib/settingslogic.rb | 38 +++++++++++++++++++++++++++++--------- spec/settings4.rb | 14 +++++++++++++- spec/settings_invalid.yml | 3 +++ spec/settingslogic_spec.rb | 20 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 spec/settings_invalid.yml diff --git a/lib/settingslogic.rb b/lib/settingslogic.rb index 3dcc0e7..072840e 100644 --- a/lib/settingslogic.rb +++ b/lib/settingslogic.rb @@ -16,7 +16,8 @@ def deep_delete_nil # 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: @@ -116,10 +117,12 @@ def initialize(hash_or_file_or_array = self.class.source, section = nil) when Hash self.replace hash_or_file_or_array when Array - hash = {} - hash_or_file_or_array.each do |filename| - #puts "loading from #{filename}" - hash.deep_merge!(load_into_hash(filename).deep_delete_nil) + 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 @@ -133,10 +136,27 @@ def initialize(hash_or_file_or_array = self.class.source, section = nil) create_accessors! end - def load_into_hash(file) - return {} unless FileTest.exist?(file) - #puts "loading into hash from #{file} (namespace: #{self.class.namespace})" - hash = YAML.load(ERB.new(File.read(file)).result).to_hash + 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 diff --git a/spec/settings4.rb b/spec/settings4.rb index a0213bd..9bc5ca8 100644 --- a/spec/settings4.rb +++ b/spec/settings4.rb @@ -1,3 +1,15 @@ class Settings4 < Settingslogic - source "#{File.dirname(__FILE__)}/settings.yml", "#{File.dirname(__FILE__)}/settings_local.yml", "#{File.dirname(__FILE__)}/settings_local2.yml" + 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/settingslogic_spec.rb b/spec/settingslogic_spec.rb index f947839..2c50254 100644 --- a/spec/settingslogic_spec.rb +++ b/spec/settingslogic_spec.rb @@ -50,6 +50,26 @@ 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