ruby-hl7 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2a1652685f5af3d1977d1d32eda59c87bad5523b
4
+ data.tar.gz: 6d995a2efccfb7fd8a0dad2d2c386b530f565824
5
+ SHA512:
6
+ metadata.gz: fbaab918091adf2be1062b6343b9fea752d0210091765b0725b4d5d356fb643578a279e9bf21cb8525d7ba4e58b48073196c23f97138f1c3c2bb43c4d2afe7fe
7
+ data.tar.gz: fd5aaf2b829bc1cdbf3cce507312cba40d33c497915c94ba0accc09b96ca8bb60b2883188e5b7d88f6550f0b7612157811499d418424cfd73ce7242f215943f4
data/README.rdoc CHANGED
@@ -1,5 +1,8 @@
1
1
  = Ruby HL7 Library README
2
2
 
3
+ {<img src="https://travis-ci.org/ruby-hl7/ruby-hl7.png?branch=master" alt="Build Status" />}[https://travis-ci.org/ruby-hl7/ruby-hl7]
4
+ {<img src="https://codeclimate.com/github/ruby-hl7/ruby-hl7.png" />}[https://codeclimate.com/github/ruby-hl7/ruby-hl7]
5
+
3
6
  A simple way to parse and create HL7 2.x messages with Ruby.
4
7
 
5
8
  Examples can be found in HL7::Message.
@@ -7,17 +10,14 @@ Examples can be found in HL7::Message.
7
10
  The version id can be found in the HL7::VERSION constant.
8
11
 
9
12
  * Git: http://github.com/ruby-hl7/ruby-hl7.git
10
- * Docs: http://ruby-hl7.rubyforge.org
13
+ * Docs: http://rubydoc.info/gems/ruby-hl7/1.0.3/frames
11
14
  * Rubygems: https://rubygems.org/gems/ruby-hl7
12
- * Rubyforge: http://rubyforge.org/projects/ruby-hl7
13
15
 
14
16
  Lists
15
17
 
16
- * Developers: mailto:ruby-hl7-devel@rubyforge.org
17
- * Users: mailto:ruby-hl7-users@rubyforge.org
18
18
  * Google group: http://groups.google.com/group/ruby-hl7
19
19
 
20
- Copyright (C) 2006-2010 Mark Guzman
20
+ Copyright (C) 2006-2014 Mark Guzman
21
21
 
22
22
  Maintained by the Collaborative Software Initiative
23
23
 
@@ -0,0 +1,65 @@
1
+ module HL7Time
2
+ # Get a HL7 timestamp (type TS) for a Time instance.
3
+ #
4
+ # fraction_digits:: specifies a number of digits of fractional seconds.
5
+ # Its default value is 0.
6
+ # Time.parse('01:23').to_hl7
7
+ # => "20091202012300"
8
+ # Time.now.to_hl7(3)
9
+ # => "20091202153652.302"
10
+ def to_hl7( fraction_digits = 0)
11
+ strftime('%Y%m%d%H%M%S') + hl7_fractions(fraction_digits)
12
+ end
13
+
14
+ private
15
+ def hl7_fractions(fraction_digits = 0)
16
+ return '' unless fraction_digits > 0
17
+ time_fraction = hl7_time_fraction
18
+ answer = ".#{sprintf('%06d', time_fraction)}"
19
+ answer += '0' * ((fraction_digits - 6)).abs if fraction_digits > 6
20
+ answer[0, 1 + fraction_digits]
21
+ end
22
+
23
+ def hl7_time_fraction
24
+ if respond_to? :usec
25
+ usec
26
+ else
27
+ sec_fraction.to_f * 1000000
28
+ end
29
+ end
30
+ end
31
+
32
+ # Adding the to_hl7 method to the ruby Date class.
33
+ class Date
34
+ # Get a HL7 timestamp (type TS) for a Date instance.
35
+ #
36
+ # Date.parse('2009-12-02').to_hl7
37
+ # => "20091202"
38
+ def to_hl7
39
+ strftime('%Y%m%d')
40
+ end
41
+ end
42
+
43
+ # Adding the to_hl7 method to the ruby Time class.
44
+ class Time
45
+ include HL7Time
46
+ end
47
+
48
+ # Adding the to_hl7 method to the ruby DateTime class.
49
+ class DateTime
50
+ include HL7Time
51
+ end
52
+
53
+ # TODO
54
+ # parse an hl7 formatted date
55
+ #def Date.from_hl7( hl7_date )
56
+ #end
57
+
58
+ #def Date.to_hl7_short( ruby_date )
59
+ #end
60
+
61
+ #def Date.to_hl7_med( ruby_date )
62
+ #end
63
+
64
+ #def Date.to_hl7_long( ruby_date )
65
+ #end
@@ -0,0 +1,6 @@
1
+ # Adding a helper to the String class for the batch parse.
2
+ class String
3
+ def hl7_batch?
4
+ !!match(/^FHS/)
5
+ end
6
+ end
data/lib/message.rb ADDED
@@ -0,0 +1,272 @@
1
+ # Ruby Object representation of an hl7 2.x message
2
+ # the message object is actually a "smart" collection of hl7 segments
3
+ # == Examples
4
+ #
5
+ # ==== Creating a new HL7 message
6
+ #
7
+ # # create a message
8
+ # msg = HL7::Message.new
9
+ #
10
+ # # create a MSH segment for our new message
11
+ # msh = HL7::Message::Segment::MSH.new
12
+ # msh.recv_app = "ruby hl7"
13
+ # msh.recv_facility = "my office"
14
+ # msh.processing_id = rand(10000).to_s
15
+ #
16
+ # msg << msh # add the MSH segment to the message
17
+ #
18
+ # puts msg.to_s # readable version of the message
19
+ #
20
+ # puts msg.to_hl7 # hl7 version of the message (as a string)
21
+ #
22
+ # puts msg.to_mllp # mllp version of the message (as a string)
23
+ #
24
+ # ==== Parse an existing HL7 message
25
+ #
26
+ # raw_input = open( "my_hl7_msg.txt" ).readlines
27
+ # msg = HL7::Message.new( raw_input )
28
+ #
29
+ # puts "message type: %s" % msg[:MSH].message_type
30
+ #
31
+ #
32
+ class HL7::Message
33
+ include Enumerable # we treat an hl7 2.x message as a collection of segments
34
+ extend HL7::MessageBatchParser
35
+
36
+ attr_reader :message_parser
37
+ attr :element_delim
38
+ attr :item_delim
39
+ attr :segment_delim
40
+ attr :delimiter
41
+
42
+ # setup a new hl7 message
43
+ # raw_msg:: is an optional object containing an hl7 message
44
+ # it can either be a string or an Enumerable object
45
+ def initialize( raw_msg=nil, &blk )
46
+ @segments = []
47
+ @segments_by_name = {}
48
+ @item_delim = "^"
49
+ @element_delim = '|'
50
+ @segment_delim = "\r"
51
+ @delimiter = HL7::Message::Delimiter.new( @element_delim,
52
+ @item_delim,
53
+ @segment_delim)
54
+
55
+ @message_parser = HL7::MessageParser.new(@delimiter)
56
+
57
+ parse( raw_msg ) if raw_msg
58
+
59
+ if block_given?
60
+ blk.call self
61
+ end
62
+ end
63
+
64
+ def parse( inobj )
65
+ if inobj.kind_of?(String)
66
+ generate_segments( message_parser.parse_string( inobj ))
67
+ elsif inobj.respond_to?(:each)
68
+ generate_segments_enumerable(inobj)
69
+ else
70
+ raise HL7::ParseError.new( "object to parse should be string or enumerable" )
71
+ end
72
+ end
73
+
74
+ def generate_segments_enumerable(enumerable)
75
+ enumerable.each do |segment|
76
+ generate_segments( message_parser.parse_string( segment.to_s ))
77
+ end
78
+ end
79
+
80
+ # access a segment of the message
81
+ # index:: can be a Range, Fixnum or anything that
82
+ # responds to to_sym
83
+ def []( index )
84
+ ret = nil
85
+
86
+ if index.kind_of?(Range) || index.kind_of?(Fixnum)
87
+ ret = @segments[ index ]
88
+ elsif (index.respond_to? :to_sym)
89
+ ret = @segments_by_name[ index.to_sym ]
90
+ ret = ret.first if ret && ret.length == 1
91
+ end
92
+
93
+ ret
94
+ end
95
+
96
+ # modify a segment of the message
97
+ # index:: can be a Range, Fixnum or anything that
98
+ # responds to to_sym
99
+ # value:: an HL7::Message::Segment object
100
+ def []=( index, value )
101
+ unless ( value && value.kind_of?(HL7::Message::Segment) )
102
+ raise HL7::Exception.new( "attempting to assign something other than an HL7 Segment" )
103
+ end
104
+
105
+ if index.kind_of?(Range) || index.kind_of?(Fixnum)
106
+ @segments[ index ] = value
107
+ elsif index.respond_to?(:to_sym)
108
+ (@segments_by_name[ index.to_sym ] ||= []) << value
109
+ else
110
+ raise HL7::Exception.new( "attempting to use an indice that is not a Range, Fixnum or to_sym providing object" )
111
+ end
112
+
113
+ value.segment_parent = self
114
+ end
115
+
116
+ # return the index of the value if it exists, nil otherwise
117
+ # value:: is expected to be a string
118
+ def index( value )
119
+ return nil unless (value && value.respond_to?(:to_sym))
120
+
121
+ segs = @segments_by_name[ value.to_sym ]
122
+ return nil unless segs
123
+
124
+ @segments.index( segs.to_a.first )
125
+ end
126
+
127
+ # add a segment or array of segments to the message
128
+ # * will force auto set_id sequencing for segments containing set_id's
129
+ def <<( value )
130
+ # do nothing if value is nil
131
+ return unless value
132
+
133
+ if value.kind_of? Array
134
+ value.map{|item| append(item)}
135
+ else
136
+ append(value)
137
+ end
138
+ end
139
+
140
+ def append( value )
141
+ unless ( value && value.kind_of?(HL7::Message::Segment) )
142
+ raise HL7::Exception.new( "attempting to append something other than an HL7 Segment" )
143
+ end
144
+
145
+ value.segment_parent = self unless value.segment_parent
146
+ (@segments ||= []) << value
147
+ name = value.class.to_s.gsub("HL7::Message::Segment::", "").to_sym
148
+ (@segments_by_name[ name ] ||= []) << value
149
+ sequence_segments unless @parsing # let's auto-set the set-id as we go
150
+ end
151
+
152
+ # yield each segment in the message
153
+ def each # :yields: segment
154
+ return unless @segments
155
+ @segments.each { |s| yield s }
156
+ end
157
+
158
+ # return the segment count
159
+ def length
160
+ 0 unless @segments
161
+ @segments.length
162
+ end
163
+
164
+ # provide a screen-readable version of the message
165
+ def to_s
166
+ @segments.collect { |s| s if s.to_s.length > 0 }.join( "\n" )
167
+ end
168
+
169
+ # provide a HL7 spec version of the message
170
+ def to_hl7
171
+ @segments.collect { |s| s if s.to_s.length > 0 }.join( @delimiter.segment )
172
+ end
173
+
174
+ # provide the HL7 spec version of the message wrapped in MLLP
175
+ def to_mllp
176
+ pre_mllp = to_hl7
177
+ "\x0b" + pre_mllp + "\x1c\r"
178
+ end
179
+
180
+ # auto-set the set_id fields of any message segments that
181
+ # provide it and have more than one instance in the message
182
+ def sequence_segments(base=nil)
183
+ last = nil
184
+ segs = @segments
185
+ segs = base.children if base
186
+
187
+ segs.each do |s|
188
+ if s.kind_of?( last.class ) && s.respond_to?( :set_id )
189
+ last.set_id = 1 unless last.set_id && last.set_id.to_i > 0
190
+ s.set_id = last.set_id.to_i + 1
191
+ end
192
+
193
+ sequence_segments( s ) if s.has_children?
194
+
195
+ last = s
196
+ end
197
+ end
198
+
199
+ private
200
+ def generate_segments( ary )
201
+ raise HL7::ParseError.new( "no array to generate segments" ) unless ary.length > 0
202
+
203
+ @parsing = true
204
+ segment_stack = []
205
+ ary.each do |elm|
206
+ if elm.slice(0,3) == "MSH"
207
+ update_delimiters(elm)
208
+ end
209
+ last_seg = generate_segment( elm, segment_stack ) || last_seg
210
+ end
211
+ @parsing = nil
212
+ end
213
+
214
+ def update_delimiters(elm)
215
+ @item_delim = message_parser.parse_item_delim(elm)
216
+ @element_delim = message_parser.parse_element_delim(elm)
217
+ @delimiter.item = @item_delim
218
+ @delimiter.element = @element_delim
219
+ end
220
+
221
+ def generate_segment( elm, segment_stack )
222
+ segment_generator = HL7::Message::SegmentGenerator.new( elm,
223
+ segment_stack,
224
+ @delimiter )
225
+
226
+ return nil unless segment_generator.valid_segments_parts?
227
+ segment_generator.seg_name = segment_generator.seg_parts[0]
228
+
229
+ new_seg = segment_generator.build
230
+ new_seg.segment_parent = self
231
+
232
+ choose_segment_from(segment_stack, new_seg, segment_generator.seg_name)
233
+ end
234
+
235
+ def choose_segment_from(segment_stack, new_seg, seg_name)
236
+
237
+ # Segments have been previously seen
238
+ while(segment_stack.length > 0)
239
+ if segment_stack.last && segment_stack.last.has_children? && segment_stack.last.accepts?( seg_name )
240
+ # If a previous segment can accept the current segment as a child,
241
+ # add it to the previous segments children
242
+ segment_stack.last.children << new_seg
243
+ new_seg.is_child_segment = true
244
+ segment_stack << new_seg
245
+ break;
246
+ else
247
+ segment_stack.pop
248
+ end
249
+ end
250
+
251
+ # Root of segment 'tree'
252
+ if segment_stack.length == 0
253
+ @segments << new_seg
254
+ segment_stack << new_seg
255
+ setup_segment_lookup_by_name( seg_name, new_seg)
256
+ end
257
+ end
258
+
259
+ # Allow segment lookup by name
260
+ def setup_segment_lookup_by_name(seg_name, new_seg)
261
+ seg_sym = get_symbol_from_name(seg_name)
262
+ if seg_sym
263
+ @segments_by_name[ seg_sym ] ||= []
264
+ @segments_by_name[ seg_sym ] << new_seg
265
+ end
266
+ end
267
+
268
+ def get_symbol_from_name(seg_name)
269
+ seg_name.to_s.strip.length > 0 ? seg_name.to_sym : nil
270
+ end
271
+
272
+ end
@@ -0,0 +1,75 @@
1
+ module HL7::MessageBatchParser
2
+ def parse_batch(batch) # :yields: message
3
+ raise HL7::ParseError, 'badly_formed_batch_message' unless
4
+ batch.hl7_batch?
5
+
6
+ batch = clean_batch_for_jruby batch
7
+
8
+ raise HL7::ParseError, 'empty_batch_message' unless
9
+ match = /\rMSH/.match(batch)
10
+
11
+ match.post_match.split(/\rMSH/).each do |_msg|
12
+ if md = /\rBTS/.match(_msg)
13
+ # TODO: Validate the message count in the BTS segment
14
+ # should == index + 1
15
+ _msg = md.pre_match
16
+ end
17
+
18
+ yield 'MSH' + _msg
19
+ end
20
+ end
21
+
22
+ # parse a String or Enumerable object into an HL7::Message if possible
23
+ # * returns a new HL7::Message if successful
24
+ def parse( inobj )
25
+ HL7::Message.new do |msg|
26
+ msg.parse( inobj )
27
+ end
28
+ end
29
+
30
+ # JRuby seems to change our literal \r characters in sample
31
+ # messages (from here documents) into newlines. We make a copy
32
+ # here, reverting to carriage returns. The input is unchanged.
33
+ #
34
+ # This does not occur when posts are received with CR
35
+ # characters, only in sample messages from here documents. The
36
+ # expensive copy is only incurred when the batch message has a
37
+ # newline character in it.
38
+ private
39
+
40
+ def clean_batch_for_jruby(batch)
41
+ batch.gsub("\n", "\r") if batch.include?("\n")
42
+ end
43
+ end
44
+
45
+ # Provides basic methods to parse_string, element and item delimeter parser
46
+ class HL7::MessageParser
47
+ attr_reader :delimiter
48
+
49
+ def self.split_by_delimiter(element, delimiter)
50
+ element.split( delimiter, -1 )
51
+ end
52
+
53
+ def initialize(delimiter)
54
+ @delimiter = delimiter
55
+ end
56
+
57
+ # parse the provided String or Enumerable object into this message
58
+ def parse_string( instr )
59
+ post_mllp = instr
60
+ if /\x0b((:?.|\r|\n)+)\x1c\r/.match( instr )
61
+ post_mllp = $1 #strip the mllp bytes
62
+ end
63
+ HL7::MessageParser.split_by_delimiter(post_mllp, @delimiter.segment)
64
+ end
65
+
66
+ # Get the element delimiter from an MSH segment
67
+ def parse_element_delim(str)
68
+ (str && str.kind_of?(String)) ? str.slice(3,1) : "|"
69
+ end
70
+
71
+ # Get the item delimiter from an MSH segment
72
+ def parse_item_delim(str)
73
+ (str && str.kind_of?(String)) ? str.slice(4,1) : "^"
74
+ end
75
+ end