cordawyn-iso8583 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/exception.rb ADDED
@@ -0,0 +1,4 @@
1
+ module ISO8583
2
+ class ISO8583Exception < Exception; end
3
+ class ISO8583ParseException < ISO8583Exception; end
4
+ end
data/lib/field.rb ADDED
@@ -0,0 +1,90 @@
1
+ module ISO8583
2
+
3
+ class Field
4
+ # may either be some other Field in which the length is encoded or a Fixnum for
5
+ # fixed length fields. Length should always be the length of the *encoded* value.
6
+ # A 6 digit BCD field will require a length 3, as will a 5 digit BCD field.
7
+ # The subclass BCDField handles this to keep things consistant.
8
+ attr_accessor :length
9
+ attr_accessor :codec
10
+ attr_accessor :padding
11
+ attr_accessor :max
12
+
13
+ attr_writer :name
14
+ attr_accessor :bmp
15
+
16
+ def name
17
+ "BMP #{bmp}: #{@name}"
18
+ end
19
+
20
+ def parse(raw)
21
+ len, raw = case length
22
+ when Fixnum
23
+ [length, raw]
24
+ when Field
25
+ length.parse(raw)
26
+ else
27
+ raise ISO8583Exception.new("Cannot determine the length of '#{name}' field")
28
+ end
29
+
30
+ raw_value = raw[0,len]
31
+
32
+ # make sure we have enough data ...
33
+ if raw_value.length != len
34
+ mes = "Field has incorrect length! field: #{raw_value} len/expected: #{raw_value.length}/#{len}"
35
+ raise ISO8583ParseException.new(mes)
36
+ end
37
+
38
+ rest = raw[len, raw.length]
39
+ begin
40
+ real_value = codec.decode(raw_value)
41
+ rescue
42
+ raise ISO8583ParseException.new($!.message+" (#{name})")
43
+ end
44
+
45
+ [ real_value, rest ]
46
+ end
47
+
48
+
49
+ # Encoding needs to consider length representation, the actual encoding (such as charset or BCD)
50
+ # and padding.
51
+ # The order may be important! This impl calls codec.encode and then pads, in case you need the other
52
+ # special treatment, you may need to override this method alltogether.
53
+ # In other cases, the padding has to be implemented by the codec, such as BCD with an odd number of nibbles.
54
+ def encode(value)
55
+ encoded_value = codec.encode(value)
56
+
57
+ if padding
58
+ if padding.arity == 1
59
+ encoded_value = padding.call(encoded_value)
60
+ elsif padding.arity == 2
61
+ encoded_value = padding.call(encoded_value, length)
62
+ end
63
+ end
64
+
65
+ len_str = case length
66
+ when Fixnum
67
+ raise ISO8583Exception.new("Too long: #{value} (#{name})! length=#{length}") if encoded_value.length > length
68
+ raise ISO8583Exception.new("Too short: #{value} (#{name})! length=#{length}") if encoded_value.length < length
69
+ ""
70
+ when Field
71
+ raise ISO8583Exception.new("Max lenth exceeded: #{value}, max: #{max}") if max && encoded_value.length > max
72
+ length.encode(encoded_value.length)
73
+ else
74
+ raise ISO8583Exception.new("Invalid length (#{length}) for '#{name}' field")
75
+ end
76
+
77
+ len_str + encoded_value
78
+ end
79
+ end
80
+
81
+ class BCDField < Field
82
+ # This corrects the length for BCD fields, as their encoded length is half (+ parity) of the
83
+ # content length. E.g. 123 (length = 3) encodes to "\x01\x23" (length 2)
84
+ def length
85
+ _length = super
86
+ (_length % 2) != 0 ? (_length / 2) + 1 : _length / 2
87
+ end
88
+ end
89
+
90
+ end
data/lib/fields.rb ADDED
@@ -0,0 +1,148 @@
1
+ #--
2
+ # Copyright 2009 by Tim Becker (tim.becker@kuriostaet.de)
3
+ # MIT License, for details, see the LICENSE file accompaning
4
+ # this distribution
5
+ #++
6
+
7
+ module ISO8583
8
+
9
+ # This file contains a number of preinstantiated Field definitions. You
10
+ # will probably need to create own fields in your implementation, please
11
+ # see Field and Codec for further discussion on how to do this.
12
+ # The fields currently available are those necessary to implement the
13
+ # Berlin Groups Authorization Spec.
14
+ #
15
+ # The following fields are available:
16
+ #
17
+ # [+LL+] special form to de/encode variable length indicators, two bytes ASCII numerals
18
+ # [+LLL+] special form to de/encode variable length indicators, two bytes ASCII numerals
19
+ # [+LL_BCD+] special form to de/encode variable length indicators, two BCD digits
20
+ # [+LLVAR_N+] two byte variable length ASCII numeral, payload ASCII numerals
21
+ # [+LLLVAR_N+] three byte variable length ASCII numeral, payload ASCII numerals
22
+ # [+LLVAR_Z+] two byte variable length ASCII numeral, payload Track2 data
23
+ # [+LLVAR_ANS+] two byte variable length ASCII numeral, payload ASCII+special
24
+ # [+LLLVAR_ANS+] three byte variable length ASCII numeral, payload ASCII+special
25
+ # [+LLVAR_B+] Two byte variable length binary payload
26
+ # [+LLLVAR_B+] Three byte variable length binary payload
27
+ # [+N+] fixed lengh numerals, repesented in ASCII, padding right justified using zeros
28
+ # [+AN+] fixed lengh ASCII [A-Za-z0-9], padding left justified using spaces.
29
+ # [+ANP+] fixed lengh ASCII [A-Za-z0-9] and space, padding left, spaces
30
+ # [+ANS+] fixed length ASCII [\x20-\x7E], padding left, spaces
31
+ # [+B+] binary data, padding left using nulls (0x00)
32
+ # [+MMDDhhmmss+] Date, formatted as described in ASCII numerals
33
+ # [+YYMMDDhhmmss+] Date, formatted as named in ASCII numerals
34
+ # [+YYMM+] Expiration Date, formatted as named in ASCII numerals
35
+
36
+
37
+ # Special form to de/encode variable length indicators, two bytes ASCII numerals
38
+ LL = Field.new
39
+ LL.name = "LL"
40
+ LL.length = 2
41
+ LL.codec = ASCII_Number
42
+ LL.padding = lambda {|value|
43
+ sprintf("%02d", value)
44
+ }
45
+ # Special form to de/encode variable length indicators, three bytes ASCII numerals
46
+ LLL = Field.new
47
+ LLL.name = "LLL"
48
+ LLL.length = 3
49
+ LLL.codec = ASCII_Number
50
+ LLL.padding = lambda {|value|
51
+ sprintf("%03d", value)
52
+ }
53
+
54
+ LL_BCD = BCDField.new
55
+ LL_BCD.length = 2
56
+ LL_BCD.codec = Packed_Number
57
+
58
+ # Two byte variable length ASCII numeral, payload ASCII numerals
59
+ LLVAR_N = Field.new
60
+ LLVAR_N.length = LL
61
+ LLVAR_N.codec = ASCII_Number
62
+
63
+ # Three byte variable length ASCII numeral, payload ASCII numerals
64
+ LLLVAR_N = Field.new
65
+ LLLVAR_N.length = LLL
66
+ LLLVAR_N.codec = ASCII_Number
67
+
68
+ # Two byte variable length ASCII numeral, payload Track2 data
69
+ LLVAR_Z = Field.new
70
+ LLVAR_Z.length = LL
71
+ LLVAR_Z.codec = Track2
72
+
73
+ # Two byte variable length ASCII numeral, payload ASCII+special
74
+ LLVAR_ANS = Field.new
75
+ LLVAR_ANS.length = LL
76
+ LLVAR_ANS.codec = ANS_Codec
77
+
78
+ # Three byte variable length ASCII numeral, payload ASCII+special
79
+ LLLVAR_ANS = Field.new
80
+ LLLVAR_ANS.length = LLL
81
+ LLLVAR_ANS.codec = ANS_Codec
82
+
83
+ # Two byte variable length binary payload
84
+ LLVAR_B = Field.new
85
+ LLVAR_B.length = LL
86
+ LLVAR_B.codec = Null_Codec
87
+
88
+
89
+ # Three byte variable length binary payload
90
+ LLLVAR_B = Field.new
91
+ LLLVAR_B.length = LLL
92
+ LLLVAR_B.codec = Null_Codec
93
+
94
+ # Fixed lengh numerals, repesented in ASCII, padding right justified using zeros
95
+ N = Field.new
96
+ N.codec = ASCII_Number
97
+ N.padding = lambda {|val, len|
98
+ sprintf("%0#{len}d", val)
99
+ }
100
+
101
+ N_BCD = BCDField.new
102
+ N_BCD.codec = Packed_Number
103
+
104
+ PADDING_LEFT_JUSTIFIED_SPACES = lambda {|val, len|
105
+ sprintf "%-#{len}s", val
106
+ }
107
+
108
+ # Fixed lengh ASCII [A-Za-z0-9], padding left justified using spaces.
109
+ AN = Field.new
110
+ AN.codec = AN_Codec
111
+ AN.padding = PADDING_LEFT_JUSTIFIED_SPACES
112
+
113
+ # Fixed lengh ASCII [A-Za-z0-9] and space, padding left, spaces
114
+ ANP = Field.new
115
+ ANP.codec = ANP_Codec
116
+ ANP.padding = PADDING_LEFT_JUSTIFIED_SPACES
117
+
118
+ # Fixed length ASCII [\x20-\x7E], padding left, spaces
119
+ ANS = Field.new
120
+ ANS.codec = ANS_Codec
121
+ ANS.padding = PADDING_LEFT_JUSTIFIED_SPACES
122
+
123
+ # Binary data, padding left using nulls (0x00)
124
+ B = Field.new
125
+ B.codec = Null_Codec
126
+ B.padding = lambda {|val, len|
127
+ while val.length < len
128
+ val = val + "\000"
129
+ end
130
+ val
131
+ }
132
+
133
+ # Date, formatted as described in ASCII numerals
134
+ MMDDhhmmss = Field.new
135
+ MMDDhhmmss.codec = MMDDhhmmssCodec
136
+ MMDDhhmmss.length = 10
137
+
138
+ #Date, formatted as described in ASCII numerals
139
+ YYMMDDhhmmss = Field.new
140
+ YYMMDDhhmmss.codec = YYMMDDhhmmssCodec
141
+ YYMMDDhhmmss.length = 12
142
+
143
+ #Date, formatted as described in ASCII numerals
144
+ YYMM = Field.new
145
+ YYMM.codec = YYMMCodec
146
+ YYMM.length = 4
147
+
148
+ end
data/lib/iso8583.rb ADDED
@@ -0,0 +1,12 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ module ISO8583
5
+ require "field"
6
+ require "codec"
7
+ require "fields"
8
+ require "exception"
9
+ require "bitmap"
10
+ require "message"
11
+ require "util"
12
+ end
data/lib/message.rb ADDED
@@ -0,0 +1,417 @@
1
+ # Copyright 2009 by Tim Becker (tim.becker@kuriostaet.de)
2
+ # MIT License, for details, see the LICENSE file accompaning
3
+ # this distribution
4
+
5
+ module ISO8583
6
+
7
+ # The class `Message` defines functionality to describe classes
8
+ # representing different type of messages, or message families.
9
+ # A message family consists of a number of possible message types that
10
+ # are allowed, and a way of naming and encoding the bitmaps allowed in
11
+ # the messages.
12
+ #
13
+ # To create your own message, start by subclassing Message:
14
+ #
15
+ # class MyMessage < Message
16
+ # (...)
17
+ # end
18
+ #
19
+ # the subtyped message should be told how the MTI is encoded:
20
+ #
21
+ # class MyMessage < Message
22
+ # mti_format N, :length => 4
23
+ # (...)
24
+ # end
25
+ #
26
+ # `N` above is an instance of Field which encodes numbers into their
27
+ # ASCII representations in a fixed length field. The option `length=>4`
28
+ # indicates the length of the fixed field.
29
+ #
30
+ # Next, the allowed message types are specified:
31
+ #
32
+ # class MyMessage < Message
33
+ # (...)
34
+ # mti 1100, "Authorization Request Acquirer Gateway"
35
+ # mti 1110, "Authorization Request Response Issuer Gateway"
36
+ # (...)
37
+ # end
38
+ #
39
+ # This basically defines to message types, 1100 and 1110 which may
40
+ # be accessed later either via their name or value:
41
+ #
42
+ # mes = MyMessage.new 1100
43
+ #
44
+ # or
45
+ # mes = MyMessage.new "Authorization Request Acquirer Gateway"
46
+ #
47
+ # or
48
+ # mes = MyMessage.new
49
+ # mes.mti = 1110 # or Auth. Req. Acq. Gateway ...
50
+ #
51
+ # Finally the allowed bitmaps, their names and the encoding rules are
52
+ # specified:
53
+ #
54
+ # class MyMessage < Message
55
+ # (...)
56
+ # bmp 2, "Primary Account Number (PAN)", LLVAR_N, :max => 19
57
+ # bmp 3, "Processing Code", N, :length => 6
58
+ # bmp 4, "Amount (Transaction)", N, :length => 12
59
+ # bmp 6, "Amount, Cardholder Billing" , N, :length => 12
60
+ # (...)
61
+ # end
62
+ #
63
+ # The example above defines four bitmaps (2,3,4 and 6), and provides
64
+ # their bitmap number and description. The PAN field is variable length
65
+ # encoded (LL length indicator, ASCII, contents numeric, ASCII) and the
66
+ # maximum length of the field is limited to 19 using options.
67
+ #
68
+ # The other fields are fixed length numeric ASCII fields (the length of the fields is
69
+ # indicated by the `:length` options.)
70
+ #
71
+ # This message may be used as follows in order to interpret a received message.:
72
+ #
73
+ # mes = MyMessage.parse inputData
74
+ # puts mes[2] # prints the PAN from the message.
75
+ #
76
+ # Constructing own messages works as follows:
77
+ #
78
+ # mes = MyMessage.new 1100
79
+ # mes[2]= 474747474747
80
+ # # Alternatively
81
+ # mes["Primary Account Number (PAN)"]= 4747474747
82
+ # mes[3] = 1234 # padding is added by the Field en/decoder
83
+ # mes["Amount (Transaction)"] = 100
84
+ # mes[6] = 200
85
+ #
86
+ # the convenience method bmp_alias may be used in defining the class in
87
+ # order to provide direct access to fields using methods:
88
+ #
89
+ # class MyMessage < Message
90
+ # (...)
91
+ # bmp 2, "Primary Account Number (PAN)", LLVAR_N, :max => 19
92
+ # (...)
93
+ # bmp_alias 2, :pan
94
+ # end
95
+ #
96
+ # this allows accessing fields in the following manner:
97
+ #
98
+ # mes = MyMessage.new 1100
99
+ # mes.pan = 474747474747
100
+ # puts mes.pan
101
+ # # Identical functionality to:
102
+ # mes[2]= 474747474747
103
+ # # or:
104
+ # mes["Primary Account Number (PAN)"]= 4747474747
105
+ #
106
+ # Most of the work in implementing a new set of message type lays in
107
+ # figuring out the correct fields to use defining the Message class via
108
+ # bmp.
109
+ #
110
+ class Message
111
+
112
+ # The value of the MTI (Message Type Indicator) of this message.
113
+ attr_reader :mti
114
+
115
+ # Instantiate a new instance of this type of Message
116
+ # optionally specifying an mti.
117
+ def initialize(mti = nil)
118
+ # values is an internal field used to collect all the
119
+ # bmp number | bmp name | field en/decoders | values
120
+ # which are set in this message.
121
+ @values = {}
122
+ self.mti = mti if mti
123
+ end
124
+
125
+ # Set the mti of the Message using either the actual value
126
+ # or the name of the message type that was defined using
127
+ # Message.mti
128
+ #
129
+ # === Example
130
+ # class MyMessage < Message
131
+ # (...)
132
+ # mti 1100, "Authorization Request Acquirer Gateway"
133
+ # end
134
+ #
135
+ # mes = MyMessage.new
136
+ # mes.mti = 1100 # or mes.mti = "Authorization Request Acquirer Gateway"
137
+ def mti=(value)
138
+ num, name = _get_mti_definition(value)
139
+ @mti = num
140
+ end
141
+
142
+ # Set a field in this message, `key` is either the
143
+ # bmp number or it's name.
144
+ # ===Example
145
+ #
146
+ # mes = BlaBlaMessage.new
147
+ # mes[2]=47474747 # bmp 2 is generally the PAN
148
+ # mes["Primary Account Number"]=47474747 # if thats what you called the field in Message.bmp.
149
+ def []=(key, value)
150
+ bmp_def = _get_definition key
151
+ bmp_def.value = value
152
+ @values[bmp_def.bmp] = bmp_def
153
+ end
154
+
155
+ # Retrieve the decoded value of the contents of a bitmap
156
+ # described either by the bitmap number or name.
157
+ #
158
+ # ===Example
159
+ #
160
+ # mes = BlaBlaMessage.parse someMessageBytes
161
+ # mes[2] # bmp 2 is generally the PAN
162
+ # mes["Primary Account Number"] # if thats what you called the field in Message.bmp.
163
+ def [](key)
164
+ bmp_def = _get_definition key
165
+ bmp = @values[bmp_def.bmp]
166
+ bmp ? bmp.value : nil
167
+ end
168
+
169
+ # Retrieve the byte representation of the bitmap.
170
+ def to_b
171
+ raise ISO8583Exception.new "no MTI set!" unless mti
172
+ mti_enc = self.class._mti_format.encode(mti)
173
+ mti_enc << body.join
174
+ end
175
+
176
+ # Returns a nicely formatted representation of this
177
+ # message.
178
+ def to_s
179
+ _mti_name = _get_mti_definition(mti)[1]
180
+ str = "MTI:#{mti} (#{_mti_name})\n\n"
181
+ _max = @values.values.max {|a,b|
182
+ a.name.length <=> b.name.length
183
+ }
184
+ _max_name = _max.name.length
185
+
186
+ @values.keys.sort.each{|bmp_num|
187
+ _bmp = @values[bmp_num]
188
+ str += ("%03d %#{_max_name}s : %s\n" % [bmp_num, _bmp.name, _bmp.value])
189
+ }
190
+ str
191
+ end
192
+
193
+
194
+ private
195
+
196
+ def body
197
+ bitmap = Bitmap.new
198
+ message = ""
199
+ @values.keys.sort.each do |bmp_num|
200
+ bitmap.set(bmp_num)
201
+ enc_value = @values[bmp_num].encode
202
+ message << enc_value
203
+ end
204
+ [ bitmap.to_bytes, message ]
205
+ end
206
+
207
+ def _get_definition(key) #:nodoc:
208
+ b = self.class._definitions[key]
209
+ unless b
210
+ raise ISO8583Exception.new "no definition for field: #{key}"
211
+ end
212
+ b
213
+ end
214
+
215
+ # return [mti_num, mti_value] for key being either
216
+ # mti_num or mti_value
217
+ def _get_mti_definition(key)
218
+ num_hash,name_hash = self.class._mti_definitions
219
+ if num_hash[key]
220
+ [key, num_hash[key]]
221
+ elsif name_hash[key]
222
+ [name_hash[key], key]
223
+ else
224
+ raise ISO8583Exception.new("MTI: #{key} not allowed!")
225
+ end
226
+ end
227
+
228
+ class << self
229
+
230
+ # Defines how the message type indicator is encoded into bytes.
231
+ # ===Params:
232
+ # * field : the decoder/encoder for the MTI
233
+ # * opts : the options to pass to this field
234
+ #
235
+ # === Example
236
+ # class MyMessage < Message
237
+ # mti_format N, :length =>4
238
+ # (...)
239
+ # end
240
+ #
241
+ # encodes the mti of this message using the `N` field (fixed
242
+ # length, plain ASCII) and sets the fixed lengh to 4 bytes.
243
+ #
244
+ # See also: mti
245
+ def mti_format(field, opts)
246
+ f = field.dup
247
+ _handle_opts(f, opts)
248
+ @mti_format = f
249
+ end
250
+
251
+ # Defines the message types allowed for this type of message and
252
+ # gives them names
253
+ #
254
+ # === Example
255
+ # class MyMessage < Message
256
+ # (...)
257
+ # mti 1100, "Authorization Request Acquirer Gateway"
258
+ # end
259
+ #
260
+ # mes = MyMessage.new
261
+ # mes.mti = 1100 # or mes.mti = "Authorization Request Acquirer Gateway"
262
+ #
263
+ # See Also: mti_format
264
+ def mti(value, name)
265
+ @mtis_v ||= {}
266
+ @mtis_n ||= {}
267
+ @mtis_v[value] = name
268
+ @mtis_n[name] = value
269
+ end
270
+
271
+ # Define a bitmap in the message
272
+ # ===Params:
273
+ # * bmp : bitmap number
274
+ # * name : human readable form
275
+ # * field : field for encoding/decoding
276
+ # * opts : options to pass to the field, e.g. length for fxed len fields.
277
+ #
278
+ # ===Example
279
+ #
280
+ # class MyMessage < Message
281
+ # bmp 2, "PAN", LLVAR_N, :max =>19
282
+ # (...)
283
+ # end
284
+ #
285
+ # creates a class MyMessage that allows for a bitmap 2 which
286
+ # is named "PAN" and encoded by an LLVAR_N Field. The maximum
287
+ # length of the value is 19. This class may be used as follows:
288
+ #
289
+ # mes = MyMessage.new
290
+ # mes[2] = 474747474747 # or mes["PAN"] = 4747474747
291
+ #
292
+ def bmp(bmp, name, field, opts = nil)
293
+ @defs ||= {}
294
+
295
+ field = field.dup
296
+ field.name = name
297
+ field.bmp = bmp
298
+ _handle_opts(field, opts) if opts
299
+
300
+ bmp_def = BMP.new bmp, name, field
301
+
302
+ @defs[bmp] = bmp_def
303
+ @defs[name] = bmp_def
304
+ end
305
+
306
+ # Create an alias to access bitmaps directly using a method.
307
+ # Example:
308
+ # class MyMessage < Message
309
+ # (...)
310
+ # bmp 2, "PAN", LLVAR_N
311
+ # (...)
312
+ # bmp_alias 2, :pan
313
+ # end #class
314
+ #
315
+ # would allow you to access the PAN like this:
316
+ #
317
+ # mes.pan = 1234
318
+ # puts mes.pan
319
+ #
320
+ # instead of:
321
+ #
322
+ # mes[2] = 1234
323
+ #
324
+ def bmp_alias(bmp, aliaz)
325
+ define_method (aliaz) {
326
+ bmp_ = @values[bmp]
327
+ bmp_ ? bmp_.value : nil
328
+ }
329
+
330
+ define_method ("#{aliaz}=") {|value|
331
+ self[bmp] = value
332
+ # bmp_def = _get_definition(bmp)
333
+ # bmp_def.value= value
334
+ # @values[bmp] = bmp_def
335
+ }
336
+ end
337
+
338
+ # Parse the bytes `str` returning a message of the defined type.
339
+ def parse(str)
340
+ message = self.new
341
+ message.mti, rest = _mti_format.parse(str)
342
+ bmp,rest = Bitmap.parse(rest)
343
+ bmp.each {|bit|
344
+ bmp_def = _definitions[bit]
345
+ value, rest = bmp_def.field.parse(rest)
346
+ message[bit] = value
347
+ }
348
+ message
349
+ end
350
+
351
+ # access the mti definitions applicable to the Message
352
+ #
353
+ # returns a pair of hashes containing:
354
+ #
355
+ # mti_value => mti_name
356
+ #
357
+ # mti_name => mti_value
358
+ #
359
+ def _mti_definitions
360
+ [@mtis_v, @mtis_n]
361
+ end
362
+
363
+ # Access the field definitions of this class, this is a
364
+ # hash containing [bmp_number, BMP] and [bitmap_name, BMP]
365
+ # pairs.
366
+ #
367
+ def _definitions
368
+ @defs
369
+ end
370
+
371
+ # Returns the field definition to format the mti.
372
+ def _mti_format
373
+ @mti_format
374
+ end
375
+
376
+
377
+ private
378
+
379
+ # METHODS starting with an underscore are meant for
380
+ # internal use only ...
381
+
382
+ # Modifies the field definitions of the fields passed
383
+ # in through the `bmp` and `mti_format` class methods.
384
+ #
385
+ def _handle_opts(field, opts)
386
+ opts.each_pair {|key, value|
387
+ key = (key.to_s+"=").to_sym
388
+ if field.respond_to?(key)
389
+ field.send(key, value)
390
+ else
391
+ warn "unknown option #{key} for #{field.name}"
392
+ end
393
+ }
394
+ end
395
+ end
396
+ end
397
+
398
+ # Internal class used to tie together name, bitmap number, field en/decoder
399
+ # and the value of the corresponding field
400
+ class BMP
401
+ attr_accessor :bmp
402
+ attr_accessor :name
403
+ attr_accessor :field
404
+ attr_accessor :value
405
+
406
+ def initialize(bmp, name, field)
407
+ @bmp = bmp
408
+ @name = name
409
+ @field = field
410
+ end
411
+
412
+ def encode
413
+ field.encode(value)
414
+ end
415
+ end
416
+
417
+ end