lib/bindata/choice.rb in bindata-0.9.3 vs lib/bindata/choice.rb in bindata-0.10.0

- old
+ new

@@ -1,8 +1,9 @@ require 'forwardable' require 'bindata/base' require 'bindata/sanitize' +require 'bindata/trace' 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. @@ -27,98 +28,114 @@ # mychoice = 'big' # choices = {'big' => :uint16be, 'little' => :uint16le} # a = BinData::Choice.new(:choices => choices, # :selection => lambda { mychoice }) # a.value = 256 - # a.to_s #=> "\001\000" + # a.to_binary_s #=> "\001\000" # mychoice.replace 'little' # a.selection #=> 'little' - # a.to_s #=> "\000\001" + # a.to_binary_s #=> "\000\001" # # # == Parameters # # Parameters may be provided at initialisation to control the behaviour of # an object. These params are: # - # <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. + # <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 constraint 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. + # <tt>:copy_on_change</tt>:: If set to true, copy the value of the previous + # selection to the current selection whenever the + # selection changes. Default is false. 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 + mandatory_parameters :choices, :selection + optional_parameter :copy_on_change 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] + choices = choices_as_hash(params[:choices]) + ensure_valid_keys(choices) + params[:choices] = sanitized_choices(sanitizer, choices) + end - # 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 + super(sanitizer, params) + 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 + #------------- + private - # 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 + def choices_as_hash(choices) + if choices.respond_to?(:to_ary) + key_array_by_index(choices.to_ary) + else + choices end + end - super(sanitizer, params) + def key_array_by_index(array) + result = {} + array.each_with_index do |el, i| + result[i] = el unless el.nil? + end + result end + + def ensure_valid_keys(choices) + 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 + end + + def sanitized_choices(sanitizer, choices) + result = {} + choices.each_pair do |key, val| + type, param = val + the_class = sanitizer.lookup_class(type) + sanitized_params = sanitizer.sanitized_params(the_class, param) + result[key] = [the_class, sanitized_params] + end + result + end end def initialize(params = {}, parent = nil) super(params, parent) - @choices = {} - @last_key = nil + @choices = {} + @last_selection = nil end # A convenience method that returns the current selection. def selection - eval_param(:selection) + eval_parameter(: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 + # class ProgrammaticChoice < BinData::Record # choice :data, :choices => :choices, :selection => :selection # attrib_accessor :selection # end # # type1 = [:string, {:value => "Type1"}] @@ -134,64 +151,122 @@ # pc.data #=> "Type2" def selection=(v) raise NoMethodError end - # A choice represents a specific object. - def obj - the_choice - end + def_delegators :current_choice, :clear, :clear? - 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) + super || current_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) + if current_choice.respond_to?(symbol) + current_choice.__send__(symbol, *args, &block) else super end end + def debug_name_of(child) + debug_name + end + + def offset_of(child) + offset + end + #--------------- private - # Returns the selected data object. - def the_choice - key = eval_param(:selection) + def _do_read(io) + trace_selection + current_choice.do_read(io) + end - if key.nil? - raise IndexError, ":selection returned nil value" + def trace_selection + BinData::trace_message do |tracer| + selection_string = eval_parameter(:selection).inspect + if selection_string.length > 30 + selection_string = selection_string.slice(0 .. 30) + "..." + end + + tracer.trace("#{debug_name}-selection- => #{selection_string}") end + end - obj = @choices[key] + def _done_read + current_choice.done_read + end + + def _do_write(io) + current_choice.do_write(io) + end + + def _do_num_bytes(what) + current_choice.do_num_bytes(what) + end + + def _assign(val) + current_choice.assign(val) + end + + def _snapshot + current_choice.snapshot + end + + def current_choice + selection = eval_parameter(:selection) + if selection.nil? + raise IndexError, ":selection returned nil for #{debug_name}" + end + + obj = get_or_instantiate_choice(selection) + copy_previous_value_if_required(selection, obj) + + obj + end + + def get_or_instantiate_choice(selection) + obj = @choices[selection] 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 + obj = instantiate_choice(selection) + @choices[selection] = obj end + 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 + def instantiate_choice(selection) + choice_class, choice_params = get_parameter(:choices)[selection] + if choice_class.nil? + raise IndexError, "selection '#{selection}' does not exist in :choices for #{debug_name}" end + choice_class.new(choice_params, self) + end - obj + def copy_previous_value_if_required(selection, obj) + prev = get_previous_choice(selection) + if should_copy_value?(prev, obj) + obj.assign(prev) + end + remember_current_selection(selection) + end + + def should_copy_value?(prev, cur) + prev != nil and eval_parameter(:copy_on_change) == true + end + + def get_previous_choice(selection) + if selection != @last_selection and @last_selection != nil + @choices[@last_selection] + else + nil + end + end + + def remember_current_selection(selection) + if selection != @last_selection + @last_selection = selection + end end end end