ruby-hl7 0.3 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/README.rdoc +40 -0
  2. data/lib/ruby-hl7.rb +112 -69
  3. data/lib/segments/err.rb +12 -0
  4. data/lib/segments/evn.rb +1 -1
  5. data/lib/segments/msa.rb +2 -2
  6. data/lib/segments/msh.rb +4 -1
  7. data/lib/segments/nk1.rb +14 -0
  8. data/lib/segments/nte.rb +1 -1
  9. data/lib/segments/obr.rb +6 -4
  10. data/lib/segments/obx.rb +2 -1
  11. data/lib/segments/orc.rb +34 -0
  12. data/lib/segments/oru.rb +1 -1
  13. data/lib/segments/pid.rb +3 -2
  14. data/lib/segments/pv1.rb +1 -1
  15. data/lib/segments/pv2.rb +1 -1
  16. data/lib/segments/qrd.rb +1 -1
  17. data/lib/segments/qrf.rb +1 -1
  18. data/lib/segments/sft.rb +15 -0
  19. data/lib/segments/spm.rb +35 -0
  20. data/lib/string.rb +5 -0
  21. data/lib/test/hl7_messages.rb +309 -0
  22. data/test_data/cerner/cerner_bordetella.hl7 +1 -0
  23. data/test_data/cerner/cerner_en.hl7 +1 -0
  24. data/test_data/cerner/cerner_lead.hl7 +1 -0
  25. data/test_data/cerner/cerner_sequential.hl7 +1 -0
  26. data/test_data/empty-batch.hl7 +1 -0
  27. data/test_data/nist/ORU_R01_2.5.1_SampleTestCase1.er7 +1 -0
  28. data/test_data/nist/ORU_R01_2.5.1_SampleTestCase2.er7 +1 -0
  29. data/test_data/nist/ORU_R01_2.5.1_SampleTestCase3.er7 +1 -0
  30. data/test_data/nist/ORU_R01_2.5.1_SampleTestCase4.er7 +1 -0
  31. data/test_data/nist/ORU_R01_2.5.1_SampleTestCase5.er7 +1 -0
  32. data/test_data/nist/ORU_R01_2.5.1_SampleTestCase6.er7 +1 -0
  33. data/test_data/realm/realm-animal-rabies.hl7 +1 -0
  34. data/test_data/realm/realm-bad-batch.hl7 +1 -0
  35. data/test_data/realm/realm-batch.hl7 +1 -0
  36. data/test_data/realm/realm-campylobacter-jejuni.hl7 +1 -0
  37. data/test_data/realm/realm-cj-badloinc.hl7 +1 -0
  38. data/test_data/realm/realm-cj-joeslab.hl7 +1 -0
  39. data/test_data/realm/realm-cj.hl7 +1 -0
  40. data/test_data/realm/realm-err.hl7 +1 -0
  41. data/test_data/realm/realm-hepatitis-c-virus.hl7 +1 -0
  42. data/test_data/realm/realm-lead-laboratory-result.hl7 +2 -0
  43. data/test_data/realm/realm-minimal-message.hl7 +1 -0
  44. metadata +85 -39
  45. data/README +0 -24
  46. data/test/test_basic_parsing.rb +0 -337
  47. data/test/test_child_segment.rb +0 -54
  48. data/test/test_default_segment.rb +0 -31
  49. data/test/test_dynamic_segment_def.rb +0 -42
  50. data/test/test_msa_segment.rb +0 -27
  51. data/test/test_obr_segment.rb +0 -29
  52. data/test/test_obx_segment.rb +0 -27
  53. data/test/test_pid_segment.rb +0 -28
  54. data/test/test_speed_parsing.rb +0 -19
data/README.rdoc ADDED
@@ -0,0 +1,40 @@
1
+ = Ruby HL7 Library README
2
+
3
+ A simple way to parse and create HL7 2.x messages with Ruby.
4
+
5
+ Examples can be found in HL7::Message.
6
+
7
+ The version id can be found in the HL7::VERSION constant.
8
+
9
+ * Git: http://github.com/ruby-hl7/ruby-hl7.git
10
+ * Docs: http://ruby-hl7.rubyforge.org
11
+ * Rubyforge: http://rubyforge.org/projects/ruby-hl7
12
+
13
+ Lists
14
+
15
+ * Developers: mailto:ruby-hl7-devel@rubyforge.org
16
+ * Users: mailto:ruby-hl7-users@rubyforge.org
17
+ * Google group: http://groups.google.com/group/ruby-hl7
18
+
19
+ Copyright (C) 2006-2010 Mark Guzman
20
+
21
+ Maintained by the Collaborative Software Initiative
22
+
23
+ * http://csinitiative.com
24
+ * http://trisano.org
25
+ * http://github.com/csinitiative/trisano
26
+
27
+ == Download and Installation
28
+
29
+ === Rubygems
30
+ Install the gem using the following command:
31
+
32
+ gem install ruby-hl7
33
+
34
+ === Bundler
35
+ In your Gemfile:
36
+
37
+ gem 'ruby-hl7'
38
+
39
+ == License
40
+ See the LICENSE file.
data/lib/ruby-hl7.rb CHANGED
@@ -1,18 +1,19 @@
1
+ # encoding: UTF-8
1
2
  #= ruby-hl7.rb
2
3
  # Ruby HL7 is designed to provide a simple, easy to use library for
3
4
  # parsing and generating HL7 (2.x) messages.
4
5
  #
5
6
  #
6
- # Author: Mark Guzman (mailto:segfault@hasno.info)
7
+ # Author: Mark Guzman (mailto:segfault@hasno.info)
7
8
  #
8
- # Copyright: (c) 2006-2007 Mark Guzman
9
+ # Copyright: (c) 2006-2009 Mark Guzman
9
10
  #
10
- # License: BSD
11
+ # License: BSD
11
12
  #
12
13
  # $Id$
13
- #
14
+ #
14
15
  # == License
15
- # see the LICENSE file
16
+ # see the LICENSE file
16
17
  #
17
18
 
18
19
  require 'rubygems'
@@ -20,7 +21,7 @@ require "stringio"
20
21
  require "date"
21
22
 
22
23
  module HL7 # :nodoc:
23
- VERSION = "0.3"
24
+ VERSION = "1.0"
24
25
  def self.ParserConfig
25
26
  @parser_cfg ||= { :empty_segment_is_error => true }
26
27
  end
@@ -45,32 +46,32 @@ end
45
46
  # Ruby Object representation of an hl7 2.x message
46
47
  # the message object is actually a "smart" collection of hl7 segments
47
48
  # == Examples
48
- #
49
+ #
49
50
  # ==== Creating a new HL7 message
50
- #
51
+ #
51
52
  # # create a message
52
53
  # msg = HL7::Message.new
53
- #
54
+ #
54
55
  # # create a MSH segment for our new message
55
56
  # msh = HL7::Message::Segment::MSH.new
56
57
  # msh.recv_app = "ruby hl7"
57
58
  # msh.recv_facility = "my office"
58
59
  # msh.processing_id = rand(10000).to_s
59
- #
60
+ #
60
61
  # msg << msh # add the MSH segment to the message
61
- #
62
+ #
62
63
  # puts msg.to_s # readable version of the message
63
- #
64
+ #
64
65
  # puts msg.to_hl7 # hl7 version of the message (as a string)
65
- #
66
+ #
66
67
  # puts msg.to_mllp # mllp version of the message (as a string)
67
- #
68
- # ==== Parse an existing HL7 message
69
- #
68
+ #
69
+ # ==== Parse an existing HL7 message
70
+ #
70
71
  # raw_input = open( "my_hl7_msg.txt" ).readlines
71
72
  # msg = HL7::Message.new( raw_input )
72
- #
73
- # puts "message type: %s" % msg[:MSH].message_type
73
+ #
74
+ # puts "message type: %s" % msg[:MSH].message_type
74
75
  #
75
76
  #
76
77
  class HL7::Message
@@ -86,7 +87,7 @@ class HL7::Message
86
87
  @segments = []
87
88
  @segments_by_name = {}
88
89
  @item_delim = "^"
89
- @element_delim = '|'
90
+ @element_delim = '|'
90
91
  @segment_delim = "\r"
91
92
 
92
93
  parse( raw_msg ) if raw_msg
@@ -118,7 +119,7 @@ class HL7::Message
118
119
  # value:: an HL7::Message::Segment object
119
120
  def []=( index, value )
120
121
  unless ( value && value.kind_of?(HL7::Message::Segment) )
121
- raise HL7::Exception.new( "attempting to assign something other than an HL7 Segment" )
122
+ raise HL7::Exception.new( "attempting to assign something other than an HL7 Segment" )
122
123
  end
123
124
 
124
125
  if index.kind_of?(Range) || index.kind_of?(Fixnum)
@@ -136,7 +137,7 @@ class HL7::Message
136
137
  # value:: is expected to be a string
137
138
  def index( value )
138
139
  return nil unless (value && value.respond_to?(:to_sym))
139
-
140
+
140
141
  segs = @segments_by_name[ value.to_sym ]
141
142
  return nil unless segs
142
143
 
@@ -147,7 +148,7 @@ class HL7::Message
147
148
  # * will force auto set_id sequencing for segments containing set_id's
148
149
  def <<( value )
149
150
  unless ( value && value.kind_of?(HL7::Message::Segment) )
150
- raise HL7::Exception.new( "attempting to append something other than an HL7 Segment" )
151
+ raise HL7::Exception.new( "attempting to append something other than an HL7 Segment" )
151
152
  end
152
153
 
153
154
  value.segment_parent = self unless value.segment_parent
@@ -157,12 +158,42 @@ class HL7::Message
157
158
  sequence_segments unless @parsing # let's auto-set the set-id as we go
158
159
  end
159
160
 
160
- # parse a String or Enumerable object into an HL7::Message if possible
161
- # * returns a new HL7::Message if successful
162
- def self.parse( inobj )
163
- HL7::Message.new do |msg|
164
- msg.parse( inobj )
165
- end
161
+ class << self
162
+ def parse_batch(batch) # :yields: message
163
+ raise HL7::ParseError, 'badly_formed_batch_message' unless
164
+ batch.hl7_batch?
165
+
166
+ # JRuby seems to change our literal \r characters in sample
167
+ # messages (from here documents) into newlines. We make a copy
168
+ # here, reverting to carriage returns. The input is unchanged.
169
+ #
170
+ # This does not occur when posts are received with CR
171
+ # characters, only in sample messages from here documents. The
172
+ # expensive copy is only incurred when the batch message has a
173
+ # newline character in it.
174
+ batch = batch.gsub("\n", "\r") if batch.include?("\n")
175
+
176
+ raise HL7::ParseError, 'empty_batch_message' unless
177
+ match = /\rMSH/.match(batch)
178
+
179
+ match.post_match.split(/\rMSH/).each_with_index do |_msg, index|
180
+ if md = /\rBTS/.match(_msg)
181
+ # TODO: Validate the message count in the BTS segment
182
+ # should == index + 1
183
+ _msg = md.pre_match
184
+ end
185
+
186
+ yield 'MSH' + _msg
187
+ end
188
+ end
189
+
190
+ # parse a String or Enumerable object into an HL7::Message if possible
191
+ # * returns a new HL7::Message if successful
192
+ def parse( inobj )
193
+ HL7::Message.new do |msg|
194
+ msg.parse( inobj )
195
+ end
196
+ end
166
197
  end
167
198
 
168
199
  # parse the provided String or Enumerable object into this message
@@ -183,7 +214,7 @@ class HL7::Message
183
214
  return unless @segments
184
215
  @segments.each { |s| yield s }
185
216
  end
186
-
217
+
187
218
  # return the segment count
188
219
  def length
189
220
  0 unless @segments
@@ -191,13 +222,13 @@ class HL7::Message
191
222
  end
192
223
 
193
224
  # provide a screen-readable version of the message
194
- def to_s
195
- @segments.collect { |s| s if s.to_s.length > 0 }.join( "\n" )
225
+ def to_s
226
+ @segments.collect { |s| s if s.to_s.length > 0 }.join( "\n" )
196
227
  end
197
228
 
198
229
  # provide a HL7 spec version of the message
199
230
  def to_hl7
200
- @segments.collect { |s| s if s.to_s.length > 0 }.join( @segment_delim )
231
+ @segments.collect { |s| s if s.to_s.length > 0 }.join( @segment_delim )
201
232
  end
202
233
 
203
234
  # provide the HL7 spec version of the message wrapped in MLLP
@@ -232,12 +263,12 @@ class HL7::Message
232
263
  def parse_element_delim(str)
233
264
  (str && str.kind_of?(String)) ? str.slice(3,1) : "|"
234
265
  end
235
-
266
+
236
267
  # Get the item delimiter from an MSH segment
237
268
  def parse_item_delim(str)
238
269
  (str && str.kind_of?(String)) ? str.slice(4,1) : "^"
239
270
  end
240
-
271
+
241
272
  def parse_enumerable( inary )
242
273
  #assumes an enumeration of strings....
243
274
  inary.each do |oary|
@@ -261,7 +292,7 @@ class HL7::Message
261
292
  @parsing = true
262
293
  last_seg = nil
263
294
  ary.each do |elm|
264
- if elm.slice(0,3) == "MSH"
295
+ if elm.slice(0,3) == "MSH"
265
296
  @item_delim = parse_item_delim(elm)
266
297
  @element_delim = parse_element_delim(elm)
267
298
  end
@@ -276,9 +307,11 @@ class HL7::Message
276
307
  raise HL7::ParseError.new if HL7.ParserConfig[:empty_segment_is_error] || false
277
308
  return nil
278
309
  end
279
-
310
+
280
311
  seg_name = seg_parts[0]
281
- if HL7::Message::Segment.constants.index(seg_name) # do we have an implementation?
312
+ if RUBY_VERSION < "1.9" && HL7::Message::Segment.constants.index(seg_name) # do we have an implementation?
313
+ kls = eval("HL7::Message::Segment::%s" % seg_name)
314
+ elsif RUBY_VERSION >= "1.9" && HL7::Message::Segment.constants.index(seg_name.to_sym)
282
315
  kls = eval("HL7::Message::Segment::%s" % seg_name)
283
316
  else
284
317
  # we don't have an implementation for this segment
@@ -287,13 +320,13 @@ class HL7::Message
287
320
  end
288
321
  new_seg = kls.new( elm, [@element_delim, @item_delim] )
289
322
  new_seg.segment_parent = self
290
-
323
+
291
324
  if last_seg && last_seg.respond_to?(:children) && last_seg.accepts?( seg_name )
292
325
  last_seg.children << new_seg
293
326
  new_seg.is_child_segment = true
294
327
  return last_seg
295
328
  end
296
-
329
+
297
330
  @segments << new_seg
298
331
 
299
332
  # we want to allow segment lookup by name
@@ -303,9 +336,9 @@ class HL7::Message
303
336
  @segments_by_name[ seg_sym ] << new_seg
304
337
  end
305
338
 
306
- new_seg
339
+ new_seg
307
340
  end
308
- end
341
+ end
309
342
 
310
343
  # Ruby Object representation of an hl7 2.x message segment
311
344
  # The segments can be setup to provide aliases to specific fields with
@@ -321,10 +354,10 @@ end
321
354
  # add_field :block_example do |value|
322
355
  # raise HL7::InvalidDataError.new unless value.to_i < 100 && value.to_i > 10
323
356
  # return value
324
- # end
357
+ # end
325
358
  # # this block will be executed when seg.block_example= is called
326
359
  # # and when seg.block_example is called
327
- #
360
+ #
328
361
  class HL7::Message::Segment
329
362
  attr :segment_parent, true
330
363
  attr :element_delim
@@ -334,14 +367,14 @@ class HL7::Message::Segment
334
367
  # setup a new HL7::Message::Segment
335
368
  # raw_segment:: is an optional String or Array which will be used as the
336
369
  # segment's field data
337
- # delims:: an optional array of delimiters, where
370
+ # delims:: an optional array of delimiters, where
338
371
  # delims[0] = element delimiter
339
372
  # delims[1] = item delimiter
340
373
  def initialize(raw_segment="", delims=[], &blk)
341
374
  @segments_by_name = {}
342
375
  @field_total = 0
343
376
  @is_child = false
344
-
377
+
345
378
  @element_delim = (delims.kind_of?(Array) && delims.length>0) ? delims[0] : "|"
346
379
  @item_delim = (delims.kind_of?(Array) && delims.length>1) ? delims[1] : "^"
347
380
 
@@ -350,33 +383,41 @@ class HL7::Message::Segment
350
383
  else
351
384
  @elements = raw_segment.split( @element_delim, -1 )
352
385
  if raw_segment == ""
353
- @elements[0] = self.class.to_s.split( "::" ).last
386
+ @elements[0] = self.class.to_s.split( "::" ).last
354
387
  @elements << ""
355
388
  end
356
389
  end
357
390
 
358
391
  if block_given?
359
- callctx = eval( "self", blk )
392
+ callctx = eval( "self", blk.binding )
360
393
  def callctx.__seg__(val=nil)
361
394
  @__seg_val__ ||= val
362
395
  end
363
396
  callctx.__seg__(self)
364
397
  # TODO: find out if this pollutes the calling namespace permanently...
365
-
398
+
366
399
  to_do = <<-END
367
400
  def method_missing( sym, *args, &blk )
368
- __seg__.send( sym, args, blk )
401
+ __seg__.send( sym, args, blk )
369
402
  end
370
403
  END
371
404
 
372
- eval( to_do, blk )
373
- yield self
374
- eval( "undef method_missing", blk )
405
+ eval( to_do, blk.binding )
406
+ yield self
407
+ eval( "undef method_missing", blk.binding )
408
+ end
409
+ end
410
+
411
+ def self.add_child_type(child_type)
412
+ if @child_types
413
+ @child_types << child_type.to_sym
414
+ else
415
+ has_children [ child_type.to_sym ]
375
416
  end
376
417
  end
377
418
 
378
419
  def to_info
379
- "%s: empty segment >> %s" % [ self.class.to_s, @elements.inspect ]
420
+ "%s: empty segment >> %s" % [ self.class.to_s, @elements.inspect ]
380
421
  end
381
422
 
382
423
  # output the HL7 spec version of the segment
@@ -408,7 +449,7 @@ class HL7::Message::Segment
408
449
  else
409
450
 
410
451
  if args.length > 0
411
- write_field( base_sym, args )
452
+ write_field( base_sym, args.flatten.select { |arg| arg } )
412
453
  else
413
454
  read_field( base_sym )
414
455
  end
@@ -417,7 +458,7 @@ class HL7::Message::Segment
417
458
  end
418
459
 
419
460
  # sort-compare two Segments, 0 indicates equality
420
- def <=>( other )
461
+ def <=>( other )
421
462
  return nil unless other.kind_of?(HL7::Message::Segment)
422
463
 
423
464
  # per Comparable docs: http://www.ruby-doc.org/core/classes/Comparable.html
@@ -426,7 +467,7 @@ class HL7::Message::Segment
426
467
  return 1 if diff < 0
427
468
  return 0
428
469
  end
429
-
470
+
430
471
  # get the defined sort-weight of this segment class
431
472
  # an alias for self.weight
432
473
  def weight
@@ -434,7 +475,7 @@ class HL7::Message::Segment
434
475
  end
435
476
 
436
477
 
437
- # return true if the segment has a parent
478
+ # return true if the segment has a parent
438
479
  def is_child_segment?
439
480
  (@is_child_segment ||= false)
440
481
  end
@@ -473,6 +514,7 @@ class HL7::Message::Segment
473
514
  end
474
515
 
475
516
 
517
+
476
518
  # allows a segment to store other segment objects
477
519
  # used to handle associated lists like one OBR to many OBX segments
478
520
  def self.has_children(child_types)
@@ -513,19 +555,19 @@ class HL7::Message::Segment
513
555
  t = t.to_sym if t && (t.to_s.length > 0) && t.respond_to?(:to_sym)
514
556
  child_types.index t
515
557
  end
516
- end
517
- end
558
+ end
559
+ end
518
560
 
519
- # define a field alias
561
+ # define a field alias
520
562
  # * name is the alias itself (required)
521
- # * options is a hash of parameters
563
+ # * options is a hash of parameters
522
564
  # * :id is the field number to reference (optional, auto-increments from 1
523
565
  # by default)
524
566
  # * :blk is a validation proc (optional, overrides the second argument)
525
567
  # * blk is an optional validation proc which MUST take a parameter
526
568
  # and always return a value for the field (it will be used on read/write
527
569
  # calls)
528
- def self.add_field( name, options={}, &blk )
570
+ def self.add_field( name, options={}, &blk )
529
571
  options = { :idx =>-1, :blk =>blk}.merge!( options )
530
572
  name ||= :id
531
573
  namesym = name.to_sym
@@ -534,10 +576,10 @@ class HL7::Message::Segment
534
576
  options[:idx] = @field_cnt # provide default auto-incrementing
535
577
  end
536
578
  @field_cnt = options[:idx].to_i + 1
537
-
579
+
538
580
  singleton.module_eval do
539
581
  @fields ||= {}
540
- @fields[ namesym ] = options
582
+ @fields[ namesym ] = options
541
583
  end
542
584
 
543
585
  self.class_eval <<-END
@@ -551,7 +593,7 @@ class HL7::Message::Segment
551
593
  end
552
594
 
553
595
  def #{name}=(value)
554
- write_field( :#{namesym}, value )
596
+ write_field( :#{namesym}, value )
555
597
  end
556
598
  END
557
599
  end
@@ -577,7 +619,7 @@ class HL7::Message::Segment
577
619
  def read_field( name ) #:nodoc:
578
620
  idx, field_blk = field_info( name )
579
621
  return nil unless idx
580
- return nil if (idx >= @elements.length)
622
+ return nil if (idx >= @elements.length)
581
623
 
582
624
  ret = @elements[ idx ]
583
625
  ret = ret.first if (ret.kind_of?(Array) && ret.length == 1)
@@ -596,6 +638,7 @@ class HL7::Message::Segment
596
638
  @elements += missing
597
639
  end
598
640
 
641
+ value = value.first if (value && value.kind_of?(Array) && value.length == 1)
599
642
  value = field_blk.call( value ) if field_blk
600
643
  @elements[ idx ] = value.to_s
601
644
  end
@@ -618,8 +661,8 @@ end
618
661
  #end
619
662
 
620
663
  # Provide a catch-all information preserving segment
621
- # * no aliases are not provided BUT you can use the numeric element accessor
622
- #
664
+ # * nb: aliases are not provided BUT you can use the numeric element accessor
665
+ #
623
666
  # seg = HL7::Message::Segment::Default.new
624
667
  # seg.e0 = "NK1"
625
668
  # seg.e1 = "SOMETHING ELSE"
@@ -628,7 +671,7 @@ end
628
671
  class HL7::Message::Segment::Default < HL7::Message::Segment
629
672
  def initialize(raw_segment="", delims=[])
630
673
  segs = [] if (raw_segment == "")
631
- segs ||= raw_segment
674
+ segs ||= raw_segment
632
675
  super( segs, delims )
633
676
  end
634
677
  end