require 'bindata/base'
require 'bindata/trace'
module BinData
# A BinData::BasePrimitive object is a container for a value that has a
# particular binary representation. A value corresponds to a primitive type
# such as as integer, float or string. Only one value can be contained by
# this object. This value can be read from or written to an IO stream.
#
# require 'bindata'
#
# obj = BinData::Uint8.new(:initial_value => 42)
# obj.value #=> 42
# obj.value = 5
# obj.value #=> 5
# obj.clear
# obj.value #=> 42
#
# obj = BinData::Uint8.new(:value => 42)
# obj.value #=> 42
# obj.value = 5
# obj.value #=> 42
#
# obj = BinData::Uint8.new(:check_value => 3)
# obj.read("\005") #=> BinData::ValidityError: value is '5' but expected '3'
#
# obj = BinData::Uint8.new(:check_value => lambda { value < 5 })
# obj.read("\007") #=> BinData::ValidityError: value not as expected
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params include those for BinData::Base as well as:
#
# [:initial_value] This is the initial value to use before one is
# either #read or explicitly set with #value=.
# [:value] The object will always have this value.
# Calls to #value= are ignored when
# using this param. In the interval between
# calls to #do_read and #done_read, #value
# will return the value of the data read from the
# IO, not the result of the :value param.
# [:check_value] Raise an error unless the value read in meets
# this criteria. The variable +value+ is made
# available to any lambda assigned to this
# parameter. A boolean return indicates success
# or failure. Any other return is compared to
# the value just read in.
class BasePrimitive < BinData::Base
optional_parameters :initial_value, :value, :check_value
mutually_exclusive_parameters :initial_value, :value
def initialize(params = {}, parent = nil)
super(params, parent)
@value = nil
@in_read = false
end
def clear
@value = nil
@in_read = false
end
def clear?
@value.nil?
end
def value
# TODO: warn "#value is deprecated, use #snapshot instead"
snapshot
end
def value=(val)
# TODO: warn "#value= is deprecated, use #assign instead"
assign(val)
end
def respond_to?(symbol, include_private=false)
super || value.respond_to?(symbol, include_private)
end
def method_missing(symbol, *args, &block)
if value.respond_to?(symbol)
value.__send__(symbol, *args, &block)
else
super
end
end
def eql?(other)
# double dispatch
other.eql?(snapshot)
end
def hash
snapshot.hash
end
#---------------
private
def _do_read(io)
@in_read = true
@value = read_and_return_value(io)
trace_value
if has_parameter?(:check_value)
check_value(value)
end
end
def trace_value
BinData::trace_message do |tracer|
value_string = _value.inspect
if value_string.length > 30
value_string = value_string.slice(0 .. 30) + "..."
end
tracer.trace("#{debug_name} => #{value_string}")
end
end
def check_value(current_value)
expected = eval_parameter(:check_value, :value => current_value)
if not expected
raise ValidityError,
"value '#{current_value}' not as expected for #{debug_name}"
elsif current_value != expected and expected != true
raise ValidityError,
"value is '#{current_value}' but " +
"expected '#{expected}' for #{debug_name}"
end
end
def _done_read
@in_read = false
end
def _do_write(io)
raise "can't write whilst reading #{debug_name}" if @in_read
io.writebytes(value_to_binary_string(_value))
end
def _do_num_bytes(ignored)
value_to_binary_string(_value).length
end
def _assign(val)
raise ArgumentError, "can't set a nil value for #{debug_name}" if val.nil?
unless has_parameter?(:value)
raw_val = val.respond_to?(:snapshot) ? val.snapshot : val
@value = begin
raw_val.dup
rescue TypeError
# can't dup Fixnums
raw_val
end
end
end
def _snapshot
_value
end
# The unmodified value of this data object. Note that #value calls this
# method. This is so that #value can be overridden in subclasses to
# modify the value.
def _value
# Table of possible preconditions and expected outcome
# 1. :value and !in_read -> :value
# 2. :value and in_read -> @value
# 3. :initial_value and clear? -> :initial_value
# 4. :initial_value and !clear? -> @value
# 5. clear? -> sensible_default
# 6. !clear? -> @value
if not @in_read and has_parameter?(:value)
# rule 1 above
eval_parameter(:value)
else
# combining all other rules gives this simplified expression
@value || eval_parameter(:value) ||
eval_parameter(:initial_value) || sensible_default()
end
end
###########################################################################
# To be implemented by subclasses
# Return the string representation that +val+ will take when written.
def value_to_binary_string(val)
raise NotImplementedError
end
# Read a number of bytes from +io+ and return the value they represent.
def read_and_return_value(io)
raise NotImplementedError
end
# Return a sensible default for this data.
def sensible_default
raise NotImplementedError
end
# To be implemented by subclasses
###########################################################################
end
end