1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
|
require 'bindata/base'
require 'bindata/dsl'
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 # => "Type1"
#
# choices = [ type1, type2 ]
# a = BinData::Choice.new(choices: choices, selection: 1)
# a # => "Type2"
#
# choices = [ nil, nil, nil, type1, nil, type2 ]
# a = BinData::Choice.new(choices: choices, selection: 3)
# a # => "Type1"
#
#
# Chooser = Struct.new(:choice)
# mychoice = Chooser.new
# mychoice.choice = 'big'
#
# choices = {'big' => :uint16be, 'little' => :uint16le}
# a = BinData::Choice.new(choices: choices, copy_on_change: true,
# selection: -> { mychoice.choice })
# a.assign(256)
# a.to_binary_s #=> "\001\000"
#
# mychoice.choice = 'little'
# 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 constraint is that the hash may
# not contain symbols as keys, with the exception
# of :default. :default is to be used when then
# :selection does not exist in the :choices hash.
# <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 DSLMixin
dsl_parser :choice
arg_processor :choice
mandatory_parameters :choices, :selection
optional_parameter :copy_on_change
def initialize_shared_instance
extend CopyOnChangePlugin if eval_parameter(:copy_on_change) == true
super
end
def initialize_instance
@choices = {}
@last_selection = nil
end
# Returns the current selection.
def selection
selection = eval_parameter(:selection)
if selection.nil?
raise IndexError, ":selection returned nil for #{debug_name}"
end
selection
end
def respond_to?(symbol, include_private = false) #:nodoc:
current_choice.respond_to?(symbol, include_private) || super
end
def method_missing(symbol, *args, &block) #:nodoc:
current_choice.__send__(symbol, *args, &block)
end
%w(clear? assign snapshot do_read do_write do_num_bytes).each do |m|
module_eval <<-END
def #{m}(*args)
current_choice.#{m}(*args)
end
END
end
#---------------
private
def current_choice
current_selection = selection
@choices[current_selection] ||= instantiate_choice(current_selection)
end
def instantiate_choice(selection)
prototype = get_parameter(:choices)[selection]
if prototype.nil?
raise IndexError, "selection '#{selection}' does not exist in :choices for #{debug_name}"
end
prototype.instantiate(nil, self)
end
end
class ChoiceArgProcessor < BaseArgProcessor
def sanitize_parameters!(obj_class, params) #:nodoc:
params.merge!(obj_class.dsl_params)
params.sanitize_choices(:choices) do |choices|
hash_choices = choices_as_hash(choices)
ensure_valid_keys(hash_choices)
hash_choices
end
end
#-------------
private
def choices_as_hash(choices)
if choices.respond_to?(:to_ary)
key_array_by_index(choices.to_ary)
else
choices
end
end
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.key?(nil)
raise ArgumentError, ":choices hash may not have nil key"
end
if choices.keys.detect { |key| key.is_a?(Symbol) && key != :default }
raise ArgumentError, ":choices hash may not have symbols for keys"
end
end
end
# Logic for the :copy_on_change parameter
module CopyOnChangePlugin
def current_choice
obj = super
copy_previous_value(obj)
obj
end
def copy_previous_value(obj)
current_selection = selection
prev = get_previous_choice(current_selection)
obj.assign(prev) unless prev.nil?
remember_current_selection(current_selection)
end
def get_previous_choice(selection)
if @last_selection && selection != @last_selection
@choices[@last_selection]
end
end
def remember_current_selection(selection)
@last_selection = selection
end
end
end
|