Skip to content

Commit

Permalink
def keys as methods rather than using method_missing, added load!/rel…
Browse files Browse the repository at this point in the history
…oad!, more docs/specs
  • Loading branch information
Nate Wiger authored and binarylogic committed Jan 29, 2010
1 parent 9d11fce commit c8da048
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 49 deletions.
84 changes: 66 additions & 18 deletions README.rdoc
@@ -1,6 +1,7 @@
= Settingslogic = Settingslogic


Settingslogic is a simple configuration / settings solution that uses an ERB enabled YAML file. It has been great for my apps, maybe you will enjoy it too. Settingslogic is a simple configuration / settings solution that uses an ERB enabled YAML file. It has been great for
our apps, maybe you will enjoy it too. Settingslogic works with Rails, Sinatra, or any Ruby project.


So here is my question to you.....is Settingslogic a great settings solution or the greatest? So here is my question to you.....is Settingslogic a great settings solution or the greatest?


Expand All @@ -10,43 +11,47 @@ So here is my question to you.....is Settingslogic a great settings solution or
* <b>Repository:</b> http://github.com/binarylogic/settingslogic/tree/master * <b>Repository:</b> http://github.com/binarylogic/settingslogic/tree/master
* <b>Issues:</b> http://github.com/binarylogic/settingslogic/issues * <b>Issues:</b> http://github.com/binarylogic/settingslogic/issues


== Install and use == Installation


Install from rubyforge: Install from rubyforge/gemcutter:


sudo gem install settingslogic sudo gem install settingslogic


Install from github: Or as a Rails plugin:


sudo gem install binarylogic-settingslogic script/plugin install git://github.com/binarylogic/settingslogic.git


Or as a plugin Settingslogic does not have any dependencies on Rails. Installing as a gem is recommended.


script/plugin install git://github.com/binarylogic/settingslogic.git == Usage


== 1. Define your constant === 1. Define your class


Instead of defining a Settings constant for you, that task is left to you. Simply create a class in your application that looks like: Instead of defining a Settings constant for you, that task is left to you. Simply create a class in your application
that looks like:


class Settings < Settingslogic class Settings < Settingslogic
source "#{Rails.root}/config/application.yml" source "#{Rails.root}/config/application.yml"
namespace Rails.env namespace Rails.env
end end


Name it Settings, name it Config, name it whatever you want. Add as many or as few as you like. A good place to put this file in a rails app is models/settings.rb Name it Settings, name it Config, name it whatever you want. Add as many or as few as you like. A good place to put
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. I felt adding a settings file in your app was more straightforward, less tricky, and more flexible.


== 2. Create your settings === 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. Also notice above that we specified a namespace for our environment. This allows us to namespace our configuration depending on our environment: Notice above we specified an absolute path to our settings file called "application.yml". This is just a typical YAML file.
Also notice above that we specified a namespace for our environment. A namespace is just a string that corresponds to a key
in the YAML file. By using Rails.env, this allows us to namespace our configuration depending on our environment:


# app/config/application.yml # app/config/application.yml
defaults: &defaults defaults: &defaults
cool: cool:
saweet: nested settings saweet: nested settings
neat_setting: 24 neat_setting: 24
awesome_setting: <%= "Did you know 5 + 5 = " + (5 + 5) + "?" %> awesome_setting: <%= "Did you know 5 + 5 = #{5 + 5}?" %>


development: development:
<<: *defaults <<: *defaults
Expand All @@ -58,11 +63,11 @@ Notice above we specified an absolute path to our settings file called "applicat
production: production:
<<: *defaults <<: *defaults


== Access your settings === 3. Access your settings

>> Rails.env
=> "development"


>> Rails.env.development?
=> true

>> Settings.cool >> Settings.cool
=> "#<Settingslogic::Settings ... >" => "#<Settingslogic::Settings ... >"


Expand All @@ -75,5 +80,48 @@ Notice above we specified an absolute path to our settings file called "applicat
>> Settings.awesome_setting >> Settings.awesome_setting
=> "Did you know 5 + 5 = 10?" => "Did you know 5 + 5 = 10?"


=== 4. Optional / dynamic settings

Often, you will want to handle defaults in your application logic itself, to reduce the number of settings
you need to put in your YAML file. You can access an optional setting by using Hash notation:

>> Settings.messaging.queue_name
=> Exception: Missing setting 'queue_name' in 'message' section in 'application.yml'

>> Settings.messaging['queue_name']
=> nil

>> Settings.messaging['queue_name'] ||= 'user_mail'
=> "user_mail"

>> Settings.messaging.queue_name
=> "user_mail"

== Note on Sinatra / Capistrano / Vlad

Each of these uses a +set+ convention which actually defines methods in the global Object namespace:

set :application, "myapp" # does "def application" globally

This can cause collisions with Settingslogic, since this settings approach makes them global.
Luckily, the solution is to just add a call to load! in your class:

class Settings < Settingslogic
source "#{Rails.root}/config/application.yml"
namespace Rails.env
load!
end

Truthfully, it's always safest to add load! to your class, since this guarantees settings will be
loaded at that time, rather than lazily later via method_missing.

Finally, you can reload all your settings dynamically as well:

Settings.reload!

This is useful if you want to support changing your settings YAML without restarting your app.

== Author


Copyright (c) 2008 {Ben Johnson}[http://github.com/binarylogic] of {Binary Logic}[http://www.binarylogic.com], released under the MIT license Copyright (c) 2008-2010 {Ben Johnson}[http://github.com/binarylogic] of {Binary Logic}[http://www.binarylogic.com],
released under the MIT license. Support for optional settings and avoiding collisions by {Nate Wiger}[http://nate.wiger.org].
83 changes: 59 additions & 24 deletions lib/settingslogic.rb
Expand Up @@ -3,6 +3,8 @@


# A simple settings solution using a YAML file. See README for more information. # A simple settings solution using a YAML file. See README for more information.
class Settingslogic < Hash class Settingslogic < Hash
class MissingSetting < StandardError; end

class << self class << self
def name # :nodoc: def name # :nodoc:
instance.key?("name") ? instance.name : super instance.key?("name") ? instance.name : super
Expand All @@ -15,7 +17,7 @@ def source(value = nil)
@source = value @source = value
end end
end end

def namespace(value = nil) def namespace(value = nil)
if value.nil? if value.nil?
@namespace @namespace
Expand All @@ -24,6 +26,26 @@ def namespace(value = nil)
end end
end end


def [](key)
# Setting.key.value or Setting[:key][:value] or Setting['key']['value']
fetch(key.to_s,nil)
end

def []=(key,val)
# Setting[:key] = 'value' for dynamic settings
store(key.to_s,val)
end

def load!
instance
true
end

def reload!
@instance = nil
load!
end

private private
def instance def instance
@instance ||= new @instance ||= new
Expand All @@ -33,46 +55,59 @@ def method_missing(name, *args, &block)
instance.send(name, *args, &block) instance.send(name, *args, &block)
end end
end end

# Initializes a new settings object. You can initialize an object in any of the following ways: # Initializes a new settings object. You can initialize an object in any of the following ways:
# #
# Settings.new(:application) # will look for config/application.yml # Settings.new(:application) # will look for config/application.yml
# Settings.new("application.yaml") # will look for application.yaml # Settings.new("application.yaml") # will look for application.yaml
# Settings.new("/var/configs/application.yml") # will look for /var/configs/application.yml # Settings.new("/var/configs/application.yml") # will look for /var/configs/application.yml
# Settings.new(:config1 => 1, :config2 => 2) # Settings.new(:config1 => 1, :config2 => 2)
# #
# 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. # 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. # 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) def initialize(hash_or_file = self.class.source, section = nil)
case hash_or_file case hash_or_file
when Hash when Hash
self.update hash_or_file self.replace hash_or_file
else else
hash = YAML.load(ERB.new(File.read(hash_or_file)).result).to_hash hash = YAML.load(ERB.new(File.read(hash_or_file)).result).to_hash
hash = hash[self.class.namespace] if self.class.namespace hash = hash[self.class.namespace] if self.class.namespace
self.update hash self.replace hash
end end
@section = section || hash_or_file # so end of error says "in application.yml"
create_accessors!
end end

module EigenMethodDefiner # :nodoc:
def method_missing(name, *args, &block)
if key?(name.to_s)
define_eigen_method(name.to_s)
value = self[name.to_s]
value.extend(EigenMethodDefiner) if value.is_a?(Hash)
value
else
super
end
end


private # 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(key, *args, &block)
begin
value = fetch(key.to_s)
rescue KeyError
raise MissingSetting, "Missing setting '#{key}' in #{@section}"
end
value.is_a?(Hash) ? self.class.new(value, "'#{key}' section in #{@section}") : value
end


def define_eigen_method(name) private
eigen_class = class << self; self; end # This handles naming collisions with Sinatra/Vlad/Capistrano. Since these use a set()
eigen_class.send(:define_method, name) { self[name] } # helper that defines methods in Object, ANY method_missing ANYWHERE picks up the Vlad/Sinatra
# settings! So settings.deploy_to title actually calls Object.deploy_to (from set :deploy_to, "host"),
# rather than the app_yml['deploy_to'] hash. Jeezus.
def create_accessors!
self.each do |key,val|
# Use instance_eval/class_eval because they're actually more efficient than define_method{}
# http://stackoverflow.com/questions/185947/ruby-definemethod-vs-def
# http://bmorearty.wordpress.com/2009/01/09/fun-with-rubys-instance_eval-and-class_eval/
self.class.class_eval <<-EndEval
def #{key}
return @#{key} if @#{key} # cache (performance)
value = fetch('#{key}')
@#{key} = value.is_a?(Hash) ? self.class.new(value, "'#{key}' section in #{@section}") : value
end
EndEval
end end
end end


include EigenMethodDefiner
end end
3 changes: 3 additions & 0 deletions spec/settings.yml
Expand Up @@ -14,3 +14,6 @@ language:
paradigm: functional paradigm: functional
smalltalk: smalltalk:
paradigm: object oriented paradigm: object oriented

collides:
does: not
4 changes: 4 additions & 0 deletions spec/settings3.rb
@@ -0,0 +1,4 @@
class Settings3 < Settingslogic
source "#{File.dirname(__FILE__)}/settings.yml"
load! # test of load
end
65 changes: 59 additions & 6 deletions spec/settingslogic_spec.rb
@@ -1,10 +1,6 @@
require File.expand_path(File.dirname(__FILE__) + "/spec_helper") require File.expand_path(File.dirname(__FILE__) + "/spec_helper")


describe "Settingslogic" do describe "Settingslogic" do
it "should be a hash" do
Settings.send(:instance).should be_is_a(Hash)
end

it "should access settings" do it "should access settings" do
Settings.setting2.should == 5 Settings.setting2.should == 5
end end
Expand All @@ -16,21 +12,78 @@
it "should access deep nested settings" do it "should access deep nested settings" do
Settings.setting1.deep.another.should == "my value" Settings.setting1.deep.another.should == "my value"
end end

it "should access extra deep nested settings" do it "should access extra deep nested settings" do
Settings.setting1.deep.child.value.should == 2 Settings.setting1.deep.child.value.should == 2
end end

it "should enable erb" do it "should enable erb" do
Settings.setting3.should == 25 Settings.setting3.should == 25
end end


it "should namespace settings" do it "should namespace settings" do
Settings2.setting1_child.should == "saweet" Settings2.setting1_child.should == "saweet"
Settings2.deep.another.should == "my value"
end

it "should return the namespace" do
Settings.namespace.should be_nil
Settings2.namespace.should == 'setting1'
end end


it "should distinguish nested keys" do it "should distinguish nested keys" do
Settings.language.haskell.paradigm.should == 'functional' Settings.language.haskell.paradigm.should == 'functional'
Settings.language.smalltalk.paradigm.should == 'object oriented' Settings.language.smalltalk.paradigm.should == 'object oriented'
end end

it "should not collide with global methods" do
Settings3.collides.does.should == 'not'
end

it "should raise a helpful error message" do
e = nil
begin
Settings.missing
rescue => e
e.should be_kind_of Settingslogic::MissingSetting
end
e.should_not be_nil
e.message.should =~ /Missing setting 'missing' in/

e = nil
begin
Settings.language.missing
rescue => e
e.should be_kind_of Settingslogic::MissingSetting
end
e.should_not be_nil
e.message.should =~ /Missing setting 'missing' in 'language' section/
end

it "should handle optional / dynamic settings" do
e = nil
begin
Settings.language.erlang
rescue => e
e.should be_kind_of Settingslogic::MissingSetting
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

Settings.language['erlang'] = {'paradigm' => 'functional'}
Settings.language.erlang.paradigm.should == 'functional'

Settings.reload!
Settings.language['erlang'].should be_nil
end

# Put this test last or else call to .instance will load @instance,
# masking bugs.
it "should be a hash" do
Settings.send(:instance).should be_is_a(Hash)
end
end end
8 changes: 7 additions & 1 deletion spec/spec_helper.rb
@@ -1,12 +1,18 @@
require 'spec' require 'spec'
require 'rubygems' require 'rubygems'
require 'ruby-debug' require 'ruby-debug' if RUBY_VERSION < '1.9' # ruby-debug does not work on 1.9.1 yet


$LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require 'settingslogic' require 'settingslogic'
require 'settings' require 'settings'
require 'settings2' require 'settings2'
require 'settings3'

# Needed to test Settings3
def collides
'collision'
end


Spec::Runner.configure do |config| Spec::Runner.configure do |config|
end end

0 comments on commit c8da048

Please sign in to comment.