File: choice.rb

package info (click to toggle)
ruby-bindata 2.4.14-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 600 kB
  • sloc: ruby: 8,566; makefile: 4
file content (186 lines) | stat: -rw-r--r-- 5,598 bytes parent folder | download | duplicates (2)
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