lib/bindata/single.rb



require 'bindata/base'

module BinData
  # A BinData::Single 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:
  #
  # [<tt>:initial_value</tt>] This is the initial value to use before one is
  #                           either #read or explicitly set with #value=.
  # [<tt>:value</tt>]         The object will always have this value.
  #                           Explicitly calling #value= is prohibited 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 <tt>:value</tt> param.
  # [<tt>:check_value</tt>]   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 Single < BinData::Base
    # These are the parameters used by this class.
    bindata_optional_parameters :initial_value, :value, :check_value
    bindata_mutually_exclusive_parameters :initial_value, :value

    def initialize(params = {}, parent = nil)
      super(params, parent)
      clear
    end

    # Resets the internal state to that of a newly created object.
    def clear
      @value = nil
      @in_read = false
    end

    # Returns if the value of this data has been read or explicitly set.
    def clear?
      @value.nil?
    end

    # Single objects are single_values
    def single_value?
      true
    end

    # To be called after calling #do_read.
    def done_read
      @in_read = false
    end

    # Returns the current value of this data.
    def value
      _value
    end

    # Sets the value of this data.
    def value=(v)
      # only allow modification if the value isn't predefined
      unless has_param?(:value)
        raise ArgumentError, "can't set a nil value" if v.nil?
        @value = v

        # Note that this doesn't do anything in ruby 1.8.x so ignore for now
        # # explicitly return the output of #value as v may be different
        # self.value
      end
    end

    #---------------
    private

    # Reads the value for this data from +io+.
    def _do_read(io)
      @in_read = true
      @value   = read_val(io)

      # does the value meet expectations?
      if has_param?(:check_value)
        current_value = self.value
        expected = eval_param(:check_value, :value => current_value)
        if not expected
          raise ValidityError, "value '#{current_value}' not as expected"
        elsif current_value != expected and expected != true
          raise ValidityError, "value is '#{current_value}' but " +
                               "expected '#{expected}'"
        end
      end
    end

    # Writes the value for this data to +io+.
    def _do_write(io)
      raise "can't write whilst reading" if @in_read
      io.writebytes(val_to_str(_value))
    end

    # Returns the number of bytes it will take to write this data.
    def _do_num_bytes(ignored)
      val_to_str(_value).length
    end

    # Returns a snapshot of this data object.
    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 (evaluated_value = eval_param(:value))
        # rule 1 above
        evaluated_value
      else
        # combining all other rules gives this simplified expression
        @value || eval_param(:value) ||
          eval_param(:initial_value) || sensible_default()
      end
    end

    ###########################################################################
    # To be implemented by subclasses

    # Return the string representation that +val+ will take when written.
    def val_to_str(val)
      raise NotImplementedError
    end

    # Read a number of bytes from +io+ and return the value they represent.
    def read_val(io)
      raise NotImplementedError
    end

    # Return a sensible default for this data.
    def sensible_default
      raise NotImplementedError
    end

    # To be implemented by subclasses
    ###########################################################################
  end
end