Skip to content

Loading…

Add support for multiple files #20

Open
wants to merge 5 commits into from

6 participants

@greghaygood

I've updated SettingsLogic to support multiple files, in order to allow local environments to override settings in source control managed global files as needed.

@igor-alexandrov

Here is Gist, how this can be easily done without any patching.
https://gist.github.com/1462080

@vitaliel

Hello,

Thanks for the feature, but it will look better if we will have support for config directories:

/etc/app_name.d/
/etc/app_name.yml

and these directories can contain more *.yml files.

@enortham

I was thinking about doing the same because I have secure information that I want to be different in staging and production. Generally the file is going to be sym linked during a deploy. I realized that it was easier to just create another Settings Logic object for example SecureSettings and link/overwrite the development/test version of the config file. We don't store the sensitive information in source control so this turned out to be a trivial alternative solution. I wanted to share this solution in case it's useful for someone else.

@m5rk

You may want to checkout chamber, which supports an arbitrary number of files.

@X0nic X0nic referenced this pull request in settingslogic/settingslogic
Open

decide what to merge from binarylogic/settingslogic #1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Showing with 132 additions and 16 deletions.
  1. +7 −0 README.rdoc
  2. +64 −13 lib/settingslogic.rb
  3. +6 −1 spec/settings.yml
  4. +15 −0 spec/settings4.rb
  5. +3 −0 spec/settings_invalid.yml
  6. +6 −0 spec/settings_local.yml
  7. +28 −0 spec/settingslogic_spec.rb
  8. +3 −2 spec/spec_helper.rb
View
7 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.
View
77 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)
View
7 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
+ does: not either
View
15 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
View
3 spec/settings_invalid.yml
@@ -0,0 +1,3 @@
+setting1: invalid_value
+ when: nesting
+
View
6 spec/settings_local.yml
@@ -0,0 +1,6 @@
+setting2: 10
+
+going:
+ going:
+ and: gone
+
View
28 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
View
5 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
Something went wrong with that request. Please try again.