require 'lazydoc'
require 'configurable/delegate_hash'
require 'configurable/indifferent_access'
require 'configurable/validation'
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 of (key, Delegate) pairs defining the class configurations.
attr_reader :configurations
def self.extended(base) # :nodoc:
unless base.instance_variable_defined?(:@source_file)
caller[2] =~ Lazydoc::CALLER_REGEXP
base.instance_variable_set(:@source_file, File.expand_path($1))
end
base.send(:initialize_configurations).extend(IndifferentAccess)
end
def inherited(child) # :nodoc:
unless child.instance_variable_defined?(:@source_file)
caller[0] =~ Lazydoc::CALLER_REGEXP
child.instance_variable_set(:@source_file, File.expand_path($1))
end
# deep duplicate configurations
unless child.instance_variable_defined?(:@configurations)
duplicate = child.instance_variable_set(:@configurations, configurations.dup)
duplicate.each_pair {|key, config| duplicate[key] = config.dup }
duplicate.extend(IndifferentAccess) if configurations.kind_of?(IndifferentAccess)
end
super
end
# Parses configurations from argv in a non-destructive manner by generating
# a ConfigParser using the configurations for self. Parsed configs are
# added to config (note that you must keep a separate reference to
# config as it is not returned by parse). The parser will is yielded to the
# block, if given, to register additonal options. Returns an array of the
# arguments that remain after parsing.
#
# See ConfigParser#parse for more information.
def parse(argv=ARGV, config={})
ConfigParser.new do |parser|
parser.add(configurations)
yield(parser) if block_given?
end.parse(argv, config)
end
# Same as parse, but removes parsed args from argv.
def parse!(argv=ARGV, config={})
argv.replace(parse(argv, config))
end
protected
# Sets configurations to symbolize keys for AGET ([]) and ASET([]=)
# operations, or not. By default, configurations will use
# indifferent access.
def use_indifferent_access(input=true)
if input
@configurations.extend(IndifferentAccess)
else
@configurations = configurations.dup
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, attributes={}, &block)
attributes = merge_attributes(block, attributes)
if block_given?
instance_variable = "@#{key}".to_sym
config_attr(key, value, attributes) do |input|
instance_variable_set(instance_variable, yield(input))
end
else
config_attr(key, value, attributes)
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
#
# === Attributes
#
# Several attributes may be specified to modify how a config is constructed.
# Attribute keys should be specified as symbols.
#
# Attribute:: Description
# reader:: The method used to read the configuration.
# (default: key)
# writer:: The method used to write the configuration
# (default: "#{key}=")
#
# Neither attribute may be set to nil, but they may be set to non-default
# values. In that case, config_attr will register the method names as
# provided, but it will not define the methods themselves. Specifying true
# uses and defines the default methods. Specifying false uses the default
# method name, but does not define the method itself.
#
# Any additional attributes are registered with the configuration.
def config_attr(key, value=nil, attributes={}, &block)
attributes = merge_attributes(block, attributes)
# define the reader
reader = define_attribute_method(:reader, attributes, key) do |attribute|
attr_reader(attribute)
public(attribute)
end
# define the writer
if block_given? && attributes[:writer] != true
raise ArgumentError, "a block may not be specified without writer == true"
end
writer = define_attribute_method(:writer, attributes, "#{key}=") do |attribute|
block_given? ? define_method(attribute, &block) : attr_writer(key)
public(attribute)
end
configurations[key] = Delegate.new(reader, writer, value, attributes)
end
# Adds nested configurations to self. Nest creates a new configurable
# class using the block, and provides accessors to an instance of the
# new class. Everything is set up so you can access configs through
# the instance or through config.
#
# class A
# include Configurable
#
# config :key, 'one'
# nest :nest do
# config :key, 'two'
# end
# end
#
# a = A.new
# a.key # => 'one'
# a.config[:key] # => 'one'
#
# a.nest.key # => 'two'
# a.config[:nest][:key] # => 'two'
#
# a.nest.key = 'TWO'
# a.config[:nest][:key] # => 'TWO'
#
# a.config[:nest][:key] = 2
# a.nest.key # => 2
#
# a.config.to_hash # => {:key => 'one', :nest => {:key => 2}}
# a.nest.config.to_hash # => {:key => 2}
# a.nest.class # => A::Nest
#
# An existing configurable class may be provided instead of using the block
# to define a new configurable class. Recursive nesting is supported.
#
# class B
# include Configurable
#
# config :key, 1, &c.integer
# nest :nest do
# config :key, 2, &c.integer
# nest :nest do
# config :key, 3, &c.integer
# end
# end
# end
#
# class C
# include Configurable
# nest :a, A
# nest :b, B
# end
#
# c = C.new
# c.b.key = 7
# c.b.nest.key = "8"
# c.config[:b][:nest][:nest][:key] = "9"
#
# c.config.to_hash
# # => {
# # :a => {
# # :key => 'one',
# # :nest => {:key => 'two'}
# # },
# # :b => {
# # :key => 7,
# # :nest => {
# # :key => 8,
# # :nest => {:key => 9}
# # }
# # }}
#
# === Attributes
#
# Nest provides a number of attributes that can modify how a nest is
# constructed. Attribute keys should be specified as symbols.
#
# Attribute:: Description
# const_name:: Determines the constant name of the configurable
# class within the nesting class. May be nil.
# (default: key.to_s.capitalize)
# instance_reader:: The method accessing the nested instance. (default: key)
# instance_writer:: The method to set the nested instance. (default: "#{key}=")
# instance_initializer:: The method that initializes the instance.
# (default: "initialize_#{key}")
# reader:: The method used to read the instance configuration.
# (default: "#{key}_config_reader")
# writer:: The method used to initialize or reconfigure the
# instance. (default: "#{key}_config_writer")
#
# Except for const_name, these attributes are used to define methods
# required for nesting to work properly. None of the method attributes may
# be set to nil, but they may be set to non-default values. In that case,
# nest will register the method names as provided, but it will not define
# the methods themselves. The user must define methods with the following
# functionality:
#
# Attribute:: Function
# instance_reader:: Returns the instance of the configurable class
# instance_writer:: Inputs and sets the instance of the configurable class
# instance_initializer:: Receives the initial config and return an instance of
# configurable class
# reader:: Returns instance.config
# writer:: Reconfigures instance using the input overrides,
# or uses instance_initializer and instance_writer to
# initialize and set the instance.
#
# Methods can be public or otherwise. Specifying true uses and defines the
# default methods. Specifying false uses the default method name, but does
# not define the method itself.
#
# Any additional attributes are registered with the configuration.
def nest(key, configurable_class=nil, attributes={}, &block)
attributes = merge_attributes(block, attributes)
attributes = {
:instance_reader => true,
:instance_writer => true,
:initializer => true
}.merge(attributes)
# define the nested configurable
if configurable_class
raise "a block is not allowed when a configurable class is specified" if block_given?
else
configurable_class = Class.new { include Configurable }
configurable_class.class_eval(&block) if block_given?
end
# set the new constant
const_name = if attributes.has_key?(:const_name)
attributes.delete(:const_name)
else
key.to_s.capitalize
end
const_set(const_name, configurable_class) if const_name
# define instance reader
instance_reader = define_attribute_method(:instance_reader, attributes, key) do |attribute|
attr_reader(key)
public(key)
end
# define instance writer
instance_writer = define_attribute_method(:instance_writer, attributes, "#{key}=") do |attribute|
attr_writer(key)
public(attribute)
end
# define initializer
initializer = define_attribute_method(:initializer, attributes, "initialize_#{key}") do |attribute|
define_method(attribute) {|config| configurable_class.new.reconfigure(config) }
private(attribute)
end
# define the reader
reader = define_attribute_method(:reader, attributes, "#{key}_config_reader") do |attribute|
define_method(attribute) { send(instance_reader).config }
private(attribute)
end
# define the writer
writer = define_attribute_method(:writer, attributes, "#{key}_config_writer") do |attribute|
define_method(attribute) do |value|
if instance = send(instance_reader)
instance.reconfigure(value)
else
send(instance_writer, send(initializer, value))
end
end
private(attribute)
end
# define the configuration
nested_config = DelegateHash.new(configurable_class.configurations)
configurations[key] = Delegate.new(reader, writer, nested_config, attributes)
check_infinite_nest(configurable_class.configurations)
end
# Alias for Validation
def c
Validation
end
private
# a helper to define methods that may be overridden in attributes.
# yields the default to the block if the default is supposed to
# be defined. returns the symbolized method name.
def define_attribute_method(name, attributes, default) # :nodoc:
attribute = attributes.delete(name)
case attribute
when true
# true means use the default and define the method
attribute = default
yield(attribute)
when false
# false means use the default, but let the user define the method
attribute = default
when nil
# nil is not allowed
raise "#{name.inspect} attribute cannot be nil"
end
# ... all other values specify what the method should be,
# and lets the user define the method.
attribute.to_sym
end
# a helper to initialize configurations for the first time,
# mainly implemented as a hook for OrderedHashPatch
def initialize_configurations # :nodoc:
@configurations ||= {}
end
# a helper method to merge the default attributes for the block with
# the input attributes. also registers a Trailer description.
def merge_attributes(block, attributes) # :nodoc:
defaults = DEFAULT_ATTRIBUTES[nil].dup
defaults.merge!(DEFAULT_ATTRIBUTES[block]) if block
defaults.merge!(attributes)
# register with Lazydoc
defaults[:desc] ||= Lazydoc.register_caller(Lazydoc::Trailer, 2)
defaults
end
# helper to recursively check for an infinite nest
def check_infinite_nest(delegates) # :nodoc:
raise "infinite nest detected" if delegates == self.configurations
delegates.each_pair do |key, delegate|
if delegate.is_nest?
check_infinite_nest(delegate.default(false).delegates)
end
end
end
end
end
module Configurable
# Beginning with ruby 1.9, Hash tracks the order of insertion and methods
# like each_pair return pairs in order. Configurable leverages this feature
# to keep configurations in order for the command line documentation produced
# by ConfigParser.
#
# Pre-1.9 ruby implementations require a patched Hash that tracks insertion
# order. This very thin subclass of hash does that for ASET insertions and
# each_pair. OrderedHashPatches are used as the configurations object in
# Configurable classes for pre-1.9 ruby implementations and for nothing else.
class OrderedHashPatch < Hash
def initialize
super
@insertion_order = []
end
# ASET insertion, tracking insertion order.
def []=(key, value)
@insertion_order << key unless @insertion_order.include?(key)
super
end
# Keys, sorted into insertion order
def keys
super.sort_by do |key|
@insertion_order.index(key) || length
end
end
# Yields each key-value pair to the block in insertion order.
def each_pair
keys.each do |key|
yield(key, fetch(key))
end
end
# Ensures the insertion order of duplicates is separate from parents.
def initialize_copy(orig)
super
@insertion_order = orig.instance_variable_get(:@insertion_order).dup
end
end
module ClassMethods
undef_method :initialize_configurations
# applies OrderedHashPatch
def initialize_configurations # :nodoc:
@configurations ||= OrderedHashPatch.new
end
end
end if RUBY_VERSION < '1.9'