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