ruby-hl7 0.2.50 → 0.3

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