require 'tap/support/class_configuration' require 'tap/support/validation' require 'tap/support/lazy_attributes' require 'tap/support/lazydoc/config' module Tap module Support autoload(:Templater, 'tap/support/templater') # ConfigurableClass encapsulates class methods used to declare class configurations. # When configurations are declared using the config method, ConfigurableClass # generates accessors in the class, much like attr_accessor. # # class ConfigurableClass # extend ConfigurableClass # config :one, 'one' # end # # ConfigurableClass.configurations.to_hash # => {:one => 'one'} # # c = ConfigurableClass.new # c.respond_to?('one') # => true # c.respond_to?('one=') # => true # # If a block is given, the block will be used to create the writer method # for the config. Used in this manner, config defines a config_key= method # wherein @config_key will be set to the return value of the block. # # class AnotherConfigurableClass # extend ConfigurableClass # config(:one, 'one') {|value| value.upcase } # end # # ac = AnotherConfigurableClass.new # ac.one = 'value' # ac.one # => 'VALUE' # # The block has class-context in this case. To have instance-context, use the # config_attr method which defines the writer method using the block directly. # # class YetAnotherConfigurableClass # extend ConfigurableClass # config_attr(:one, 'one') {|value| @one = value.reverse } # end # # ac = YetAnotherConfigurableClass.new # ac.one = 'value' # ac.one # => 'eulav' # module ConfigurableClass include Tap::Support::LazyAttributes # A ClassConfiguration holding the class configurations. attr_reader :configurations # Sets the source_file for base and initializes base.configurations. def self.extended(base) 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 base.instance_variable_set(:@configurations, ClassConfiguration.new(base)) end # When subclassed, the parent.configurations are duplicated and passed to # the child class where they can be extended/modified without affecting # the configurations of the parent class. def inherited(child) unless child.instance_variable_defined?(:@source_file) caller.first =~ Lazydoc::CALLER_REGEXP child.instance_variable_set(:@source_file, File.expand_path($1)) end child.instance_variable_set(:@configurations, ClassConfiguration.new(child, @configurations)) super end def lazydoc(resolve=true) Lazydoc.resolve_comments(configurations.code_comments) if resolve super 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 # 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. # Configurations are inherited, and can be overridden in subclasses. # # class SampleClass # include Tap::Support::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) if block_given? # add arg_type implied by block, if necessary options[:arg_type] = arg_type(block) if options[:arg_type] == nil options[:arg_name] = arg_name(block) if options[:arg_name] == nil instance_variable = "@#{key}".to_sym config_attr(key, value, options) do |input| instance_variable_set(instance_variable, block.call(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. Configurations are inherited, and can be overridden # in subclasses. # # class SampleClass # include Tap::Support::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 # # Instances of a Configurable class may set configurations through config. # The config object is an InstanceConfiguration which forwards read/write # operations to the configuration accessors. For example: # # s = SampleClass.new # s.config.class # => Tap::Support::InstanceConfiguration # s.str # => 'value' # s.config[:str] # => 'value' # # s.str = 'one' # s.config[:str] # => 'one' # # s.config[:str] = 'two' # s.str # => 'two' # # Alternative reader and writer methods may be specified as an option; # in this case config_attr assumes the methods are declared elsewhere # and will not define the associated accessors. # # class AlternativeClass # include Tap::Support::Configurable # # config_attr :sym, 'value', :reader => :get_sym, :writer => :set_sym # # def initialize # initialize_config # end # # def get_sym # @sym # end # # def set_sym(input) # @sym = input.to_sym # end # end # # alt = AlternativeClass.new # alt.respond_to?(:sym) # => false # alt.respond_to?(:sym=) # => false # # alt.config[:sym] = 'one' # alt.get_sym # => :one # # alt.set_sym('two') # alt.config[:sym] # => :two # # Idiosyncratically, true, false, and nil may also be provided as # reader/writer options. Specifying true is the same as using the # default. Specifying false or nil prevents config_attr from # defining accessors; false sets the configuration to use # the default reader/writer methods (ie key and key=, # which must be defined elsewhere) while nil prevents read/write # mapping of the config to a method. def config_attr(key, value=nil, options={}, &block) # add arg_type implied by block, if necessary options[:arg_type] = arg_type(block) if block_given? && options[:arg_type] == nil options[:arg_name] = arg_name(block) if block_given? && options[:arg_name] == nil # define the default public reader method if !options.has_key?(:reader) || options[:reader] == true attr_reader(key) public key end # define the public writer method case when options.has_key?(:writer) && options[:writer] != true raise(ArgumentError, "a block may not be specified with writer option") if block_given? when block_given? define_method("#{key}=", &block) public "#{key}=" else attr_writer(key) public "#{key}=" end # remove any true, false reader/writer declarations... # implicitly reverting the option to the default reader # and writer methods [:reader, :writer].each do |option| case options[option] when true, false then options.delete(option) end end # register with Lazydoc so that all extra documentation can be extracted caller.each do |line| case line when /in .config.$/ then next when Lazydoc::CALLER_REGEXP options[:desc] = Lazydoc.register($1, $3.to_i - 1, Lazydoc::Config) break end end if options[:desc] == nil configurations.add(key, value, options) end # Alias for Tap::Support::Validation def c Validation end private # Returns special argument types for standard validation # blocks, such as switch (Validation::SWITCH) and list # (Validation::LIST). def arg_type(block) # :nodoc: case block when Validation::SWITCH then :switch when Validation::FLAG then :flag when Validation::LIST then :list else nil end end # Returns special argument names for standard validation # blocks, such as switch (Validation::ARRAY) and list # (Validation::HASH). def arg_name(block) # :nodoc: case block when Validation::ARRAY then "'[a, b, c]'" when Validation::HASH then "'{one: 1, two: 2}'" else nil end end end end end