Skip to content

Add Stacked Configuration Files Feature #60

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -8,3 +8,4 @@ benchmarks/*
.bundle
vendor/bundle
.rvmrc
Gemfile.lock
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
source :rubygems
source 'https://rubygems.org'
gemspec
26 changes: 0 additions & 26 deletions Gemfile.lock

This file was deleted.

63 changes: 50 additions & 13 deletions lib/settingslogic.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "yaml"
require "erb"
require 'open-uri'
require 'hash_deep_merge'

# A simple settings solution using a YAML file. See README for more information.
class Settingslogic < Hash
@@ -10,7 +11,7 @@ class << self
def name # :nodoc:
self.superclass != Hash && instance.key?("name") ? instance.name : super
end

# Enables Settings.get('nested.key.name') for dynamic access
def get(key)
parts = key.split('.')
@@ -22,7 +23,45 @@ def get(key)
end

def source(value = nil)
@source ||= value
if value.is_a?(Array)
@source = flatten_stacked_settings_to_hash(value)
else
@source ||= value
end
end

# If initialize was given an array of settings, use deep merge to flatten
# all settings into one hash, where the last processed setting is chosen
def flatten_stacked_settings_to_hash (array_of_settings)
resulting_hash = Hash.new
array_of_settings.each do |settings|
new_hash = nil
case settings

when Hash
new_hash = settings

when String # assume it's a filename...
begin
file_contents = open(settings).read
new_hash = file_contents.empty? ? {} : YAML.load(ERB.new(file_contents).result).to_hash
rescue
end

else
new_hash = (settings.to_hash rescue nil)
end

#continue if empty
next if new_hash.empty?

if new_hash.is_a?(Hash)
resulting_hash.deep_merge!(new_hash)
else
puts "ExtendedSettings WARN : unable to add settings object : #{ settings.inspect}"
end
end
resulting_hash
end

def namespace(value = nil)
@@ -92,7 +131,6 @@ def create_accessor_for(key)
# 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
when nil
raise Errno::ENOENT, "No file specified as Settingslogic source"
@@ -106,7 +144,7 @@ def initialize(hash_or_file = self.class.source, section = nil)
end
self.replace hash
end
@section = section || self.class.source # so end of error says "in application.yml"
@section = section || self.class.source.to_yaml # so end of error says "in application.yml"
create_accessors!
end

@@ -167,25 +205,24 @@ def #{key}
end
EndEval
end

def symbolize_keys

inject({}) do |memo, tuple|

k = (tuple.first.to_sym rescue tuple.first) || tuple.first

v = k.is_a?(Symbol) ? send(k) : tuple.last # make sure the value is accessed the same way Settings.foo.bar works

memo[k] = v && v.respond_to?(:symbolize_keys) ? v.symbolize_keys : v #recurse for nested hashes

memo
end

end

def missing_key(msg)
return nil if self.class.suppress_errors

raise MissingSetting, msg
end
end
4 changes: 4 additions & 0 deletions settingslogic.gemspec
Original file line number Diff line number Diff line change
@@ -13,6 +13,10 @@ Gem::Specification.new do |s|

s.add_development_dependency 'rake'
s.add_development_dependency 'rspec'
s.add_development_dependency 'pry'
s.add_development_dependency 'ruby-debug'

s.add_dependency 'hash-deep-merge', '~> 0.1.1'

s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
31 changes: 31 additions & 0 deletions spec/settings2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
setting1:
setting1_child: saweeter
deep:
another: my value
child:
value: 4

setting2: 5
setting3: <%= 5 * 5 %>
name: test is gone

language:
haskell:
paradigm: functional
smalltalk:
paradigm: object oriented

collides:
does: not
nested:
collides:
does: not either

new_stuff:
because: testing has to happen

array:
-
name: first
-
name: second
41 changes: 32 additions & 9 deletions spec/settingslogic_spec.rb
Original file line number Diff line number Diff line change
@@ -4,11 +4,11 @@
it "should access settings" do
Settings.setting2.should == 5
end

it "should access nested settings" do
Settings.setting1.setting1_child.should == "saweet"
end

it "should access settings in nested arrays" do
Settings.array.first.name.should == "first"
end
@@ -39,7 +39,7 @@
Settings.language.haskell.paradigm.should == 'functional'
Settings.language.smalltalk.paradigm.should == 'object oriented'
end

it "should not collide with global methods" do
Settings3.nested.collides.does.should == 'not either'
Settings3[:nested] = 'fooey'
@@ -77,7 +77,7 @@
end
e.should_not be_nil
e.message.should =~ /Missing setting 'erlang' in 'language' section/

Settings.language['erlang'].should be_nil
Settings.language['erlang'] = 5
Settings.language['erlang'].should == 5
@@ -168,14 +168,14 @@ class NoSource < Settingslogic; end
it "should allow a name setting to be overriden" do
Settings.name.should == 'test'
end

it "should allow symbolize_keys" do
Settings.reload!
result = Settings.language.haskell.symbolize_keys
result = Settings.language.haskell.symbolize_keys
result.class.should == Hash
result.should == {:paradigm => "functional"}
result.should == {:paradigm => "functional"}
end

it "should allow symbolize_keys on nested hashes" do
Settings.reload!
result = Settings.language.symbolize_keys
@@ -203,5 +203,28 @@ class NoSource < Settingslogic; end
Settings.to_hash.object_id.should_not == Settings.object_id
end
end

describe "providing mulitple configuration sources" do
describe "#flatten_stacked_settings_to_hash" do
it "returns a hash from mulitple sources" do
configs = [
"#{File.dirname(__FILE__)}/settings.yml",
"#{File.dirname(__FILE__)}/settings_empty.yml",
"#{File.dirname(__FILE__)}/settings2.yml"]
StackedSettings.flatten_stacked_settings_to_hash(configs)
.is_a?(Hash).should == true
end
it "returns the last setting for each configuration attributed" do
StackedSettings.setting1.setting1_child.should == 'saweeter'
end
it "adds new settings from secondary sources" do
StackedSettings.new_stuff.because.should == "testing has to happen"
end
it "keeps the latest setting for each variable" do
StackedSettings.language.haskell.paradigm.should == 'functional'
end
it "accepts a hash as a source" do
StackedSettings.setting2.should == 10
end
end
end
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
require 'settings3'
require 'settings4'
require 'settings_empty'
require 'stacked_settings'

# Needed to test Settings3
Object.send :define_method, 'collides' do
7 changes: 7 additions & 0 deletions spec/stacked_settings.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class StackedSettings < Settingslogic
source ["#{File.dirname(__FILE__)}/settings.yml",
"#{File.dirname(__FILE__)}/settings_empty.yml",
"#{File.dirname(__FILE__)}/settings2.yml",
{"setting2" => 10}]
load!
end