lib/bindata/choice.rb in bindata-0.8.1 vs lib/bindata/choice.rb in bindata-0.9.0
- old
+ new
@@ -1,94 +1,151 @@
+require 'forwardable'
require 'bindata/base'
+require 'bindata/sanitize'
module BinData
# A Choice is a collection of data objects of which only one is active
# at any particular time.
#
# require 'bindata'
- # require 'stringio'
#
- # choices = [ [:int8, {:value => 3}], [:int8, {:value => 5}] ]
+ # type1 = [:string, {:value => "Type1"}]
+ # type2 = [:string, {:value => "Type2"}]
+ #
+ # choices = [ type1, type2 ]
# a = BinData::Choice.new(:choices => choices, :selection => 1)
- # a.value # => 5
+ # a.value # => "Type2"
#
+ # choices = [ nil, nil, nil, type1, nil, type2 ]
+ # a = BinData::Choice.new(:choices => choices, :selection => 3)
+ # a.value # => "Type1"
+ #
+ # choices = {5 => type1, 17 => type2}
+ # a = BinData::Choice.new(:choices => choices, :selection => 5)
+ # a.value # => "Type1"
+ #
+ # mychoice = 'big'
+ # choices = {'big' => :uint16be, 'little' => :uint16le}
+ # a = BinData::Choice.new(:choices => choices,
+ # :selection => lambda { mychoice })
+ # a.value = 256
+ # a.to_s #=> "\001\000"
+ # mychoice[0..-1] = 'little'
+ # a.to_s #=> "\000\001"
+ #
+ #
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
- # <tt>:choices</tt>:: An array specifying the possible data objects.
- # The format of the array is a list of symbols
- # representing the data object type. If a choice
- # is to have params passed to it, then it should be
- # provided as [type_symbol, hash_params].
- # <tt>:selection</tt>:: An index into the :choices array which specifies
- # the currently active choice.
- class Choice < Base
+ # <tt>:choices</tt>:: Either an array or a hash specifying the possible
+ # data objects. The format of the array/hash.values is
+ # a list of symbols representing the data object type.
+ # If a choice is to have params passed to it, then it
+ # should be provided as [type_symbol, hash_params].
+ # An implementation gotcha is that the hash may not
+ # contain symbols as keys.
+ # <tt>:selection</tt>:: An index/key into the :choices array/hash which
+ # specifies the currently active choice.
+ class Choice < BinData::Base
+ extend Forwardable
# Register this class
register(self.name, self)
# These are the parameters used by this class.
mandatory_parameters :choices, :selection
- def initialize(params = {}, env = nil)
- super(params, env)
+ class << self
- # instantiate all choices
- @choices = []
- param(:choices).each do |choice_type, choice_params|
- choice_params ||= {}
- klass = klass_lookup(choice_type)
- if klass.nil?
- raise TypeError, "unknown type '#{choice_type.id2name}' for #{self}"
- end
- @choices << klass.new(choice_params, create_env)
- end
- end
+ # Returns a sanitized +params+ that is of the form expected
+ # by #initialize.
+ def sanitize_parameters(params, endian = nil)
+ params = params.dup
- # Resets the internal state to that of a newly created object.
- def clear
- the_choice.clear
- end
+ if params.has_key?(:choices)
+ choices = params[:choices]
- # Returns if the selected data object is clear?.
- def clear?
- the_choice.clear?
- end
+ case choices
+ when ::Hash
+ new_choices = {}
+ choices.keys.each do |key|
+ # ensure valid hash keys
+ if Symbol === key
+ msg = ":choices hash may not have symbols for keys"
+ raise ArgumentError, msg
+ elsif key.nil?
+ raise ArgumentError, ":choices hash may not have nil key"
+ end
- # Reads the value of the selected data object from +io+.
- def _do_read(io)
- the_choice.do_read(io)
- end
+ # collect sanitized choice values
+ type, param = choices[key]
+ klass = lookup(type, endian)
+ if klass.nil?
+ raise TypeError, "unknown type '#{type}' for #{self}"
+ end
+ val = [klass, SanitizedParameters.new(klass, param, endian)]
+ new_choices[key] = val
+ end
+ params[:choices] = new_choices
+ when ::Array
+ choices.collect! do |type, param|
+ if type.nil?
+ # allow sparse arrays
+ nil
+ else
+ klass = lookup(type, endian)
+ if klass.nil?
+ raise TypeError, "unknown type '#{type}' for #{self}"
+ end
+ [klass, SanitizedParameters.new(klass, param, endian)]
+ end
+ end
+ params[:choices] = choices
+ else
+ raise ArgumentError, "unknown type for :choices (#{choices.class})"
+ end
+ end
- # To be called after calling #do_read.
- def done_read
- the_choice.done_read
- end
+ super(params, endian)
+ end
- # Writes the value of the selected data object to +io+.
- def _write(io)
- the_choice.write(io)
- end
+ # Returns all the possible field names a :choice may have.
+ def all_possible_field_names(sanitized_params)
+ unless SanitizedParameters === sanitized_params
+ raise ArgumentError, "parameters aren't sanitized"
+ end
- # Returns the number of bytes it will take to write the
- # selected data object.
- def _num_bytes(what)
- the_choice.num_bytes(what)
- end
+ choices = sanitized_params[:choices]
- # Returns a snapshot of the selected data object.
- def snapshot
- the_choice.snapshot
+ names = []
+ if ::Array === choices
+ choices.each do |cklass, cparams|
+ names.concat(cklass.all_possible_field_names(cparams))
+ end
+ elsif ::Hash === choices
+ choices.values.each do |cklass, cparams|
+ names.concat(cklass.all_possible_field_names(cparams))
+ end
+ end
+ names
+ end
end
- # Returns a list of the names of all fields of the selected data object.
- def field_names
- the_choice.field_names
+ def initialize(params = {}, env = nil)
+ super(params, env)
+
+ # prepare collection of instantiated choice objects
+ @choices = (param(:choices) === ::Array) ? [] : {}
+ @last_key = nil
end
+ def_delegators :the_choice, :clear, :clear?, :_do_read, :done_read
+ def_delegators :the_choice, :_write, :_num_bytes, :snapshot
+ def_delegators :the_choice, :single_value?, :field_names
+
# Returns the data object that stores values for +name+.
def find_obj_for_name(name)
field_names.include?(name) ? the_choice.find_obj_for_name(name) : nil
end
@@ -108,13 +165,37 @@
#---------------
private
# Returns the selected data object.
def the_choice
- index = eval_param(:selection)
- if index < 0 or index >= @choices.length
- raise IndexError, "selection #{index} is out of range"
+ key = eval_param(:selection)
+
+ if key.nil?
+ raise IndexError, ":selection returned nil value"
end
- @choices[index]
+
+ obj = @choices[key]
+ if obj.nil?
+ # instantiate choice object
+ choice_klass, choice_params = param(:choices)[key]
+ if choice_klass.nil?
+ raise IndexError, "selection #{key} does not exist in :choices"
+ end
+ obj = choice_klass.new(choice_params, create_env)
+ @choices[key] = obj
+ end
+
+ # for single_values copy the value when the selected object changes
+ if key != @last_key
+ if @last_key != nil
+ prev = @choices[@last_key]
+ if prev != nil and prev.single_value? and obj.single_value?
+ obj.value = prev.value
+ end
+ end
+ @last_key = key
+ end
+
+ obj
end
end
end