segfault-ruby-hl7 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ = License
2
+ Permission is hereby granted, free of charge, to any person obtaining
3
+ a copy of this software and associated documentation files (the
4
+ "Software"), to deal in the Software without restriction, including
5
+ without limitation the rights to use, copy, modify, merge, publish,
6
+ distribute, sublicense, and/or sell copies of the Software, and to
7
+ permit persons to whom the Software is furnished to do so, subject to
8
+ the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be
11
+ included in all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
17
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
19
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,24 @@
1
+ =Ruby HL7 Library README
2
+
3
+ A simple way to parse and create hl7 2.x messages with ruby.
4
+ Examples can be found in HL7::Message
5
+ The version id can be found in the HL7::VERSION constant.
6
+
7
+ * Bug tracking: http://trac.hasno.info/ruby-hl7
8
+ * Subversion: svn://hasno.info/ruby-hl7
9
+ * Git: git://github.com/segfault/ruby-hl7.git
10
+ * Docs: http://ruby-hl7.rubyforge.org
11
+ * Rubyforge: http://rubyforge.org/projects/ruby-hl7
12
+ * Lists
13
+ * Developers: mailto:ruby-hl7-devel@rubyforge.org
14
+ * Users: mailto:ruby-hl7-users@rubyforge.org
15
+
16
+ Copyright (c) 2006-2008 Mark Guzman
17
+
18
+ == Download and Installation
19
+ Install the gem using the following command:
20
+ gem install ruby-hl7
21
+
22
+
23
+ == License
24
+ see the LICENSE file
data/lib/ruby-hl7.rb ADDED
@@ -0,0 +1,644 @@
1
+ # encoding: UTF-8
2
+ #= ruby-hl7.rb
3
+ # Ruby HL7 is designed to provide a simple, easy to use library for
4
+ # parsing and generating HL7 (2.x) messages.
5
+ #
6
+ #
7
+ # Author: Mark Guzman (mailto:segfault@hasno.info)
8
+ #
9
+ # Copyright: (c) 2006-2009 Mark Guzman
10
+ #
11
+ # License: BSD
12
+ #
13
+ # $Id$
14
+ #
15
+ # == License
16
+ # see the LICENSE file
17
+ #
18
+
19
+ require 'rubygems'
20
+ require "stringio"
21
+ require "date"
22
+
23
+ module HL7 # :nodoc:
24
+ VERSION = "0.3"
25
+ def self.ParserConfig
26
+ @parser_cfg ||= { :empty_segment_is_error => true }
27
+ end
28
+ end
29
+
30
+ # Encapsulate HL7 specific exceptions
31
+ class HL7::Exception < StandardError
32
+ end
33
+
34
+ # Parsing failed
35
+ class HL7::ParseError < HL7::Exception
36
+ end
37
+
38
+ # Attempting to use an invalid indice
39
+ class HL7::RangeError < HL7::Exception
40
+ end
41
+
42
+ # Attempting to assign invalid data to a field
43
+ class HL7::InvalidDataError < HL7::Exception
44
+ end
45
+
46
+ # Ruby Object representation of an hl7 2.x message
47
+ # the message object is actually a "smart" collection of hl7 segments
48
+ # == Examples
49
+ #
50
+ # ==== Creating a new HL7 message
51
+ #
52
+ # # create a message
53
+ # msg = HL7::Message.new
54
+ #
55
+ # # create a MSH segment for our new message
56
+ # msh = HL7::Message::Segment::MSH.new
57
+ # msh.recv_app = "ruby hl7"
58
+ # msh.recv_facility = "my office"
59
+ # msh.processing_id = rand(10000).to_s
60
+ #
61
+ # msg << msh # add the MSH segment to the message
62
+ #
63
+ # puts msg.to_s # readable version of the message
64
+ #
65
+ # puts msg.to_hl7 # hl7 version of the message (as a string)
66
+ #
67
+ # puts msg.to_mllp # mllp version of the message (as a string)
68
+ #
69
+ # ==== Parse an existing HL7 message
70
+ #
71
+ # raw_input = open( "my_hl7_msg.txt" ).readlines
72
+ # msg = HL7::Message.new( raw_input )
73
+ #
74
+ # puts "message type: %s" % msg[:MSH].message_type
75
+ #
76
+ #
77
+ class HL7::Message
78
+ include Enumerable # we treat an hl7 2.x message as a collection of segments
79
+ attr :element_delim
80
+ attr :item_delim
81
+ attr :segment_delim
82
+
83
+ # setup a new hl7 message
84
+ # raw_msg:: is an optional object containing an hl7 message
85
+ # it can either be a string or an Enumerable object
86
+ def initialize( raw_msg=nil, &blk )
87
+ @segments = []
88
+ @segments_by_name = {}
89
+ @item_delim = "^"
90
+ @element_delim = '|'
91
+ @segment_delim = "\r"
92
+
93
+ parse( raw_msg ) if raw_msg
94
+
95
+ if block_given?
96
+ blk.call self
97
+ end
98
+ end
99
+
100
+ # access a segment of the message
101
+ # index:: can be a Range, Fixnum or anything that
102
+ # responds to to_sym
103
+ def []( index )
104
+ ret = nil
105
+
106
+ if index.kind_of?(Range) || index.kind_of?(Fixnum)
107
+ ret = @segments[ index ]
108
+ elsif (index.respond_to? :to_sym)
109
+ ret = @segments_by_name[ index.to_sym ]
110
+ ret = ret.first if ret && ret.length == 1
111
+ end
112
+
113
+ ret
114
+ end
115
+
116
+ # modify a segment of the message
117
+ # index:: can be a Range, Fixnum or anything that
118
+ # responds to to_sym
119
+ # value:: an HL7::Message::Segment object
120
+ def []=( index, value )
121
+ unless ( value && value.kind_of?(HL7::Message::Segment) )
122
+ raise HL7::Exception.new( "attempting to assign something other than an HL7 Segment" )
123
+ end
124
+
125
+ if index.kind_of?(Range) || index.kind_of?(Fixnum)
126
+ @segments[ index ] = value
127
+ elsif index.respond_to?(:to_sym)
128
+ (@segments_by_name[ index.to_sym ] ||= []) << value
129
+ else
130
+ raise HL7::Exception.new( "attempting to use an indice that is not a Range, Fixnum or to_sym providing object" )
131
+ end
132
+
133
+ value.segment_parent = self
134
+ end
135
+
136
+ # return the index of the value if it exists, nil otherwise
137
+ # value:: is expected to be a string
138
+ def index( value )
139
+ return nil unless (value && value.respond_to?(:to_sym))
140
+
141
+ segs = @segments_by_name[ value.to_sym ]
142
+ return nil unless segs
143
+
144
+ @segments.index( segs.to_a.first )
145
+ end
146
+
147
+ # add a segment to the message
148
+ # * will force auto set_id sequencing for segments containing set_id's
149
+ def <<( value )
150
+ unless ( value && value.kind_of?(HL7::Message::Segment) )
151
+ raise HL7::Exception.new( "attempting to append something other than an HL7 Segment" )
152
+ end
153
+
154
+ value.segment_parent = self unless value.segment_parent
155
+ (@segments ||= []) << value
156
+ name = value.class.to_s.gsub("HL7::Message::Segment::", "").to_sym
157
+ (@segments_by_name[ name ] ||= []) << value
158
+ sequence_segments unless @parsing # let's auto-set the set-id as we go
159
+ end
160
+
161
+ # parse a String or Enumerable object into an HL7::Message if possible
162
+ # * returns a new HL7::Message if successful
163
+ def self.parse( inobj )
164
+ HL7::Message.new do |msg|
165
+ msg.parse( inobj )
166
+ end
167
+ end
168
+
169
+ # parse the provided String or Enumerable object into this message
170
+ def parse( inobj )
171
+ unless inobj.kind_of?(String) || inobj.respond_to?(:each)
172
+ raise HL7::ParseError.new
173
+ end
174
+
175
+ if inobj.kind_of?(String)
176
+ parse_string( inobj )
177
+ elsif inobj.respond_to?(:each)
178
+ parse_enumerable( inobj )
179
+ end
180
+ end
181
+
182
+ # yield each segment in the message
183
+ def each # :yeilds: segment
184
+ return unless @segments
185
+ @segments.each { |s| yield s }
186
+ end
187
+
188
+ # return the segment count
189
+ def length
190
+ 0 unless @segments
191
+ @segments.length
192
+ end
193
+
194
+ # provide a screen-readable version of the message
195
+ def to_s
196
+ @segments.collect { |s| s if s.to_s.length > 0 }.join( "\n" )
197
+ end
198
+
199
+ # provide a HL7 spec version of the message
200
+ def to_hl7
201
+ @segments.collect { |s| s if s.to_s.length > 0 }.join( @segment_delim )
202
+ end
203
+
204
+ # provide the HL7 spec version of the message wrapped in MLLP
205
+ def to_mllp
206
+ pre_mllp = to_hl7
207
+ "\x0b" + pre_mllp + "\x1c\r"
208
+ end
209
+
210
+ # auto-set the set_id fields of any message segments that
211
+ # provide it and have more than one instance in the message
212
+ def sequence_segments(base=nil)
213
+ last = nil
214
+ segs = @segments
215
+ segs = base.children if base
216
+
217
+ segs.each do |s|
218
+ if s.kind_of?( last.class ) && s.respond_to?( :set_id )
219
+ last.set_id = 1 unless last.set_id && last.set_id.to_i > 0
220
+ s.set_id = last.set_id.to_i + 1
221
+ end
222
+
223
+ if s.respond_to?(:children)
224
+ sequence_segments( s )
225
+ end
226
+
227
+ last = s
228
+ end
229
+ end
230
+
231
+ private
232
+ # Get the element delimiter from an MSH segment
233
+ def parse_element_delim(str)
234
+ (str && str.kind_of?(String)) ? str.slice(3,1) : "|"
235
+ end
236
+
237
+ # Get the item delimiter from an MSH segment
238
+ def parse_item_delim(str)
239
+ (str && str.kind_of?(String)) ? str.slice(4,1) : "^"
240
+ end
241
+
242
+ def parse_enumerable( inary )
243
+ #assumes an enumeration of strings....
244
+ inary.each do |oary|
245
+ parse_string( oary.to_s )
246
+ end
247
+ end
248
+
249
+ def parse_string( instr )
250
+ post_mllp = instr
251
+ if /\x0b((:?.|\r|\n)+)\x1c\r/.match( instr )
252
+ post_mllp = $1 #strip the mllp bytes
253
+ end
254
+
255
+ ary = post_mllp.split( segment_delim, -1 )
256
+ generate_segments( ary )
257
+ end
258
+
259
+ def generate_segments( ary )
260
+ raise HL7::ParseError.new unless ary.length > 0
261
+
262
+ @parsing = true
263
+ last_seg = nil
264
+ ary.each do |elm|
265
+ if elm.slice(0,3) == "MSH"
266
+ @item_delim = parse_item_delim(elm)
267
+ @element_delim = parse_element_delim(elm)
268
+ end
269
+ last_seg = generate_segment( elm, last_seg ) || last_seg
270
+ end
271
+ @parsing = nil
272
+ end
273
+
274
+ def generate_segment( elm, last_seg )
275
+ seg_parts = elm.split( @element_delim, -1 )
276
+ unless seg_parts && (seg_parts.length > 0)
277
+ raise HL7::ParseError.new if HL7.ParserConfig[:empty_segment_is_error] || false
278
+ return nil
279
+ end
280
+
281
+ seg_name = seg_parts[0]
282
+ if RUBY_VERSION < "1.9" && HL7::Message::Segment.constants.index(seg_name) # do we have an implementation?
283
+ kls = eval("HL7::Message::Segment::%s" % seg_name)
284
+ elsif RUBY_VERSION >= "1.9" && HL7::Message::Segment.constants.index(seg_name.to_sym)
285
+ kls = eval("HL7::Message::Segment::%s" % seg_name)
286
+ else
287
+ # we don't have an implementation for this segment
288
+ # so lets just preserve the data
289
+ kls = HL7::Message::Segment::Default
290
+ end
291
+ new_seg = kls.new( elm, [@element_delim, @item_delim] )
292
+ new_seg.segment_parent = self
293
+
294
+ if last_seg && last_seg.respond_to?(:children) && last_seg.accepts?( seg_name )
295
+ last_seg.children << new_seg
296
+ new_seg.is_child_segment = true
297
+ return last_seg
298
+ end
299
+
300
+ @segments << new_seg
301
+
302
+ # we want to allow segment lookup by name
303
+ if seg_name && (seg_name.strip.length > 0)
304
+ seg_sym = seg_name.to_sym
305
+ @segments_by_name[ seg_sym ] ||= []
306
+ @segments_by_name[ seg_sym ] << new_seg
307
+ end
308
+
309
+ new_seg
310
+ end
311
+ end
312
+
313
+ # Ruby Object representation of an hl7 2.x message segment
314
+ # The segments can be setup to provide aliases to specific fields with
315
+ # optional validation code that is run when the field is modified
316
+ # The segment field data is also accessible via the e<number> method.
317
+ #
318
+ # == Defining a New Segment
319
+ # class HL7::Message::Segment::NK1 < HL7::Message::Segment
320
+ # wieght 100 # segments are sorted ascendingly
321
+ # add_field :something_you_want # assumes :idx=>1
322
+ # add_field :something_else, :idx=>6 # :idx=>6 and field count=6
323
+ # add_field :something_more # :idx=>7
324
+ # add_field :block_example do |value|
325
+ # raise HL7::InvalidDataError.new unless value.to_i < 100 && value.to_i > 10
326
+ # return value
327
+ # end
328
+ # # this block will be executed when seg.block_example= is called
329
+ # # and when seg.block_example is called
330
+ #
331
+ class HL7::Message::Segment
332
+ attr :segment_parent, true
333
+ attr :element_delim
334
+ attr :item_delim
335
+ attr :segment_weight
336
+
337
+ # setup a new HL7::Message::Segment
338
+ # raw_segment:: is an optional String or Array which will be used as the
339
+ # segment's field data
340
+ # delims:: an optional array of delimiters, where
341
+ # delims[0] = element delimiter
342
+ # delims[1] = item delimiter
343
+ def initialize(raw_segment="", delims=[], &blk)
344
+ @segments_by_name = {}
345
+ @field_total = 0
346
+ @is_child = false
347
+
348
+ @element_delim = (delims.kind_of?(Array) && delims.length>0) ? delims[0] : "|"
349
+ @item_delim = (delims.kind_of?(Array) && delims.length>1) ? delims[1] : "^"
350
+
351
+ if (raw_segment.kind_of? Array)
352
+ @elements = raw_segment
353
+ else
354
+ @elements = raw_segment.split( @element_delim, -1 )
355
+ if raw_segment == ""
356
+ @elements[0] = self.class.to_s.split( "::" ).last
357
+ @elements << ""
358
+ end
359
+ end
360
+
361
+ if block_given?
362
+ callctx = eval( "self", blk.binding )
363
+ def callctx.__seg__(val=nil)
364
+ @__seg_val__ ||= val
365
+ end
366
+ callctx.__seg__(self)
367
+ # TODO: find out if this pollutes the calling namespace permanently...
368
+
369
+ to_do = <<-END
370
+ def method_missing( sym, *args, &blk )
371
+ __seg__.send( sym, args, blk )
372
+ end
373
+ END
374
+
375
+ eval( to_do, blk.binding )
376
+ yield self
377
+ eval( "undef method_missing", blk.binding )
378
+ end
379
+ end
380
+
381
+ def to_info
382
+ "%s: empty segment >> %s" % [ self.class.to_s, @elements.inspect ]
383
+ end
384
+
385
+ # output the HL7 spec version of the segment
386
+ def to_s
387
+ @elements.join( @element_delim )
388
+ end
389
+
390
+ # at the segment level there is no difference between to_s and to_hl7
391
+ alias :to_hl7 :to_s
392
+
393
+ # handle the e<number> field accessor
394
+ # and any aliases that didn't get added to the system automatically
395
+ def method_missing( sym, *args, &blk )
396
+ base_str = sym.to_s.gsub( "=", "" )
397
+ base_sym = base_str.to_sym
398
+
399
+ if self.class.fields.include?( base_sym )
400
+ # base_sym is ok, let's move on
401
+ elsif /e([0-9]+)/.match( base_str )
402
+ # base_sym should actually be $1, since we're going by
403
+ # element id number
404
+ base_sym = $1.to_i
405
+ else
406
+ super
407
+ end
408
+
409
+ if sym.to_s.include?( "=" )
410
+ write_field( base_sym, args )
411
+ else
412
+
413
+ if args.length > 0
414
+ write_field( base_sym, args.flatten.select { |arg| arg } )
415
+ else
416
+ read_field( base_sym )
417
+ end
418
+
419
+ end
420
+ end
421
+
422
+ # sort-compare two Segments, 0 indicates equality
423
+ def <=>( other )
424
+ return nil unless other.kind_of?(HL7::Message::Segment)
425
+
426
+ # per Comparable docs: http://www.ruby-doc.org/core/classes/Comparable.html
427
+ diff = self.weight - other.weight
428
+ return -1 if diff > 0
429
+ return 1 if diff < 0
430
+ return 0
431
+ end
432
+
433
+ # get the defined sort-weight of this segment class
434
+ # an alias for self.weight
435
+ def weight
436
+ self.class.weight
437
+ end
438
+
439
+
440
+ # return true if the segment has a parent
441
+ def is_child_segment?
442
+ (@is_child_segment ||= false)
443
+ end
444
+
445
+ # indicate whether or not the segment has a parent
446
+ def is_child_segment=(val)
447
+ @is_child_segment = val
448
+ end
449
+
450
+ # get the length of the segment (number of fields it contains)
451
+ def length
452
+ 0 unless @elements
453
+ @elements.length
454
+ end
455
+
456
+
457
+ private
458
+ def self.singleton #:nodoc:
459
+ class << self; self end
460
+ end
461
+
462
+ # DSL element to define a segment's sort weight
463
+ # returns the segment's current weight by default
464
+ # segments are sorted ascending
465
+ def self.weight(new_weight=nil)
466
+ if new_weight
467
+ singleton.module_eval do
468
+ @my_weight = new_weight
469
+ end
470
+ end
471
+
472
+ singleton.module_eval do
473
+ return 999 unless @my_weight
474
+ @my_weight
475
+ end
476
+ end
477
+
478
+
479
+
480
+ # allows a segment to store other segment objects
481
+ # used to handle associated lists like one OBR to many OBX segments
482
+ def self.has_children(child_types)
483
+ @child_types = child_types
484
+ define_method(:child_types) do
485
+ @child_types
486
+ end
487
+
488
+ self.class_eval do
489
+ define_method(:children) do
490
+ unless @my_children
491
+ p = self
492
+ @my_children ||= []
493
+ @my_children.instance_eval do
494
+ @parental = p
495
+ alias :old_append :<<
496
+
497
+ def <<(value)
498
+ unless (value && value.kind_of?(HL7::Message::Segment))
499
+ raise HL7::Exception.new( "attempting to append non-segment to a segment list" )
500
+ end
501
+
502
+ value.segment_parent = @parental
503
+ k = @parental
504
+ while (k && k.segment_parent && !k.segment_parent.kind_of?(HL7::Message))
505
+ k = k.segment_parent
506
+ end
507
+ k.segment_parent << value if k && k.segment_parent
508
+ old_append( value )
509
+ end
510
+ end
511
+ end
512
+
513
+ @my_children
514
+ end
515
+
516
+ define_method('accepts?') do |t|
517
+ t = t.to_sym if t && (t.to_s.length > 0) && t.respond_to?(:to_sym)
518
+ child_types.index t
519
+ end
520
+ end
521
+ end
522
+
523
+ # define a field alias
524
+ # * name is the alias itself (required)
525
+ # * options is a hash of parameters
526
+ # * :id is the field number to reference (optional, auto-increments from 1
527
+ # by default)
528
+ # * :blk is a validation proc (optional, overrides the second argument)
529
+ # * blk is an optional validation proc which MUST take a parameter
530
+ # and always return a value for the field (it will be used on read/write
531
+ # calls)
532
+ def self.add_field( name, options={}, &blk )
533
+ options = { :idx =>-1, :blk =>blk}.merge!( options )
534
+ name ||= :id
535
+ namesym = name.to_sym
536
+ @field_cnt ||= 1
537
+ if options[:idx] == -1
538
+ options[:idx] = @field_cnt # provide default auto-incrementing
539
+ end
540
+ @field_cnt = options[:idx].to_i + 1
541
+
542
+ singleton.module_eval do
543
+ @fields ||= {}
544
+ @fields[ namesym ] = options
545
+ end
546
+
547
+ self.class_eval <<-END
548
+ def #{name}(val=nil)
549
+ unless val
550
+ read_field( :#{namesym} )
551
+ else
552
+ write_field( :#{namesym}, val )
553
+ val # this matches existing n= method functionality
554
+ end
555
+ end
556
+
557
+ def #{name}=(value)
558
+ write_field( :#{namesym}, value )
559
+ end
560
+ END
561
+ end
562
+
563
+ def self.fields #:nodoc:
564
+ singleton.module_eval do
565
+ (@fields ||= [])
566
+ end
567
+ end
568
+
569
+ def field_info( name ) #:nodoc:
570
+ field_blk = nil
571
+ idx = name # assume we've gotten a fixnum
572
+ unless name.kind_of?( Fixnum )
573
+ fld_info = self.class.fields[ name ]
574
+ idx = fld_info[:idx].to_i
575
+ field_blk = fld_info[:blk]
576
+ end
577
+
578
+ [ idx, field_blk ]
579
+ end
580
+
581
+ def read_field( name ) #:nodoc:
582
+ idx, field_blk = field_info( name )
583
+ return nil unless idx
584
+ return nil if (idx >= @elements.length)
585
+
586
+ ret = @elements[ idx ]
587
+ ret = ret.first if (ret.kind_of?(Array) && ret.length == 1)
588
+ ret = field_blk.call( ret ) if field_blk
589
+ ret
590
+ end
591
+
592
+ def write_field( name, value ) #:nodoc:
593
+ idx, field_blk = field_info( name )
594
+ return nil unless idx
595
+
596
+ if (idx >= @elements.length)
597
+ # make some space for the incoming field, missing items are assumed to
598
+ # be empty, so this is valid per the spec -mg
599
+ missing = ("," * (idx-@elements.length)).split(',',-1)
600
+ @elements += missing
601
+ end
602
+
603
+ value = value.first if (value && value.kind_of?(Array) && value.length == 1)
604
+ value = field_blk.call( value ) if field_blk
605
+ @elements[ idx ] = value.to_s
606
+ end
607
+
608
+ @elements = []
609
+
610
+ end
611
+
612
+ # parse an hl7 formatted date
613
+ #def Date.from_hl7( hl7_date )
614
+ #end
615
+
616
+ #def Date.to_hl7_short( ruby_date )
617
+ #end
618
+
619
+ #def Date.to_hl7_med( ruby_date )
620
+ #end
621
+
622
+ #def Date.to_hl7_long( ruby_date )
623
+ #end
624
+
625
+ # Provide a catch-all information preserving segment
626
+ # * nb: aliases are not provided BUT you can use the numeric element accessor
627
+ #
628
+ # seg = HL7::Message::Segment::Default.new
629
+ # seg.e0 = "NK1"
630
+ # seg.e1 = "SOMETHING ELSE"
631
+ # seg.e2 = "KIN HERE"
632
+ #
633
+ class HL7::Message::Segment::Default < HL7::Message::Segment
634
+ def initialize(raw_segment="", delims=[])
635
+ segs = [] if (raw_segment == "")
636
+ segs ||= raw_segment
637
+ super( segs, delims )
638
+ end
639
+ end
640
+
641
+ # load our segments
642
+ Dir["#{File.dirname(__FILE__)}/segments/*.rb"].each { |ext| load ext }
643
+
644
+ # vim:tw=78:sw=2:ts=2:et:fdm=marker: