From ca5e28805ad8ca2c1ee55c20ef3109601dce678f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Zurcher?= Date: Sun, 23 Aug 2009 12:48:05 +0200 Subject: add option inspired from innate --- lib/ayk/options.rb | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 lib/ayk/options.rb diff --git a/lib/ayk/options.rb b/lib/ayk/options.rb new file mode 100644 index 0000000..13b3e40 --- /dev/null +++ b/lib/ayk/options.rb @@ -0,0 +1,182 @@ +#! /usr/bin/env ruby +# -*- coding: UTF-8 -*- + +#---------------------------------------------------------------------------- +# +# File : options.rb +# Author : Jérémy Zurcher +# Date : 20/07/09 +# +#---------------------------------------------------------------------------- +# +module AYK + # + class OptionsError < Exception; end + # + # Provides a minimal DSL to describe options with defaults and metadata. + # fork of Innate::Options + class Options + # + @pedantic = true # raise OptionsError in particular situation + class << self + attr_accessor :pedantic + end + def pedantic?; self.class.pedantic; end + # + # @param [#to_s] name unused at the moment + # @param [Options] parent parent object inherited from Options + def initialize name, parent = nil + @name = name + @parent = parent + @hash = {} + yield(self) if block_given? + end + # + # Shortcut for instance_eval + def dsl &block + instance_eval(&block) if block + self + end + # + # Store an option in the Options instance. + # + # @param [#to_s] doc describing the purpose of this option + # @param [#to_sym] key used to access + # @param [Object] value may be anything + # @param [Hash] other optional Hash containing meta-data + # :doc, :value keys will be ignored + def option doc, key, value, other = {}, &block + ks = key.to_sym + raise OptionsError.new("Overriding existing option #{key}") if pedantic? and @hash.has_key? ks + trigger = block || other[:trigger] + convert = {:doc => doc.to_s, :value => value} + convert[:trigger] = trigger if trigger + @hash[ks] = other.merge(convert) + end + alias o option + # + # Create a new Options instance with +name+ and pass +block+ on to its #dsl. + # Assigns the new instance to the +name+ Symbol on current instance. + def sub name, &block + ns = name.to_sym + if @hash.has_key? ns + found = @hash[ns] + raise OptionsError.new("Calling sub on an existing option : #{name}") if not found.is_a? Options + found.dsl(&block) + else + found = @hash[name] = Options.new(name, self).dsl(&block) + end + found + end + # + # Add a block that will be called when a new value is set. + # this block should behave like Proc.new { |new_value,prev_value| ... } + def trigger key, &block + ks = key.to_sym + raise OptionError.new("Setting trigger for unknown option : #{key}") if pedantic? and not @hash.has_key? ks + @hash[ks][:trigger] = block + end + # + # To avoid lookup on the parent, we can set a default to the internal Hash. + # Parameters as in {Options#o}, but without the +key+. + def default doc, value, other={} + @hash.default = other.merge(:doc => doc, :value => value) + end + # + # Try to retrieve the corresponding Hash for the passed keys, will try to + # retrieve the key from a parent if no match is found on the current + # instance. If multiple keys are passed it will try to find a matching + # child and pass the request on to it. + def get(key, *keys) + if keys.empty? + if value = @hash[key.to_sym] + value # FIXME may be default value, OK ? + elsif not @parent.nil? + @parent.get(key) + else + nil + end + elsif sub_options = get(key) + sub_options.get(*keys) + end + end + private :get + # + # Retrieve only the :value from the value hash if found via +keys+. + def [](*keys) + value = get(*keys) + value.is_a?(Hash) ? value[:value] : value + end + # + # Assign new :value to the value hash on the current instance. + def []=(key, value) + ks = key.to_sym + if @hash.has_key? ks + ns = @hash[ks] + ns[:trigger].call(value,ns[:value]) if ns[:trigger].respond_to?(:call) + ns[:value] = value + elsif existing = get(key) + option(existing[:doc].to_s.dup, key, value) + else + option(":nodoc:", key, value) + end + end + # + # handle access to options hash + def method_missing(meth, *args) + raise NoMethodError.new "Trying to access private method #{meth} through method_missing" if private_methods.include? meth + case meth.to_s + when /^(.*)=$/ + self[$1] = args.first + else + self[meth] + end + end +# # +# # @param [Array] keys +# # @param [Object] value +# def set_value(keys, value) +# got = get(*keys) +# return got[:value] = value if got +# raise(IndexError, "There is no option available for %p" % [keys]) +# end +# def merge!(hash) +# hash.each_pair do |key, value| +# set_value(key.to_s.split('.'), value) +# end +# end + # + def to_hash; @hash end + def each █ @hash.each(&block) end + def each_pair + @hash.each do |key, values| + yield(key, self[key]) + end + end + def inspect; @hash.inspect end + def pretty_print q; q.pp_hash @hash end + end + # +# # extend your class with this +# module Optioned +# def self.included(into) +# into.extend(SingletonMethods) +# +# snaked = into.name.split('::').last +# snaked = snaked.gsub(/\B[A-Z][^A-Z]/, '_\&').downcase.gsub(' ', '_') +# +# options = Innate.options.sub(snaked) +# into.instance_variable_set(:@options, options) +# end +# +# module SingletonMethods +# attr_reader :options +# end +# +# private +# +# def options +# self.class.options +# end +# end +end -- cgit v1.1-2-g2b99