ruby-hl7 0.1.23

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/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: