ruby-hl7 1.0.3 → 1.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.
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'