ruby-hl7 0.1.23

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ = License
2
+ Permission is hereby granted, free of charge, to any person obtaining
3
+ a copy of this software and associated documentation files (the
4
+ "Software"), to deal in the Software without restriction, including
5
+ without limitation the rights to use, copy, modify, merge, publish,
6
+ distribute, sublicense, and/or sell copies of the Software, and to
7
+ permit persons to whom the Software is furnished to do so, subject to
8
+ the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be
11
+ included in all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
17
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
19
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,24 @@
1
+ =Ruby HL7 Library README
2
+
3
+ A simple way to parse and create hl7 2.x messages with ruby.
4
+ Examples can be found in HL7::Message
5
+ The version id can be found in the HL7::VERSION constant.
6
+
7
+ * Bug tracking: http://trac.hasno.info/ruby-hl7
8
+ * Subversion: svn://hasno.info/ruby-hl7
9
+ * Docs: http://ruby-hl7.rubyforge.org
10
+ * Rubyforge: http://rubyforge.org/projects/ruby-hl7
11
+ * Lists
12
+ * Developers: mailto:ruby-hl7-devel@rubyforge.org
13
+ * Users: mailto:ruby-hl7-users@rubyforge.org
14
+
15
+ Copyright (c) 2006-2007 Mark Guzman
16
+ $Id: README 26 2007-03-18 14:41:41Z segfault $
17
+
18
+ == Download and Installation
19
+ Install the gem using the following command:
20
+ gem install ruby-hl7
21
+
22
+
23
+ == License
24
+ see the LICENSE file
@@ -0,0 +1,529 @@
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 23 2007-03-18 01:33:12Z segfault $
13
+ #
14
+ # == License
15
+ # see the LICENSE file
16
+ #
17
+
18
+ require 'rubygems'
19
+ require "stringio"
20
+ require "date"
21
+ require 'facets/core/class/cattr'
22
+
23
+ module HL7 # :nodoc:
24
+ VERSION = "0.1.%s" % "$Rev: 23 $".gsub(/\$Rev:\s+/, '').gsub(/\s*\$$/, '')
25
+ end
26
+
27
+ # Encapsulate HL7 specific exceptions
28
+ class HL7::Exception < StandardError
29
+ end
30
+
31
+ # Parsing failed
32
+ class HL7::ParseError < HL7::Exception
33
+ end
34
+
35
+ # Attempting to use an invalid indice
36
+ class HL7::RangeError < HL7::Exception
37
+ end
38
+
39
+ # Attempting to assign invalid data to a field
40
+ class HL7::InvalidDataError < HL7::Exception
41
+ end
42
+
43
+ # Ruby Object representation of an hl7 2.x message
44
+ # the message object is actually a "smart" collection of hl7 segments
45
+ # == Examples
46
+ #
47
+ # ==== Creating a new HL7 message
48
+ #
49
+ # # create a message
50
+ # msg = HL7::Message.new
51
+ #
52
+ # # create a MSH segment for our new message
53
+ # msh = HL7::Message::Segment::MSH.new
54
+ # msh.recv_app = "ruby hl7"
55
+ # msh.recv_facility = "my office"
56
+ # msh.processing_id = rand(10000).to_s
57
+ #
58
+ # msg << msh # add the MSH segment to the message
59
+ #
60
+ # puts msg.to_s # readable version of the message
61
+ #
62
+ # puts msg.to_hl7 # hl7 version of the message (as a string)
63
+ #
64
+ # puts msg.to_mllp # mllp version of the message (as a string)
65
+ #
66
+ # ==== Parse an existing HL7 message
67
+ #
68
+ # raw_input = open( "my_hl7_msg.txt" ).readlines
69
+ # msg = HL7::Message.new( raw_input )
70
+ #
71
+ # puts "message type: %s" % msg[:MSH].message_type
72
+ #
73
+ #
74
+ class HL7::Message
75
+ include Enumerable # we treat an hl7 2.x message as a collection of segments
76
+ attr :element_delim
77
+ attr :item_delim
78
+ attr :segment_delim
79
+
80
+ # setup a new hl7 message
81
+ # raw_msg:: is an optional object containing an hl7 message
82
+ # it can either be a string or an Enumerable object
83
+ def initialize( raw_msg=nil )
84
+ @segments = []
85
+ @segments_by_name = {}
86
+ @item_delim = "^"
87
+ @element_delim = '|'
88
+ @segment_delim = "\r"
89
+
90
+ parse( raw_msg ) if raw_msg
91
+ end
92
+
93
+ # access a segment of the message
94
+ # index:: can be a Range, Fixnum or anything that
95
+ # responds to to_sym
96
+ def []( index )
97
+ ret = nil
98
+
99
+ if index.kind_of?(Range) || index.kind_of?(Fixnum)
100
+ ret = @segments[ index ]
101
+ elsif (index.respond_to? :to_sym)
102
+ ret = @segments_by_name[ index.to_sym ]
103
+ ret = ret.first if ret.length == 1
104
+ end
105
+
106
+ ret
107
+ end
108
+
109
+ # modify a segment of the message
110
+ # index:: can be a Range, Fixnum or anything that
111
+ # responds to to_sym
112
+ # value:: an HL7::Message::Segment object
113
+ def []=( index, value )
114
+ unless ( value && value.kind_of?(HL7::Message::Segment) )
115
+ raise HL7::Exception.new( "attempting to assign something other than an HL7 Segment" )
116
+ end
117
+
118
+ if index.kind_of?(Range) || index.kind_of?(Fixnum)
119
+ @segments[ index ] = value
120
+ else
121
+ (@segments_by_name[ index.to_sym ] ||= []) << value
122
+ end
123
+ end
124
+
125
+ # return the index of the value if it exists, nil otherwise
126
+ # value:: is expected to be a string
127
+ def index( value )
128
+ return nil unless (value && value.respond_to?(:to_sym))
129
+
130
+ segs = @segments_by_name[ value.to_sym ]
131
+ return nil unless segs
132
+
133
+ @segments.index( segs.to_a.first )
134
+ end
135
+
136
+ # add a segment to the message
137
+ # * will force auto set_id sequencing for segments containing set_id's
138
+ def <<( value )
139
+ unless ( value && value.kind_of?(HL7::Message::Segment) )
140
+ raise HL7::Exception.new( "attempting to append something other than an HL7 Segment" )
141
+ end
142
+
143
+ (@segments ||= []) << value
144
+ name = value.class.to_s.gsub("HL7::Message::Segment::", "").to_sym
145
+ (@segments_by_name[ name ] ||= []) << value
146
+ sequence_segments # let's auto-set the set-id as we go
147
+ end
148
+
149
+ # parse a String or Enumerable object into an HL7::Message if possible
150
+ # * returns a new HL7::Message if successful
151
+ def self.parse( inobj )
152
+ ret = HL7::Message.new
153
+ ret.parse( inobj )
154
+ ret
155
+ end
156
+
157
+ # parse the provided String or Enumerable object into this message
158
+ def parse( inobj )
159
+ unless inobj.kind_of?(String) || inobj.respond_to?(:each)
160
+ raise HL7::ParseError.new
161
+ end
162
+
163
+ if inobj.kind_of?(String)
164
+ parse_string( inobj )
165
+ elsif inobj.respond_to?(:each)
166
+ parse_enumerable( inobj )
167
+ end
168
+ end
169
+
170
+ # yield each segment in the message
171
+ def each # :yeilds: segment
172
+ return unless @segments
173
+ @segments.each { |s| yield s }
174
+ end
175
+
176
+ # provide a screen-readable version of the message
177
+ def to_s
178
+ @segments.join( '\n' )
179
+ end
180
+
181
+ # provide a HL7 spec version of the message
182
+ def to_hl7
183
+ @segments.join( @segment_delim )
184
+ end
185
+
186
+ # provide the HL7 spec version of the message wrapped in MLLP
187
+ def to_mllp
188
+ pre_mllp = to_hl7
189
+ "\x0b" + pre_mllp + "\x1c\r"
190
+ end
191
+
192
+ # auto-set the set_id fields of any message segments that
193
+ # provide it and have more than one instance in the message
194
+ def sequence_segments(base=nil)
195
+ last = nil
196
+ segs = @segments
197
+ segs = base.children if base
198
+
199
+ segs.each do |s|
200
+ if s.kind_of?( last.class ) && s.respond_to?( :set_id )
201
+ if (last.set_id == "" || last.set_id == nil)
202
+ last.set_id = 1
203
+ end
204
+ s.set_id = last.set_id.to_i + 1
205
+ end
206
+
207
+ if s.respond_to?(:children)
208
+ sequence_segments( s )
209
+ end
210
+
211
+ last = s
212
+ end
213
+ end
214
+
215
+ private
216
+ def parse_enumerable( inary )
217
+ #assumes an enumeration of strings....
218
+ inary.each do |oary|
219
+ parse_string( oary.to_s )
220
+ end
221
+ end
222
+
223
+ def parse_string( instr )
224
+ post_mllp = instr
225
+ if /\x0b((:?.|\r|\n)+)\x1c\r/.match( instr )
226
+ post_mllp = $1 #strip the mllp bytes
227
+ end
228
+
229
+ ary = post_mllp.split( segment_delim, -1 )
230
+ generate_segments( ary )
231
+ end
232
+
233
+ def generate_segments( ary )
234
+ raise HL7::ParseError.new unless ary.length > 0
235
+
236
+ ary.each do |elm|
237
+ seg_parts = elm.split( @element_delim, -1 )
238
+ raise HL7::ParseError.new unless seg_parts && (seg_parts.length > 0)
239
+
240
+ seg_name = seg_parts[0]
241
+ begin
242
+ kls = eval("HL7::Message::Segment::%s" % seg_name)
243
+ rescue Exception
244
+ # we don't have an implementation for this segment
245
+ # so lets just preserve the data
246
+ kls = HL7::Message::Segment::Default
247
+ end
248
+ new_seg = kls.new( elm )
249
+ @segments << new_seg
250
+
251
+ # we want to allow segment lookup by name
252
+ seg_sym = seg_name.to_sym
253
+ @segments_by_name[ seg_sym ] ||= []
254
+ @segments_by_name[ seg_sym ] << new_seg
255
+ end
256
+
257
+ end
258
+ end
259
+
260
+ # Ruby Object representation of an hl7 2.x message segment
261
+ # The segments can be setup to provide aliases to specific fields with
262
+ # optional validation code that is run when the field is modified
263
+ # The segment field data is also accessible via the e<number> method.
264
+ #
265
+ # == Defining a New Segment
266
+ # class HL7::Message::Segment::NK1 < HL7::Message::Segment
267
+ # wieght 100 # segments are sorted ascendingly
268
+ # add_field :name=>:something_you_want # assumes :idx=>1
269
+ # add_field :name=>:something_else, :idx=>6 # :idx=>6 and field count=6
270
+ # add_field :name=>:something_more # :idx=>7
271
+ # add_field :name=>:block_example do |value|
272
+ # raise HL7::InvalidDataError.new unless value.to_i < 100 && value.to_i > 10
273
+ # return value
274
+ # end
275
+ # # this block will be executed when seg.block_example= is called
276
+ # # and when seg.block_example is called
277
+ #
278
+ class HL7::Message::Segment
279
+ attr :element_delim
280
+ attr :item_delim
281
+ attr :segment_weight
282
+
283
+ # setup a new HL7::Message::Segment
284
+ # raw_segment:: is an optional String or Array which will be used as the
285
+ # segment's field data
286
+ def initialize(raw_segment="")
287
+ @segments_by_name = {}
288
+ @element_delim = '|'
289
+ @field_total = 0
290
+
291
+ if (raw_segment.kind_of? Array)
292
+ @elements = raw_segment
293
+ else
294
+ @elements = raw_segment.split( element_delim, -1 )
295
+ if raw_segment == ""
296
+ @elements[0] = self.class.to_s.split( "::" ).last
297
+ @elements << ""
298
+ end
299
+ end
300
+ end
301
+
302
+ def to_info
303
+ "%s: empty segment >> %s" % [ self.class.to_s, @elements.inspect ]
304
+ end
305
+
306
+ # output the HL7 spec version of the segment
307
+ def to_s
308
+ @elements.join( @element_delim )
309
+ end
310
+
311
+ # at the segment level there is no difference between to_s and to_hl7
312
+ alias :to_hl7 :to_s
313
+
314
+ # handle the e<number> field accessor
315
+ # and any aliases that didn't get added to the system automatically
316
+ def method_missing( sym, *args, &blk )
317
+ base_str = sym.to_s.gsub( "=", "" )
318
+ base_sym = base_str.to_sym
319
+
320
+ if self.class.fields.include?( base_sym )
321
+ # base_sym is ok, let's move on
322
+ elsif /e([0-9]+)/.match( base_str )
323
+ # base_sym should actually be $1, since we're going by
324
+ # element id number
325
+ base_sym = $1.to_i
326
+ else
327
+ super.method_missing( sym, args, blk )
328
+ end
329
+
330
+ if sym.to_s.include?( "=" )
331
+ write_field( base_sym, args )
332
+ else
333
+ read_field( base_sym )
334
+ end
335
+ end
336
+
337
+ def <=>( other )
338
+ return nil unless other.kind_of?(HL7::Message::Segment)
339
+
340
+ diff = self.weight - other.weight
341
+ return -1 if diff > 0
342
+ return 1 if diff < 0
343
+ return 0
344
+ end
345
+
346
+ # get the defined sort-weight of this segment class
347
+ # an alias for self.weight
348
+ def weight
349
+ self.class.weight
350
+ end
351
+
352
+ private
353
+ def self.singleton #:nodoc:
354
+ class << self; self end
355
+ end
356
+
357
+
358
+ # DSL element to define a segment's sort weight
359
+ # returns the segment's current weight by default
360
+ # segments are sorted ascending
361
+ def self.weight(new_weight=nil)
362
+ if new_weight
363
+ singleton.module_eval do
364
+ @my_weight = new_weight
365
+ end
366
+ end
367
+
368
+ singleton.module_eval do
369
+ return 999 unless @my_weight
370
+ @my_weight
371
+ end
372
+ end
373
+
374
+
375
+ # allows a segment to store other segment objects
376
+ # used to handle associated lists like one OBR to many OBX segments
377
+ def self.has_children
378
+ self.class_eval do
379
+ define_method(:children) do
380
+ unless @my_children
381
+ @my_children ||= []
382
+ @my_children.instance_eval do
383
+ alias :old_append :<<
384
+
385
+ def <<(value)
386
+ unless (value && value.kind_of?(HL7::Message::Segment))
387
+ raise HL7::Exception.new( "attempting to append non-segment to a segment list" )
388
+ end
389
+
390
+ old_append( value )
391
+ end
392
+ end
393
+ end
394
+
395
+ @my_children
396
+ end
397
+
398
+ alias :my_to_s :to_s
399
+ define_method(:to_s) do
400
+ out = my_to_s
401
+ if @my_children
402
+ @my_children.each do |seg|
403
+ out << '\r'
404
+ out << seg.to_s
405
+ end
406
+ end
407
+ out
408
+ end
409
+ end
410
+
411
+ end
412
+
413
+ # define a field alias
414
+ # * options is a hash of parameters
415
+ # * :name is the alias itself (required)
416
+ # * :id is the field number to reference (optional, auto-increments from 1
417
+ # by default)
418
+ # * :blk is a validation proc (optional, overrides the second argument)
419
+ # * blk is an optional validation proc which MUST take a parameter
420
+ # and always return a value for the field (it will be used on read/write
421
+ # calls)
422
+ def self.add_field( options={}, &blk )
423
+ options = {:name => :id, :idx =>-1, :blk =>blk}.merge!( options )
424
+ name = options[:name]
425
+ namesym = name.to_sym
426
+ @field_cnt ||= 1
427
+ if options[:idx] == -1
428
+ options[:idx] = @field_cnt # provide default auto-incrementing
429
+ end
430
+ @field_cnt = options[:idx].to_i + 1
431
+
432
+ singleton.module_eval do
433
+ @fields ||= {}
434
+ @fields[ namesym ] = options
435
+ end
436
+
437
+ self.class_eval <<-END
438
+ def #{name}()
439
+ read_field( :#{namesym} )
440
+ end
441
+
442
+ def #{name}=(value)
443
+ write_field( :#{namesym}, value )
444
+ end
445
+ END
446
+ end
447
+
448
+ def self.fields #:nodoc:
449
+ singleton.module_eval do
450
+ (@fields ||= [])
451
+ end
452
+ end
453
+
454
+ def field_info( name ) #:nodoc:
455
+ field_blk = nil
456
+ idx = name # assume we've gotten a fixnum
457
+ unless name.kind_of?( Fixnum )
458
+ fld_info = self.class.fields[ name ]
459
+ idx = fld_info[:idx].to_i
460
+ field_blk = fld_info[:blk]
461
+ end
462
+
463
+ [ idx, field_blk ]
464
+ end
465
+
466
+ def read_field( name ) #:nodoc:
467
+ idx, field_blk = field_info( name )
468
+ return nil unless idx
469
+ return nil if (idx >= @elements.length)
470
+
471
+ ret = @elements[ idx ]
472
+ ret = ret.first if (ret.kind_of?(Array) && ret.length == 1)
473
+ ret = field_blk.call( ret ) if field_blk
474
+ ret
475
+ end
476
+
477
+ def write_field( name, value ) #:nodoc:
478
+ idx, field_blk = field_info( name )
479
+ return nil unless idx
480
+
481
+ if (idx >= @elements.length)
482
+ # make some space for the incoming field, missing items are assumed to
483
+ # be empty, so this is valid per the spec -mg
484
+ missing = ("," * (idx-@elements.length)).split(',',-1)
485
+ @elements += missing
486
+ end
487
+
488
+ value = field_blk.call( value ) if field_blk
489
+ @elements[ idx ] = value.to_s
490
+ end
491
+
492
+ @elements = []
493
+
494
+
495
+ end
496
+
497
+ # parse an hl7 formatted date
498
+ #def Date.from_hl7( hl7_date )
499
+ #end
500
+
501
+ #def Date.to_hl7_short( ruby_date )
502
+ #end
503
+
504
+ #def Date.to_hl7_med( ruby_date )
505
+ #end
506
+
507
+ #def Date.to_hl7_long( ruby_date )
508
+ #end
509
+
510
+ # Provide a catch-all information preserving segment
511
+ # * no aliases are not provided BUT you can use the numeric element accessor
512
+ #
513
+ # seg = HL7::Message::Segment::Default.new
514
+ # seg.e0 = "NK1"
515
+ # seg.e1 = "SOMETHING ELSE"
516
+ # seg.e2 = "KIN HERE"
517
+ #
518
+ class HL7::Message::Segment::Default < HL7::Message::Segment
519
+ def initialize(raw_segment="")
520
+ segs = [] if (raw_segment == "")
521
+ segs ||= raw_segment
522
+ super( segs )
523
+ end
524
+ end
525
+
526
+ # load our segments
527
+ Dir["#{File.dirname(__FILE__)}/segments/*.rb"].each { |ext| load ext }
528
+
529
+ # vim:tw=78:sw=2:ts=2:et:fdm=marker: