Permalink
Browse files

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

…oad!, more docs/specs
  • Loading branch information...
1 parent 9d11fce commit c8da048da83fcca584c6acf41faf74a1a48ded83 Nate Wiger committed with Jan 29, 2010
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.