ruby-hl7 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ruby-hl7.rb CHANGED
@@ -17,11 +17,11 @@
17
17
  #
18
18
 
19
19
  require 'rubygems'
20
- require "stringio"
21
- require "date"
20
+ require 'stringio'
21
+ require 'date'
22
22
 
23
23
  module HL7 # :nodoc:
24
- VERSION = "1.0.3"
24
+ VERSION = '1.1.0'
25
25
  def self.ParserConfig
26
26
  @parser_cfg ||= { :empty_segment_is_error => true }
27
27
  end
@@ -43,640 +43,18 @@ end
43
43
  class HL7::InvalidDataError < HL7::Exception
44
44
  end
45
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
- 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
197
- end
198
-
199
- # parse the provided String or Enumerable object into this message
200
- def parse( inobj )
201
- unless inobj.kind_of?(String) || inobj.respond_to?(:each)
202
- raise HL7::ParseError.new
203
- end
204
-
205
- if inobj.kind_of?(String)
206
- parse_string( inobj )
207
- elsif inobj.respond_to?(:each)
208
- parse_enumerable( inobj )
209
- end
210
- end
211
-
212
- # yield each segment in the message
213
- def each # :yeilds: segment
214
- return unless @segments
215
- @segments.each { |s| yield s }
216
- end
217
-
218
- # return the segment count
219
- def length
220
- 0 unless @segments
221
- @segments.length
222
- end
223
-
224
- # provide a screen-readable version of the message
225
- def to_s
226
- @segments.collect { |s| s if s.to_s.length > 0 }.join( "\n" )
227
- end
228
-
229
- # provide a HL7 spec version of the message
230
- def to_hl7
231
- @segments.collect { |s| s if s.to_s.length > 0 }.join( @segment_delim )
232
- end
233
-
234
- # provide the HL7 spec version of the message wrapped in MLLP
235
- def to_mllp
236
- pre_mllp = to_hl7
237
- "\x0b" + pre_mllp + "\x1c\r"
238
- end
239
-
240
- # auto-set the set_id fields of any message segments that
241
- # provide it and have more than one instance in the message
242
- def sequence_segments(base=nil)
243
- last = nil
244
- segs = @segments
245
- segs = base.children if base
246
-
247
- segs.each do |s|
248
- if s.kind_of?( last.class ) && s.respond_to?( :set_id )
249
- last.set_id = 1 unless last.set_id && last.set_id.to_i > 0
250
- s.set_id = last.set_id.to_i + 1
251
- end
252
-
253
- if s.respond_to?(:children)
254
- sequence_segments( s )
255
- end
256
-
257
- last = s
258
- end
259
- end
260
-
261
- private
262
- # Get the element delimiter from an MSH segment
263
- def parse_element_delim(str)
264
- (str && str.kind_of?(String)) ? str.slice(3,1) : "|"
265
- end
266
-
267
- # Get the item delimiter from an MSH segment
268
- def parse_item_delim(str)
269
- (str && str.kind_of?(String)) ? str.slice(4,1) : "^"
270
- end
271
-
272
- def parse_enumerable( inary )
273
- #assumes an enumeration of strings....
274
- inary.each do |oary|
275
- parse_string( oary.to_s )
276
- end
277
- end
278
-
279
- def parse_string( instr )
280
- post_mllp = instr
281
- if /\x0b((:?.|\r|\n)+)\x1c\r/.match( instr )
282
- post_mllp = $1 #strip the mllp bytes
283
- end
284
-
285
- ary = post_mllp.split( segment_delim, -1 )
286
- generate_segments( ary )
287
- end
288
-
289
- def generate_segments( ary )
290
- raise HL7::ParseError.new unless ary.length > 0
291
-
292
- @parsing = true
293
- last_seg = nil
294
- ary.each do |elm|
295
- if elm.slice(0,3) == "MSH"
296
- @item_delim = parse_item_delim(elm)
297
- @element_delim = parse_element_delim(elm)
298
- end
299
- last_seg = generate_segment( elm, last_seg ) || last_seg
300
- end
301
- @parsing = nil
302
- end
303
-
304
- def generate_segment( elm, last_seg )
305
- seg_parts = elm.split( @element_delim, -1 )
306
- unless seg_parts && (seg_parts.length > 0)
307
- raise HL7::ParseError.new if HL7.ParserConfig[:empty_segment_is_error] || false
308
- return nil
309
- end
310
-
311
- seg_name = seg_parts[0]
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)
315
- kls = eval("HL7::Message::Segment::%s" % seg_name)
316
- else
317
- # we don't have an implementation for this segment
318
- # so lets just preserve the data
319
- kls = HL7::Message::Segment::Default
320
- end
321
- new_seg = kls.new( elm, [@element_delim, @item_delim] )
322
- new_seg.segment_parent = self
323
-
324
- if last_seg && last_seg.respond_to?(:children) && last_seg.accepts?( seg_name )
325
- last_seg.children << new_seg
326
- new_seg.is_child_segment = true
327
- return last_seg
328
- end
329
-
330
- @segments << new_seg
331
-
332
- # we want to allow segment lookup by name
333
- if seg_name && (seg_name.strip.length > 0)
334
- seg_sym = seg_name.to_sym
335
- @segments_by_name[ seg_sym ] ||= []
336
- @segments_by_name[ seg_sym ] << new_seg
337
- end
338
-
339
- new_seg
340
- end
341
- end
342
-
343
- # Ruby Object representation of an hl7 2.x message segment
344
- # The segments can be setup to provide aliases to specific fields with
345
- # optional validation code that is run when the field is modified
346
- # The segment field data is also accessible via the e<number> method.
347
- #
348
- # == Defining a New Segment
349
- # class HL7::Message::Segment::NK1 < HL7::Message::Segment
350
- # wieght 100 # segments are sorted ascendingly
351
- # add_field :something_you_want # assumes :idx=>1
352
- # add_field :something_else, :idx=>6 # :idx=>6 and field count=6
353
- # add_field :something_more # :idx=>7
354
- # add_field :block_example do |value|
355
- # raise HL7::InvalidDataError.new unless value.to_i < 100 && value.to_i > 10
356
- # return value
357
- # end
358
- # # this block will be executed when seg.block_example= is called
359
- # # and when seg.block_example is called
360
- #
361
- class HL7::Message::Segment
362
- attr :segment_parent, true
363
- attr :element_delim
364
- attr :item_delim
365
- attr :segment_weight
366
-
367
- # setup a new HL7::Message::Segment
368
- # raw_segment:: is an optional String or Array which will be used as the
369
- # segment's field data
370
- # delims:: an optional array of delimiters, where
371
- # delims[0] = element delimiter
372
- # delims[1] = item delimiter
373
- def initialize(raw_segment="", delims=[], &blk)
374
- @segments_by_name = {}
375
- @field_total = 0
376
- @is_child = false
377
-
378
- @element_delim = (delims.kind_of?(Array) && delims.length>0) ? delims[0] : "|"
379
- @item_delim = (delims.kind_of?(Array) && delims.length>1) ? delims[1] : "^"
380
-
381
- if (raw_segment.kind_of? Array)
382
- @elements = raw_segment
383
- else
384
- @elements = raw_segment.split( @element_delim, -1 )
385
- if raw_segment == ""
386
- @elements[0] = self.class.to_s.split( "::" ).last
387
- @elements << ""
388
- end
389
- end
390
-
391
- if block_given?
392
- callctx = eval( "self", blk.binding )
393
- def callctx.__seg__(val=nil)
394
- @__seg_val__ ||= val
395
- end
396
- callctx.__seg__(self)
397
- # TODO: find out if this pollutes the calling namespace permanently...
398
-
399
- to_do = <<-END
400
- def method_missing( sym, *args, &blk )
401
- __seg__.send( sym, args, blk )
402
- end
403
- END
404
-
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 ]
416
- end
417
- end
418
-
419
- def to_info
420
- "%s: empty segment >> %s" % [ self.class.to_s, @elements.inspect ]
421
- end
422
-
423
- # output the HL7 spec version of the segment
424
- def to_s
425
- @elements.join( @element_delim )
426
- end
427
-
428
- # at the segment level there is no difference between to_s and to_hl7
429
- alias :to_hl7 :to_s
430
-
431
- # handle the e<number> field accessor
432
- # and any aliases that didn't get added to the system automatically
433
- def method_missing( sym, *args, &blk )
434
- base_str = sym.to_s.gsub( "=", "" )
435
- base_sym = base_str.to_sym
436
-
437
- if self.class.fields.include?( base_sym )
438
- # base_sym is ok, let's move on
439
- elsif /e([0-9]+)/.match( base_str )
440
- # base_sym should actually be $1, since we're going by
441
- # element id number
442
- base_sym = $1.to_i
443
- else
444
- super
445
- end
446
-
447
- if sym.to_s.include?( "=" )
448
- write_field( base_sym, args )
449
- else
450
-
451
- if args.length > 0
452
- write_field( base_sym, args.flatten.select { |arg| arg } )
453
- else
454
- read_field( base_sym )
455
- end
456
-
457
- end
458
- end
459
-
460
- # sort-compare two Segments, 0 indicates equality
461
- def <=>( other )
462
- return nil unless other.kind_of?(HL7::Message::Segment)
463
-
464
- # per Comparable docs: http://www.ruby-doc.org/core/classes/Comparable.html
465
- diff = self.weight - other.weight
466
- return -1 if diff > 0
467
- return 1 if diff < 0
468
- return 0
469
- end
470
-
471
- # get the defined sort-weight of this segment class
472
- # an alias for self.weight
473
- def weight
474
- self.class.weight
475
- end
476
-
477
-
478
- # return true if the segment has a parent
479
- def is_child_segment?
480
- (@is_child_segment ||= false)
481
- end
482
-
483
- # indicate whether or not the segment has a parent
484
- def is_child_segment=(val)
485
- @is_child_segment = val
486
- end
487
-
488
- # get the length of the segment (number of fields it contains)
489
- def length
490
- 0 unless @elements
491
- @elements.length
492
- end
493
-
494
-
495
- private
496
- def self.singleton #:nodoc:
497
- class << self; self end
498
- end
499
-
500
- # DSL element to define a segment's sort weight
501
- # returns the segment's current weight by default
502
- # segments are sorted ascending
503
- def self.weight(new_weight=nil)
504
- if new_weight
505
- singleton.module_eval do
506
- @my_weight = new_weight
507
- end
508
- end
509
-
510
- singleton.module_eval do
511
- return 999 unless @my_weight
512
- @my_weight
513
- end
514
- end
515
-
516
-
517
-
518
- # allows a segment to store other segment objects
519
- # used to handle associated lists like one OBR to many OBX segments
520
- def self.has_children(child_types)
521
- @child_types = child_types
522
- define_method(:child_types) do
523
- @child_types
524
- end
525
-
526
- self.class_eval do
527
- define_method(:children) do
528
- unless @my_children
529
- p = self
530
- @my_children ||= []
531
- @my_children.instance_eval do
532
- @parental = p
533
- alias :old_append :<<
534
-
535
- def <<(value)
536
- unless (value && value.kind_of?(HL7::Message::Segment))
537
- raise HL7::Exception.new( "attempting to append non-segment to a segment list" )
538
- end
539
-
540
- value.segment_parent = @parental
541
- k = @parental
542
- while (k && k.segment_parent && !k.segment_parent.kind_of?(HL7::Message))
543
- k = k.segment_parent
544
- end
545
- k.segment_parent << value if k && k.segment_parent
546
- old_append( value )
547
- end
548
- end
549
- end
550
-
551
- @my_children
552
- end
553
-
554
- define_method('accepts?') do |t|
555
- t = t.to_sym if t && (t.to_s.length > 0) && t.respond_to?(:to_sym)
556
- child_types.index t
557
- end
558
- end
559
- end
560
-
561
- # define a field alias
562
- # * name is the alias itself (required)
563
- # * options is a hash of parameters
564
- # * :id is the field number to reference (optional, auto-increments from 1
565
- # by default)
566
- # * :blk is a validation proc (optional, overrides the second argument)
567
- # * blk is an optional validation proc which MUST take a parameter
568
- # and always return a value for the field (it will be used on read/write
569
- # calls)
570
- def self.add_field( name, options={}, &blk )
571
- options = { :idx =>-1, :blk =>blk}.merge!( options )
572
- name ||= :id
573
- namesym = name.to_sym
574
- @field_cnt ||= 1
575
- if options[:idx] == -1
576
- options[:idx] = @field_cnt # provide default auto-incrementing
577
- end
578
- @field_cnt = options[:idx].to_i + 1
579
-
580
- singleton.module_eval do
581
- @fields ||= {}
582
- @fields[ namesym ] = options
583
- end
584
-
585
- self.class_eval <<-END
586
- def #{name}(val=nil)
587
- unless val
588
- read_field( :#{namesym} )
589
- else
590
- write_field( :#{namesym}, val )
591
- val # this matches existing n= method functionality
592
- end
593
- end
594
-
595
- def #{name}=(value)
596
- write_field( :#{namesym}, value )
597
- end
598
- END
599
- end
600
-
601
- def self.fields #:nodoc:
602
- singleton.module_eval do
603
- (@fields ||= [])
604
- end
605
- end
606
-
607
- def field_info( name ) #:nodoc:
608
- field_blk = nil
609
- idx = name # assume we've gotten a fixnum
610
- unless name.kind_of?( Fixnum )
611
- fld_info = self.class.fields[ name ]
612
- idx = fld_info[:idx].to_i
613
- field_blk = fld_info[:blk]
614
- end
615
-
616
- [ idx, field_blk ]
617
- end
618
-
619
- def read_field( name ) #:nodoc:
620
- idx, field_blk = field_info( name )
621
- return nil unless idx
622
- return nil if (idx >= @elements.length)
623
-
624
- ret = @elements[ idx ]
625
- ret = ret.first if (ret.kind_of?(Array) && ret.length == 1)
626
- ret = field_blk.call( ret ) if field_blk
627
- ret
628
- end
629
-
630
- def write_field( name, value ) #:nodoc:
631
- idx, field_blk = field_info( name )
632
- return nil unless idx
633
-
634
- if (idx >= @elements.length)
635
- # make some space for the incoming field, missing items are assumed to
636
- # be empty, so this is valid per the spec -mg
637
- missing = ("," * (idx-@elements.length)).split(',',-1)
638
- @elements += missing
639
- end
640
-
641
- value = value.first if (value && value.kind_of?(Array) && value.length == 1)
642
- value = field_blk.call( value ) if field_blk
643
- @elements[ idx ] = value.to_s
644
- end
645
-
646
- @elements = []
647
-
648
- end
649
-
650
- # parse an hl7 formatted date
651
- #def Date.from_hl7( hl7_date )
652
- #end
653
-
654
- #def Date.to_hl7_short( ruby_date )
655
- #end
656
-
657
- #def Date.to_hl7_med( ruby_date )
658
- #end
659
-
660
- #def Date.to_hl7_long( ruby_date )
661
- #end
662
-
663
- # Provide a catch-all information preserving segment
664
- # * nb: aliases are not provided BUT you can use the numeric element accessor
665
- #
666
- # seg = HL7::Message::Segment::Default.new
667
- # seg.e0 = "NK1"
668
- # seg.e1 = "SOMETHING ELSE"
669
- # seg.e2 = "KIN HERE"
670
- #
671
- class HL7::Message::Segment::Default < HL7::Message::Segment
672
- def initialize(raw_segment="", delims=[])
673
- segs = [] if (raw_segment == "")
674
- segs ||= raw_segment
675
- super( segs, delims )
676
- end
46
+ # Attempting to add an empty segment
47
+ # This error per configuration setting
48
+ class HL7::EmptySegmentNotAllowed < HL7::ParseError
677
49
  end
678
50
 
679
- # load our segments
680
- Dir["#{File.dirname(__FILE__)}/segments/*.rb"].each { |ext| load ext }
51
+ require 'message_parser'
52
+ require 'message'
53
+ require 'segment_list_storage'
54
+ require 'segment_generator'
55
+ require 'segment_fields'
56
+ require 'segment'
57
+ require 'segment_default'
681
58
 
682
- # vim:tw=78:sw=2:ts=2:et:fdm=marker:
59
+ require 'core_ext/date_time'
60
+ require 'core_ext/string'