Permalink
Browse files

def keys as methods rather than using method_missing, added load!/rel…

…oad!, more docs/specs
  • Loading branch information...
Nate Wiger authored and binarylogic committed Jan 29, 2010
1 parent 9d11fce commit c8da048da83fcca584c6acf41faf74a1a48ded83
Showing with 198 additions and 49 deletions.
  1. +66 −18 README.rdoc
  2. +59 −24 lib/settingslogic.rb
  3. +3 −0 spec/settings.yml
  4. +4 −0 spec/settings3.rb
  5. +59 −6 spec/settingslogic_spec.rb
  6. +7 −1 spec/spec_helper.rb
View
@@ -1,6 +1,7 @@
= 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?
@@ -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>Issues:</b> http://github.com/binarylogic/settingslogic/issues
== Install and use
== Installation
Install from rubyforge:
Install from rubyforge/gemcutter:
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
source "#{Rails.root}/config/application.yml"
namespace Rails.env
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.
== 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
defaults: &defaults
cool:
saweet: nested settings
neat_setting: 24
awesome_setting: <%= "Did you know 5 + 5 = " + (5 + 5) + "?" %>
awesome_setting: <%= "Did you know 5 + 5 = #{5 + 5}?" %>
development:
<<: *defaults
@@ -58,11 +63,11 @@ Notice above we specified an absolute path to our settings file called "applicat
production:
<<: *defaults
== Access your settings
=== 3. Access your settings
>> Rails.env
=> "development"
>> Rails.env.development?
=> true
>> Settings.cool
=> "#<Settingslogic::Settings ... >"
@@ -75,5 +80,48 @@ Notice above we specified an absolute path to our settings file called "applicat
>> Settings.awesome_setting
=> "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].
View
@@ -3,6 +3,8 @@
# A simple settings solution using a YAML file. See README for more information.
class Settingslogic < Hash
class MissingSetting < StandardError; end
class << self
def name # :nodoc:
instance.key?("name") ? instance.name : super
@@ -15,7 +17,7 @@ def source(value = nil)
@source = value
end
end
def namespace(value = nil)
if value.nil?
@namespace
@@ -24,6 +26,26 @@ def namespace(value = nil)
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
def instance
@instance ||= new
@@ -33,46 +55,59 @@ def method_missing(name, *args, &block)
instance.send(name, *args, &block)
end
end
# 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.yaml") # will look for application.yaml
# Settings.new("/var/configs/application.yml") # will look for /var/configs/application.yml
# 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.
def initialize(hash_or_file = self.class.source)
def initialize(hash_or_file = self.class.source, section = nil)
case hash_or_file
when Hash
self.update hash_or_file
self.replace hash_or_file
else
hash = YAML.load(ERB.new(File.read(hash_or_file)).result).to_hash
hash = hash[self.class.namespace] if self.class.namespace
self.update hash
self.replace hash
end
@section = section || hash_or_file # so end of error says "in application.yml"
create_accessors!
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)
eigen_class = class << self; self; end
eigen_class.send(:define_method, name) { self[name] }
private
# This handles naming collisions with Sinatra/Vlad/Capistrano. Since these use a set()
# 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
include EigenMethodDefiner
end
View
@@ -14,3 +14,6 @@ language:
paradigm: functional
smalltalk:
paradigm: object oriented
collides:
does: not
View
@@ -0,0 +1,4 @@
class Settings3 < Settingslogic
source "#{File.dirname(__FILE__)}/settings.yml"
load! # test of load
end
View
@@ -1,10 +1,6 @@
require File.expand_path(File.dirname(__FILE__) + "/spec_helper")
describe "Settingslogic" do
it "should be a hash" do
Settings.send(:instance).should be_is_a(Hash)
end
it "should access settings" do
Settings.setting2.should == 5
end
@@ -16,21 +12,78 @@
it "should access deep nested settings" do
Settings.setting1.deep.another.should == "my value"
end
it "should access extra deep nested settings" do
Settings.setting1.deep.child.value.should == 2
end
it "should enable erb" do
Settings.setting3.should == 25
end
it "should namespace settings" do
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
it "should distinguish nested keys" do
Settings.language.haskell.paradigm.should == 'functional'
Settings.language.smalltalk.paradigm.should == 'object oriented'
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
View
@@ -1,12 +1,18 @@
require 'spec'
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.join(File.dirname(__FILE__), '..', 'lib'))
require 'settingslogic'
require 'settings'
require 'settings2'
require 'settings3'
# Needed to test Settings3
def collides
'collision'
end
Spec::Runner.configure do |config|
end

0 comments on commit c8da048

Please sign in to comment.