ruby-hl7 0.2.50 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
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: