Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple files #20

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
77 changes: 64 additions & 13 deletions lib/settingslogic.rb
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion spec/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ setting1:
value: 2

setting2: 5

setting3: <%= 5 * 5 %>
name: test

going:
going:
and: going

language:
haskell:
paradigm: functional
Expand All @@ -19,4 +24,4 @@ collides:
does: not
nested:
collides:
does: not either
does: not either
15 changes: 15 additions & 0 deletions spec/settings4.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions spec/settings_invalid.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
setting1: invalid_value
when: nesting

6 changes: 6 additions & 0 deletions spec/settings_local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
setting2: 10

going:
going:
and: gone

28 changes: 28 additions & 0 deletions spec/settingslogic_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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