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: -> { %w(a test message).join(" ") })
#
# As a shortcut, :foo is the equivalent of lambda { foo }.
#
# When evaluating lambdas, unknown methods are resolved in the context of the
# parent of the bound data object. Resolution is attempted firstly as keys
# in #parameters, and secondly as methods in this parent. This
# resolution propagates up the chain of parent data objects.
#
# An evaluation will recurse until it returns a result that is not
# a lambda or a symbol.
#
# This resolution process makes the lambda easier to read as we just write
# field instead of obj.field.
class LazyEvaluator
# Creates a new evaluator. All lazy evaluation is performed in the
# context of +obj+.
def initialize(obj)
@obj = obj
end
def lazy_eval(val, overrides = nil)
@overrides = overrides if overrides
if val.is_a? Symbol
__send__(val)
elsif val.respond_to? :arity
instance_exec(&val)
else
val
end
end
# Returns a LazyEvaluator for the parent of this data object.
def parent
if @obj.parent
@obj.parent.lazy_evaluator
else
nil
end
end
# Returns the index of this data object inside it's nearest container
# array.
def index
return @overrides[:index] if defined?(@overrides) && @overrides.key?(:index)
child = @obj
parent = @obj.parent
while parent
if parent.respond_to?(:find_index_of)
return parent.find_index_of(child)
end
child = parent
parent = parent.parent
end
raise NoMethodError, "no index found"
end
def method_missing(symbol, *args)
return @overrides[symbol] if defined?(@overrides) && @overrides.key?(symbol)
if @obj.parent
eval_symbol_in_parent_context(symbol, args)
else
super
end
end
#---------------
private
def eval_symbol_in_parent_context(symbol, args)
result = resolve_symbol_in_parent_context(symbol, args)
recursively_eval(result, args)
end
def resolve_symbol_in_parent_context(symbol, args)
obj_parent = @obj.parent
if obj_parent.has_parameter?(symbol)
obj_parent.get_parameter(symbol)
elsif obj_parent.safe_respond_to?(symbol, true)
obj_parent.__send__(symbol, *args)
else
symbol
end
end
def recursively_eval(val, args)
if val.is_a?(Symbol)
parent.__send__(val, *args)
elsif val.respond_to?(:arity)
parent.instance_exec(&val)
else
val
end
end
end
end