ruby-hl7 0.3 → 1.0

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.
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