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. Method calls will be delegated to the active # choice. # # require 'bindata' # # type1 = [:string, {:value => "Type1"}] # type2 = [:string, {:value => "Type2"}] # # choices = {5 => type1, 17 => type2} # a = BinData::Choice.new(:choices => choices, :selection => 5) # a.value # => "Type1" # # 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" # # 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.replace 'little' # a.selection #=> '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. bindata_mandatory_parameters :choices, :selection class << self # Ensures that +params+ is of the form expected by #initialize. def sanitize_parameters!(sanitizer, params) if params.has_key?(:choices) choices = params[:choices] # convert array to hash keyed by index if ::Array === choices tmp = {} choices.each_with_index do |el, i| tmp[i] = el unless el.nil? end choices = tmp end # ensure valid hash keys if choices.has_key?(nil) raise ArgumentError, ":choices hash may not have nil key" end if choices.keys.detect { |k| Symbol === k } raise ArgumentError, ":choices hash may not have symbols for keys" end # sanitize each choice new_choices = {} choices.each_pair do |key, val| type, param = val klass = sanitizer.lookup_klass(type) sanitized_params = sanitizer.sanitize_params(klass, param) new_choices[key] = [klass, sanitized_params] end params[:choices] = new_choices end super(sanitizer, params) end end def initialize(params = {}, parent = nil) super(params, parent) @choices = {} @last_key = nil end # A convenience method that returns the current selection. def selection eval_param(:selection) end # This method does not exist. This stub only exists to document why. # There is no #selection= method to complement the #selection method. # This is deliberate to promote the declarative nature of BinData. # # If you really *must* be able to programmatically adjust the selection # then try something like the following. # # class ProgrammaticChoice < BinData::MultiValue # choice :data, :choices => :choices, :selection => :selection # attrib_accessor :selection # end # # type1 = [:string, {:value => "Type1"}] # type2 = [:string, {:value => "Type2"}] # # choices = {5 => type1, 17 => type2} # pc = ProgrammaticChoice.new(:choices => choices) # # pc.selection = 5 # pc.data #=> "Type1" # # pc.selection = 17 # pc.data #=> "Type2" def selection=(v) raise NoMethodError end # A choice represents a specific object. def obj the_choice end def_delegators :the_choice, :clear, :clear?, :single_value? def_delegators :the_choice, :done_read, :_snapshot def_delegators :the_choice, :_do_read, :_do_write, :_do_num_bytes # 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 = no_eval_param(:choices)[key] if choice_klass.nil? raise IndexError, "selection #{key} does not exist in :choices" end obj = choice_klass.new(choice_params, self) @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