module BinData # A LazyEvaluator is bound to a data object. The evaluator will evaluate # lambdas in the context of this data object. These lambdas # are those that are passed to data objects as parameters, e.g.: # # BinData::String.new(:value => lambda { %w{a test message}.join(" ") }) # # When evaluating lambdas, unknown methods are resolved in the context of # the parent of the bound data object, first as keys in #parameters, and # secondly as methods in this parent. This resolution propagates up the # chain of parent data objects. # # This resolution process makes the lambda easier to read as we just write # field instead of obj.field. class LazyEvaluator class << self # Lazily evaluates +val+ in the context of +obj+, with possibility of # +overrides+. def eval(val, obj, overrides = nil) env = self.new(obj) env.lazy_eval(val, overrides) end end # An empty hash shared by all instances @@empty_hash = Hash.new.freeze # Creates a new evaluator. All lazy evaluation is performed in the # context of +obj+. def initialize(obj) @obj = obj @overrides = @@empty_hash end # Returns a LazyEvaluator for the parent of this data object. def parent if @obj.parent LazyEvaluator.new(@obj.parent) else nil end end # Evaluates +val+ in the context of this data object. Evaluation # recurses until it yields a value that is not a symbol or lambda. # +overrides+ is an optional +obj.parameters+ like hash. def lazy_eval(val, overrides = nil) result = val @overrides = overrides if overrides if val.is_a? Symbol # treat :foo as lambda { foo } result = __send__(val) elsif val.respond_to? :arity result = instance_eval(&val) end @overrides = @@empty_hash result end def method_missing(symbol, *args) if @overrides.include?(symbol) @overrides[symbol] elsif symbol == :index array_index elsif @obj.parent val = symbol if @obj.parent.parameters and @obj.parent.parameters.has_key?(symbol) val = @obj.parent.parameters[symbol] elsif @obj.parent and @obj.parent.respond_to?(symbol) val = @obj.parent.__send__(symbol, *args) end LazyEvaluator.eval(val, @obj.parent) else super end end #--------------- private # Returns the index in the closest ancestor array of this data object. def array_index bindata_array_klass = BinData.const_defined?("Array") ? BinData.const_get("Array") : nil child = @obj parent = @obj.parent while parent if parent.class == bindata_array_klass return parent.index(child) end child = parent parent = parent.parent end raise NoMethodError, "no index found" end end end