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 Stacked Configuration Files Feature #60

Open
wants to merge 2 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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('.')
Expand All @@ -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)
Expand Down Expand Up @@ -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"
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Up @@ -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")
Expand Down
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
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Up @@ -7,6 +7,7 @@
require 'settings3'
require 'settings4'
require 'settings_empty'
require 'stacked_settings'

# Needed to test Settings3
Object.send :define_method, 'collides' do
Expand Down
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