cordawyn-iso8583 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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