lib/volute.rb in volute-0.1.0 vs lib/volute.rb in volute-0.1.1

- old
+ new

@@ -20,13 +20,16 @@ # THE SOFTWARE. # # Made in Japan. #++ + +# TODO : insert lengthy explanation here +# module Volute - VOLUTE_VERSION = '0.1.0' + VOLUTE_VERSION = '0.1.1' # # adding class methods to target classes def self.included(target) @@ -63,40 +66,106 @@ def volute_set(key, value) previous_value = volute_get(key) vset(key, value) - Volute.root_eval(self, key, previous_value, value) + Volute.apply(self, key, previous_value, value) value end # # Volute class methods - def self.<<(block) + def self.register(args, block) - (@top ||= []) << block + top << [ args, block ] end # Nukes all the top level volutes. # def self.clear! - (@top = []) + @top = nil end - def self.root_eval(object, attribute, previous_value, value) + # Volute.apply is generally called from the setter of a class which include + # Volute, but it's OK to call it directly, to force volute application. + # + # class Engine + # attr_accessor :state + # def turn_key! + # @key_turned = true + # Volute.apply(self, :key_turned) + # end + # def press_red_button! + # Volute.apply(self) + # end + # end + # + # volute Engine do + # if attribute == :key_turned + # object.state = :running + # else + # object.state = :off + # end + # end + # + def self.apply(object, attribute=nil, previous_value=nil, value=nil) target = Target.new(object, attribute, previous_value, value) - (@top || []).each { |args, block| target.volute(*args, &block) } + top.each { |args, block| target.volute(*args, &block) } end + def self.top + + (@top ||= VoluteArray.new) + end + # # some classes + # Subclassing Array to add a #filter and a #remove method, so that + # things like + # + # all_the_volutes = volutes + # volutes_about_class_x = volutes(x) + # + # volutes.remove(x) + # # just removed all the volutes referring class or attribute x + # + class VoluteArray < Array + + def filter(arg) + + select { |args, block| + + classes = args.select { |a| a.is_a?(Class) } + + if args.include?(arg) + true + elsif arg.is_a?(Class) + (arg.ancestors & classes).size > 0 + elsif arg.is_a?(Module) + (arg.constants.collect { |c| arg.const_get(c) } & classes).size > 0 + #elsif arg.is_a?(Symbol) + # already handled by the initial if + else + false + end + } + end + + def remove(arg) + + filtered = filter(arg) + + reject! { |volute| filtered.include?(volute) } + end + end + class Target attr_reader :object, :attribute, :previous_value, :value def initialize(object, attribute, previous_value, value) @@ -110,62 +179,120 @@ end def volute(*args, &block) return if @over - return unless match?(args) + match = (args.first == :not) ? ( ! match?(args[1..-1])) : match?(args) + return unless match + self.instance_eval(&block) end def over @over = true end - #def is(val) - # val == value - #end - #def was(val) - # val == previous_value - #end - protected - def match? (args) + def match?(args) + return true if args.empty? + + return state_match?(args) if is_a_state_match?(args) + classes = args.select { |a| a.is_a?(Class) } args.select { |a| a.is_a?(Module) }.each { |m| classes.concat(m.constants.collect { |c| m.const_get(c) }) } - if classes.size > 0 && (classes & object.class.ancestors).empty? - return false - end + return true if (classes & @object.class.ancestors).size > 0 atts = args.select { |a| a.is_a?(Symbol) } - if atts.size > 0 && ( ! atts.include?(attribute.to_sym)) - return false + return true if atts.include?(attribute.to_sym) + + atts = args.select { |a| a.is_a?(Regexp) } + return true if atts.find { |r| r.match(attribute.to_s) } + + return transition_match?(args.last) if args.last.is_a?(Hash) + + false + end + + def has_attribute?(att) + + return false if att == :any + + #m = @object.method(att) rescue nil + #return false unless m + #return false if m.arity != -1 + # arity would be -1 on ruby 1.8.7 and 0 on ruby 1.9.1, ... + + begin + @object.send(att) + true + rescue NoMethodError => nme + false end + end - opts = args.last.is_a?(Hash) ? args.pop : {} + def is_a_state_match?(args) - return true if opts.empty? + state = args.first - opts.inject(false) do |b, (k, v)| - b || ( - (k == :any || k == previous_value) && - (v == :any || v == value) - ) + return false if args.length != 1 + return false unless state.is_a?(Hash) + return false if state.keys.find { |arg| ! arg.is_a?(Symbol) } + return false if state.keys.find { |att| ! has_attribute?(att) } + true + end + + def val_match?(target, current, in_array=false) + + return true if target == current + return true if target == :any + return current != nil if target == :not_nil + + if target.is_a?(Regexp) && current.is_a?(String) + return target.match(current) end + if in_array == false && target.is_a?(Array) + return target.find { |t| val_match?(t, current, true) } + end - #rescue Exception => e - # p e - # e.backtrace.each { |l| p l } + false end + + def state_match?(args) + + args.first.each do |att, target_val| + return false unless val_match?(target_val, @object.send(att)) + end + true + end + + def transition_match?(hash) + + hash.each do |startv, endv| + if val_match?(startv, previous_value) && val_match?(endv, value) + return true + end + end + false + end end end +# Registers a 'volute' at the top level (ie a volute not nested into another) +# def volute(*args, &block) - Volute << [ args, block ] + Volute.register(args, block) +end + +# With no arguments, it will list all the top-level volutes. +# +def volutes(arg=nil) + + arg ? Volute.top.filter(arg) : Volute.top end