extract configruation system to a gem

This commit is contained in:
Jonne Haß 2012-11-30 13:30:51 +01:00
parent 656e52360f
commit 669dd87b11
20 changed files with 17 additions and 624 deletions

View file

@ -5,6 +5,7 @@
* Removed unused stuff [#3714](https://github.com/diaspora/diaspora/pull/3714), [#3754](https://github.com/diaspora/diaspora/pull/3754)
* Last post link isn't displayed anymore if there are no visible posts [#3750](https://github.com/diaspora/diaspora/issues/3750)
* Ported tag followings to backbone [#3713](https://github.com/diaspora/diaspora/pull/3713)
* Extracted configuration system to a gem.
## Bug Fixes

View file

@ -8,6 +8,9 @@ gem 'unicorn', '4.4.0', :require => false
gem 'rails_autolink', '1.0.9'
# configuration
gem 'configurate', '0.0.1'
# cross-origin resource sharing
gem 'rack-cors', '0.2.7', :require => 'rack/cors'

View file

@ -90,6 +90,7 @@ GEM
execjs
coffee-script-source (1.4.0)
columnize (0.3.6)
configurate (0.0.1)
crack (0.3.1)
cucumber (1.2.1)
builder (>= 2.1.2)
@ -430,6 +431,7 @@ DEPENDENCIES
capybara (= 1.1.3)
carrierwave (= 0.7.1)
client_side_validations (= 3.2.1)
configurate (= 0.0.1)
cucumber-rails (= 1.3.0)
database_cleaner (= 0.9.1)
debugger (= 1.2.1)

View file

@ -1,5 +1,4 @@
require Rails.root.join('lib', 'configuration')
require Rails.root.join('lib', 'configuration', 'methods')
require Rails.root.join('lib', 'configuration_methods')
config_dir = Rails.root.join("config")
@ -9,9 +8,9 @@ if File.exists?(config_dir.join("application.yml"))
end
AppConfig ||= Configuration::Settings.create do
add_provider Configuration::Provider::Dynamic
add_provider Configuration::Provider::Env
AppConfig ||= Configurate::Settings.create do
add_provider Configurate::Provider::Dynamic
add_provider Configurate::Provider::Env
unless heroku? || Rails.env == "test" || File.exists?(config_dir.join("diaspora.yml"))
$stderr.puts "FATAL: Configuration not found. Copy over diaspora.yml.example"
@ -19,16 +18,16 @@ AppConfig ||= Configuration::Settings.create do
Process.exit(1)
end
add_provider Configuration::Provider::YAML,
add_provider Configurate::Provider::YAML,
config_dir.join("diaspora.yml"),
namespace: Rails.env, required: false
add_provider Configuration::Provider::YAML,
add_provider Configurate::Provider::YAML,
config_dir.join("diaspora.yml"),
namespace: "configuration", required: false
add_provider Configuration::Provider::YAML,
add_provider Configurate::Provider::YAML,
config_dir.join("defaults.yml"),
namespace: Rails.env
add_provider Configuration::Provider::YAML,
add_provider Configurate::Provider::YAML,
config_dir.join("defaults.yml"),
namespace: "defaults"

View file

@ -1,69 +0,0 @@
require Rails.root.join('lib', 'configuration', 'lookup_chain')
require Rails.root.join('lib', 'configuration', 'provider')
require Rails.root.join('lib', 'configuration', 'proxy')
# A flexible and extendable configuration system.
# The calling logic is isolated from the lookup logic
# through configuration providers, which only requirement
# is to define the +#lookup+ method and show a certain behavior on that.
# The providers are asked in the order they were added until one provides
# a response. This allows to even add multiple providers of the same type,
# you never easier defined your default configuration parameters.
# There are no class methods used, you can have an unlimited amount of
# independent configuration sources at the same time.
#
# See {Settings} for a quick start.
module Configuration
# This is your main entry point. Instead of lengthy explanations
# let an example demonstrate its usage:
#
# require Rails.root.join('lib', 'configuration')
#
# AppSettings = Configuration::Settings.create do
# add_provider Configuration::Provider::Env
# add_provider Configuration::Provider::YAML, '/etc/app_settings.yml',
# namespace: Rails.env, required: false
# add_provider Configuration::Provider::YAML, 'config/default_settings.yml'
#
# extend YourConfigurationMethods
# end
#
# AppSettings.setup_something if AppSettings.something.enable?
#
# Please also read the note at {Proxy}!
class Settings
attr_reader :lookup_chain
undef_method :method # Remove possible conflicts with common setting names
# @!method lookup(setting)
# (see LookupChain#lookup)
# @!method add_provider(provider, *args)
# (see LookupChain#add_provider)
# @!method [](setting)
# (see LookupChain#[])
def method_missing(method, *args, &block)
return @lookup_chain.send(method, *args, &block) if [:lookup, :add_provider, :[]].include?(method)
Proxy.new(@lookup_chain).send(method, *args, &block)
end
def initialize
@lookup_chain = LookupChain.new
$stderr.puts "Warning you called Configuration::Settings.new with a block, you really meant to call #create" if block_given?
end
# Create a new configuration object
# @yield the given block will be evaluated in the context of the new object
def self.create(&block)
config = self.new
config.instance_eval(&block) if block_given?
config
end
end
class SettingNotFoundError < RuntimeError; end
end

View file

@ -1,65 +0,0 @@
module Configuration
# This object builds a chain of configuration providers to try to find
# a setting.
class LookupChain
def initialize
@provider = []
end
# Add a provider to the chain. Providers are tried in the order
# they are added, so the order is important.
#
# @param provider [#lookup]
# @param *args the arguments passed to the providers constructor
# @raise [ArgumentError] if an invalid provider is given
# @return [void]
def add_provider(provider, *args)
unless provider.instance_method_names.include?("lookup")
raise ArgumentError, "the given provider does not respond to lookup"
end
@provider << provider.new(*args)
end
# Tries all providers in the order they were added to provide a response
# for setting.
#
# @param setting [#to_s] settings should be underscore_case,
# nested settings should be separated by a dot
# @param *args further args passed to the provider
# @return [Array,String,Boolean,nil] whatever the provider provides
# is casted to a {String}, except for some special values
def lookup(setting, *args)
setting = setting.to_s
@provider.each do |provider|
begin
return special_value_or_string(provider.lookup(setting, *args))
rescue SettingNotFoundError; end
end
nil
end
alias_method :[], :lookup
private
def special_value_or_string(value)
if [TrueClass, FalseClass, NilClass, Array, Hash].include?(value.class)
return value
elsif value.is_a?(String)
return case value.strip
when "true" then true
when "false" then false
when "", "nil" then nil
else value
end
elsif value.respond_to?(:to_s)
return value.to_s
else
return value
end
end
end
end

View file

@ -1,19 +0,0 @@
module Configuration::Provider
# This provides a basic {#lookup} method for other providers to build
# upon. Childs are expected to define +lookup_path(path, *args)+ where
# +path+ will be passed an array of settings generated by splitting the
# called setting at the dots. The method should return nil if the setting
# wasn't found and {#lookup} will raise an {SettingNotFoundError} in that
# case.
class Base
def lookup(setting, *args)
result = lookup_path(setting.split("."), *args)
return result unless result.nil?
raise Configuration::SettingNotFoundError, "The setting #{setting} was not found"
end
end
end
require Rails.root.join("lib", "configuration", "provider", "yaml")
require Rails.root.join("lib", "configuration", "provider", "env")
require Rails.root.join("lib", "configuration", "provider", "dynamic")

View file

@ -1,24 +0,0 @@
module Configuration::Provider
# This provider knows nothing upon initialization, however if you access
# a setting ending with +=+ and give one argument to that call it remembers
# that setting, stripping the +=+ and will return it on the next call
# without +=+.
class Dynamic < Base
def initialize
@settings = {}
end
def lookup_path(settings_path, *args)
key = settings_path.join(".")
if key.end_with?("=") && args.length > 0
key = key.chomp("=")
value = args.first
value = value.get if value.respond_to?(:_proxy?) && value._proxy?
@settings[key] = value
end
@settings[key]
end
end
end

View file

@ -1,14 +0,0 @@
module Configuration::Provider
# This provider looks for settings in the environment.
# For the setting +foo.bar_baz+ this provider will look for an
# environment variable +FOO_BAR_BAZ+, replacing all dots in the setting
# and upcasing the result. If an value contains +,+ it's split at them
# and returned as array.
class Env < Base
def lookup_path(settings_path, *args)
value = ENV[settings_path.join("_").upcase]
value = value.split(",") if value && value.include?(",")
value
end
end
end

View file

@ -1,52 +0,0 @@
require 'yaml'
module Configuration::Provider
# This provider tries to open a YAML file and does in nested lookups
# in it.
class YAML < Base
# @param file [String] the path to the file
# @param opts [Hash]
# @option opts [String] :namespace optionally set this as the root
# @option opts [Boolean] :required wheter or not to raise an error if
# the file or the namespace, if given, is not found. Defaults to +true+.
# @raise [ArgumentError] if the namespace isn't found in the file
# @raise [Errno:ENOENT] if the file isn't found
def initialize(file, opts = {})
@settings = {}
required = opts.has_key?(:required) ? opts.delete(:required) : true
@settings = ::YAML.load_file(file)
namespace = opts.delete(:namespace)
unless namespace.nil?
actual_settings = lookup_in_hash(namespace.split("."), @settings)
unless actual_settings.nil?
@settings = actual_settings
else
raise ArgumentError, "Namespace #{namespace} not found in #{file}" if required
end
end
rescue Errno::ENOENT => e
$stderr.puts "WARNING: configuration file #{file} not found, ensure it's present"
raise e if required
end
def lookup_path(settings_path, *args)
lookup_in_hash(settings_path, @settings)
end
private
def lookup_in_hash(setting_path, hash)
setting = setting_path.shift
if hash.has_key?(setting)
if setting_path.length > 0 && hash[setting].is_a?(Hash)
return lookup_in_hash(setting_path, hash[setting]) if setting.length > 1
else
return hash[setting]
end
end
end
end
end

View file

@ -1,76 +0,0 @@
module Configuration
# Proxy object to support nested settings
# Cavehat: Since this is always true, adding a ? at the end
# returns the value, if found, instead of the proxy object.
# So instead of +if settings.foo.bar+ use +if settings.foo.bar?+
# to check for boolean values, +if settings.foo.bar.nil?+ to
# check for nil values, +if settings.foo.bar.present?+ to check for
# empty values if you're in Rails and call {#get} to actually return the value,
# commonly when doing +settings.foo.bar.get || 'default'+. If a setting
# ends with +=+ is too called directly, just like with +?+.
class Proxy < BasicObject
COMMON_KEY_NAMES = [:key, :method]
# @param lookup_chain [#lookup]
def initialize(lookup_chain)
@lookup_chain = lookup_chain
@setting = ""
end
def !
!self.get
end
def !=(other)
self.get != other
end
def ==(other)
self.get == other
end
def _proxy?
true
end
def respond_to?(method, include_private=false)
method == :_proxy? || self.get.respond_to?(method, include_private)
end
def send(*args, &block)
self.__send__(*args, &block)
end
def method_missing(setting, *args, &block)
unless COMMON_KEY_NAMES.include? setting
target = self.get
if !(target.respond_to?(:_proxy?) && target._proxy?) && target.respond_to?(setting)
return target.send(setting, *args, &block)
end
end
setting = setting.to_s
self.append_setting(setting)
return self.get(*args) if setting.end_with?("?") || setting.end_with?("=")
self
end
# Get the setting at the current path, if found.
# (see LookupChain#lookup)
def get(*args)
setting = @setting[1..-1]
return unless setting
val = @lookup_chain.lookup(setting.chomp("?"), *args)
val
end
protected
def append_setting(setting)
@setting << "."
@setting << setting
end
end
end

View file

@ -1,67 +0,0 @@
require 'spec_helper'
class InvalidConfigurationProvider; end
class ValidConfigurationProvider
def lookup(setting, *args); end
end
describe Configuration::LookupChain do
subject { described_class.new }
describe "#add_provider" do
it "adds a valid provider" do
expect {
subject.add_provider ValidConfigurationProvider
}.to change { subject.instance_variable_get(:@provider).size }.by 1
end
it "doesn't add an invalid provider" do
expect {
subject.add_provider InvalidConfigurationProvider
}.to raise_error ArgumentError
end
it "passes extra args to the provider" do
ValidConfigurationProvider.should_receive(:new).with(:extra)
subject.add_provider ValidConfigurationProvider, :extra
end
end
describe "#lookup" do
before(:all) do
subject.add_provider ValidConfigurationProvider
subject.add_provider ValidConfigurationProvider
@provider = subject.instance_variable_get(:@provider)
end
it "it tries all providers" do
setting = "some.setting"
@provider.each do |provider|
provider.should_receive(:lookup).with(setting).and_raise(Configuration::SettingNotFoundError)
end
subject.lookup(setting)
end
it "stops if a value is found" do
@provider[0].should_receive(:lookup).and_return("something")
@provider[1].should_not_receive(:lookup)
subject.lookup("bla")
end
it "converts numbers to strings" do
@provider[0].stub(:lookup).and_return(5)
subject.lookup("foo").should == "5"
end
it "does not convert false to a string" do
@provider[0].stub(:lookup).and_return(false)
subject.lookup("enable").should be_false
end
it "returns nil if no value is found" do
@provider.each { |p| p.stub(:lookup).and_raise(Configuration::SettingNotFoundError) }
subject.lookup("not.me").should be_nil
end
end
end

View file

@ -1,23 +0,0 @@
require 'spec_helper'
describe Configuration::Provider::Dynamic do
subject { described_class.new }
describe "#lookup_path" do
it "returns nil if the setting was never set" do
subject.lookup_path(["not_me"]).should be_nil
end
it "remembers the setting if it ends with =" do
subject.lookup_path(["find_me", "later="], "there")
subject.lookup_path(["find_me", "later"]).should == "there"
end
it "calls .get on the argument if a proxy object is given" do
proxy = mock
proxy.stub(:respond_to?).and_return(true)
proxy.stub(:_proxy?).and_return(true)
proxy.should_receive(:get)
subject.lookup_path(["bla="], proxy)
end
end
end

View file

@ -1,32 +0,0 @@
require 'spec_helper'
describe Configuration::Provider::Env do
subject { described_class.new }
let(:existing_path) { ['existing', 'setting']}
let(:not_existing_path) { ['not', 'existing', 'path']}
let(:array_path) { ['array'] }
before(:all) do
ENV['EXISTING_SETTING'] = "there"
ENV['ARRAY'] = "foo,bar,baz"
end
after(:all) do
ENV['EXISTING_SETTING'] = nil
ENV['ARRAY'] = nil
end
describe '#lookup_path' do
it "joins and upcases the path" do
ENV.should_receive(:[]).with("EXISTING_SETTING")
subject.lookup_path(existing_path)
end
it "returns nil if the setting isn't available" do
subject.lookup_path(not_existing_path).should be_nil
end
it "makes an array out of comma separated values" do
subject.lookup_path(array_path).should == ["foo", "bar", "baz"]
end
end
end

View file

@ -1,72 +0,0 @@
require 'spec_helper'
describe Configuration::Provider::YAML do
let(:settings) { {"toplevel" => "bar",
"some" => {
"nested" => { "some" => "lala", "setting" => "foo"}
}
} }
describe "#initialize" do
it "loads the file" do
file = "foobar.yml"
::YAML.should_receive(:load_file).with(file).and_return({})
described_class.new file
end
it "raises if the file is not found" do
::YAML.stub(:load_file).and_raise(Errno::ENOENT)
expect {
described_class.new "foo"
}.to raise_error Errno::ENOENT
end
context "with a namespace" do
it "looks in the file for that namespace" do
namespace = "some"
::YAML.stub(:load_file).and_return(settings)
provider = described_class.new 'bla', namespace: namespace
provider.instance_variable_get(:@settings).should == settings[namespace]
end
it "raises if the namespace isn't found" do
::YAML.stub(:load_file).and_return({})
expect {
described_class.new 'bla', namespace: "foo"
}.to raise_error ArgumentError
end
end
context "with required set to false" do
it "doesn't raise if a file isn't found" do
::YAML.stub(:load_file).and_raise(Errno::ENOENT)
expect {
described_class.new "not_me", required: false
}.not_to raise_error Errno::ENOENT
end
it "doesn't raise if a namespace isn't found" do
::YAML.stub(:load_file).and_return({})
expect {
described_class.new 'bla', namespace: "foo", required: false
}.not_to raise_error ArgumentError
end
end
end
describe "#lookup_path" do
before do
::YAML.stub(:load_file).and_return(settings)
@provider = described_class.new 'dummy'
end
it "looks up the whole nesting" do
@provider.lookup_path(["some", "nested", "some"]).should == settings["some"]["nested"]["some"]
end
it "returns nil if no setting is found" do
@provider.lookup_path(["not_me"]).should be_nil
end
end
end

View file

@ -1,18 +0,0 @@
require 'spec_helper'
describe Configuration::Provider::Base do
subject { described_class.new }
describe "#lookup" do
it "calls #lookup_path with the setting as array" do
subject.should_receive(:lookup_path).with(["foo", "bar"]).and_return("something")
subject.lookup("foo.bar").should == "something"
end
it "raises SettingNotFoundError if the #lookup_path returns nil" do
subject.should_receive(:lookup_path).and_return(nil)
expect {
subject.lookup("bla")
}.to raise_error Configuration::SettingNotFoundError
end
end
end

View file

@ -1,56 +0,0 @@
require 'spec_helper'
describe Configuration::Proxy do
let(:lookup_chain) { mock }
before do
lookup_chain.stub(:lookup).and_return("something")
end
describe "#method_missing" do
it "calls #get if the method ends with a ?" do
lookup_chain.should_receive(:lookup).with("enable").and_return(false)
described_class.new(lookup_chain).method_missing(:enable?)
end
it "calls #get if the method ends with a =" do
lookup_chain.should_receive(:lookup).with("url=").and_return(false)
described_class.new(lookup_chain).method_missing(:url=)
end
end
describe "#get" do
[:to_str, :to_s, :to_xml, :respond_to?, :present?, :!=,
:each, :try, :size, :length, :count, :==, :=~, :gsub, :blank?, :chop,
:start_with?, :end_with?].each do |method|
it "is called for accessing #{method} on the proxy" do
target = mock
lookup_chain.should_receive(:lookup).and_return(target)
target.should_receive(method).and_return("something")
described_class.new(lookup_chain).something.__send__(method, mock)
end
end
described_class::COMMON_KEY_NAMES.each do |method|
it "is not called for accessing #{method} on the proxy" do
target = mock
lookup_chain.should_not_receive(:lookup).and_return(target)
target.should_not_receive(method).and_return("something")
described_class.new(lookup_chain).something.__send__(method, mock)
end
end
it "strips leading dots" do
lookup_chain.should_receive(:lookup).with("foo.bar").and_return("something")
described_class.new(lookup_chain).foo.bar.get
end
it "returns nil if no setting is given" do
described_class.new(lookup_chain).get.should be_nil
end
it "strips ? at the end" do
lookup_chain.should_receive(:lookup).with("foo.bar").and_return("something")
described_class.new(lookup_chain).foo.bar?
end
end
end

View file

@ -2,9 +2,9 @@ require 'spec_helper'
describe Configuration::Methods do
before(:all) do
@settings = Configuration::Settings.create do
add_provider Configuration::Provider::Dynamic
add_provider Configuration::Provider::Env
@settings = Configurate::Settings.create do
add_provider Configurate::Provider::Dynamic
add_provider Configurate::Provider::Env
extend Configuration::Methods
end
end

View file

@ -1,25 +0,0 @@
require 'spec_helper'
describe Configuration::Settings do
describe "#method_missing" do
subject { described_class.create }
it "delegates the call to a new proxy object" do
proxy = mock
Configuration::Proxy.should_receive(:new).and_return(proxy)
proxy.should_receive(:method_missing).with(:some_setting).and_return("foo")
subject.some_setting
end
end
[:lookup, :add_provider, :[]].each do |method|
describe "#{method}" do
subject { described_class.create }
it "delegates the call to #lookup_chain" do
subject.lookup_chain.should_receive(method)
subject.send(method)
end
end
end
end