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' # # type1 = [:string, {:value => "Type1"}] # type2 = [:string, {:value => "Type2"}] # # choices = [ type1, type2 ] # a = BinData::Choice.new(:choices => choices, :selection => 1) # 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: # # :choices:: 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. # :selection:: 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 class << self # Returns a sanitized +params+ that is of the form expected # by #initialize. def sanitize_parameters(params, endian = nil) params = params.dup if params.has_key?(:choices) choices = params[:choices] 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 # 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 super(params, endian) 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 choices = sanitized_params[:choices] 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 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?, :single_value?, :field_names def_delegators :the_choice, :snapshot, :done_read def_delegators :the_choice, :_do_read, :_do_write, :_do_num_bytes # 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 # Override to include selected data object. def respond_to?(symbol, include_private = false) super || the_choice.respond_to?(symbol, include_private) end def method_missing(symbol, *args, &block) if the_choice.respond_to?(symbol) the_choice.__send__(symbol, *args, &block) else super end end #--------------- private # Returns the selected data object. def the_choice key = eval_param(:selection) if key.nil? raise IndexError, ":selection returned nil value" end 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