require 'lazydoc/attributes'
require 'configurable/delegate_hash'
require 'configurable/validation'
require 'configurable/indifferent_access'
autoload(:ConfigParser, 'config_parser')
module Configurable
# ClassMethods extends classes that include Configurable and
# provides methods for declaring configurations.
module ClassMethods
include Lazydoc::Attributes
# A hash holding the class configurations.
attr_reader :configurations
def self.extended(base) # :nodoc:
caller.each_with_index do |line, index|
case line
when /\/configurable.rb/ then next
when Lazydoc::CALLER_REGEXP
base.instance_variable_set(:@source_file, File.expand_path($1))
break
end
end
configurations = {}.extend IndifferentAccess
base.instance_variable_set(:@configurations, configurations)
end
def inherited(child) # :nodoc:
unless child.instance_variable_defined?(:@source_file)
caller.first =~ Lazydoc::CALLER_REGEXP
child.instance_variable_set(:@source_file, File.expand_path($1))
end
configurations = {}
configurations.extend IndifferentAccess if @configurations.kind_of?(IndifferentAccess)
@configurations.each_pair {|key, config| configurations[key] = config.dup }
child.instance_variable_set(:@configurations, configurations)
super
end
def parser
ConfigParser.new do |parser|
configurations.to_a.sort_by do |(key, config)|
config.attributes[:order] || 0
end.each do |(key, config)|
parser.define(key, config.default, config.attributes)
end
end
end
# Loads the contents of path as YAML. Returns an empty hash if the path
# is empty, does not exist, or is not a file.
def load_config(path)
# the last check prevents YAML from auto-loading itself for empty files
return {} if path == nil || !File.file?(path) || File.size(path) == 0
YAML.load_file(path) || {}
end
protected
def use_indifferent_access(value=true)
current = @configurations
@configurations = value ? HashWithIndifferentAccess.new : {}
current.each_pair do |key, value|
@configurations[key] = value
end
end
# Declares a class configuration and generates the associated accessors.
# If a block is given, the key= method will set @key
# to the return of the block, which executes in class-context.
#
# class SampleClass
# include Configurable
#
# config :str, 'value'
# config(:upcase, 'value') {|input| input.upcase }
# end
#
# # An equivalent class to illustrate class-context
# class EquivalentClass
# attr_accessor :str
# attr_reader :upcase
#
# UPCASE_BLOCK = lambda {|input| input.upcase }
#
# def upcase=(input)
# @upcase = UPCASE_BLOCK.call(input)
# end
# end
#
def config(key, value=nil, options={}, &block)
# register with Lazydoc
options[:desc] ||= Lazydoc.register_caller
if block_given?
options = default_options(block).merge!(options)
instance_variable = "@#{key}".to_sym
config_attr(key, value, options) do |input|
instance_variable_set(instance_variable, yield(input))
end
else
config_attr(key, value, options)
end
end
# Declares a class configuration and generates the associated accessors.
# If a block is given, the key= method will perform the block
# with instance-context.
#
# class SampleClass
# include Configurable
#
# def initialize
# initialize_config
# end
#
# config_attr :str, 'value'
# config_attr(:upcase, 'value') {|input| @upcase = input.upcase }
# end
#
# # An equivalent class to illustrate instance-context
# class EquivalentClass
# attr_accessor :str
# attr_reader :upcase
#
# def upcase=(input)
# @upcase = input.upcase
# end
# end
#
def config_attr(key, value=nil, options={}, &block)
options = default_options(block).merge!(options)
# define the default public reader method
reader = options.delete(:reader)
case reader
when true
reader = key
attr_reader(key)
public(key)
when false
reader = key
end
# define the default public writer method
writer = options.delete(:writer)
if block_given? && writer != true
raise ArgumentError, "a block may not be specified without writer == true"
end
case writer
when true
writer = "#{key}="
block_given? ? define_method(writer, &block) : attr_writer(key)
public writer
when false
writer = "#{key}="
end
# register with Lazydoc
options[:desc] ||= Lazydoc.register_caller
configurations[key] = Delegate.new(reader, writer, value, options)
end
# Adds a configuration to self accessing the configurations for the
# configurable class. Unlike config_attr and config, nest does not
# create accessors; the configurations must be accessed through
# the instance config method.
#
# class A
# include Configurable
# config :key, 'value'
#
# def initialize(overrides={})
# initialize_config(overrides)
# end
# end
#
# class B
# include Configurable
# nest :a, A
#
# def initialize(overrides={})
# initialize_config(overrides)
# end
# end
#
# b = B.new
# b.config[:a] # => {:key => 'value'}
#
# Nest may be provided a block which receives the first value for
# the nested config and is expected to initialize an instance of
# configurable_class. In this case a reader for the instance is
# created and access becomes quite natural.
#
# class C
# include Configurable
# nest(:a, A) {|overrides| A.new(overrides) }
#
# def initialize(overrides={})
# initialize_config(overrides)
# end
# end
#
# c = C.new
# c.a.key # => "value"
#
# c.a.key = "one"
# c.config[:a].to_hash # => {:key => 'one'}
#
# c.config[:a][:key] = 'two'
# c.a.key # => "two"
#
# c.config[:a] = {:key => 'three'}
# c.a.key # => "three"
#
# Nesting with an initialization block creates private methods
# that config[:a] uses to read and write the instance configurations;
# these methods are "#{key}_config" and "#{key}_config=" by default,
# but they may be renamed using the :reader and :writer options.
#
# Nest checks for recursive nesting and raises an error if
# a recursive nest is detected.
#
def nest(key, configurable_class, options={})
unless configurable_class.kind_of?(Configurable::ClassMethods)
raise ArgumentError, "not a Configurable class: #{configurable_class}"
end
reader = options.delete(:reader)
writer = options.delete(:writer)
if block_given?
# define instance accessor methods
instance_var = "@#{key}".to_sym
reader = "#{key}_config" unless reader
writer = "#{key}_config=" unless writer
# the public accessor
attr_reader key
public(key)
# the reader returns the config for the instance
define_method(reader) do
instance_variable_get(instance_var).config
end
# the writer initializes the instance if necessary,
# or reconfigures the instance if it already exists
define_method(writer) do |value|
if instance_variable_defined?(instance_var)
instance_variable_get(instance_var).reconfigure(value)
else
instance_variable_set(instance_var, yield(value))
end
end
private(reader, writer)
else
reader = writer = nil
end
# register with Lazydoc
options[:desc] ||= Lazydoc.register_caller
value = DelegateHash.new(configurable_class.configurations).update
configurations[key] = Delegate.new(reader, writer, value, options)
check_infinite_nest(configurable_class.configurations)
end
# Alias for Validation
def c
Validation
end
private
def default_options(block)
Validation::ATTRIBUTES[block].merge(
:reader => true,
:writer => true,
:order => configurations.length)
end
# helper to recursively check a set of
# configurations for an infinite nest
def check_infinite_nest(configurations) # :nodoc:
raise "infinite nest detected" if configurations == self.configurations
configurations.each_pair do |key, config|
config_hash = config.default(false)
if config_hash.kind_of?(DelegateHash)
check_infinite_nest(config_hash.delegates)
end
end
end
end
end