pipehat 0.1.0

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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rubocop.yml +18 -0
  4. data/.travis.yml +10 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +8 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +184 -0
  9. data/Rakefile +10 -0
  10. data/bin/console +10 -0
  11. data/bin/setup +8 -0
  12. data/lib/pipehat.rb +22 -0
  13. data/lib/pipehat/component/base.rb +30 -0
  14. data/lib/pipehat/define_type.rb +68 -0
  15. data/lib/pipehat/field/base.rb +59 -0
  16. data/lib/pipehat/message.rb +32 -0
  17. data/lib/pipehat/node.rb +53 -0
  18. data/lib/pipehat/parser.rb +149 -0
  19. data/lib/pipehat/repeat/base.rb +31 -0
  20. data/lib/pipehat/segment/aip.rb +21 -0
  21. data/lib/pipehat/segment/ais.rb +21 -0
  22. data/lib/pipehat/segment/al1.rb +15 -0
  23. data/lib/pipehat/segment/base.rb +124 -0
  24. data/lib/pipehat/segment/dg1.rb +30 -0
  25. data/lib/pipehat/segment/err.rb +21 -0
  26. data/lib/pipehat/segment/evn.rb +16 -0
  27. data/lib/pipehat/segment/in1.rb +64 -0
  28. data/lib/pipehat/segment/msa.rb +17 -0
  29. data/lib/pipehat/segment/msh.rb +52 -0
  30. data/lib/pipehat/segment/obr.rb +63 -0
  31. data/lib/pipehat/segment/obx.rb +37 -0
  32. data/lib/pipehat/segment/orc.rb +43 -0
  33. data/lib/pipehat/segment/pid.rb +49 -0
  34. data/lib/pipehat/segment/prd.rb +23 -0
  35. data/lib/pipehat/segment/pv1.rb +63 -0
  36. data/lib/pipehat/segment/rf1.rb +34 -0
  37. data/lib/pipehat/segment/rgs.rb +12 -0
  38. data/lib/pipehat/segment/sch.rb +36 -0
  39. data/lib/pipehat/subcomponent/base.rb +27 -0
  40. data/lib/pipehat/types/aui.rb +8 -0
  41. data/lib/pipehat/types/ce.rb +11 -0
  42. data/lib/pipehat/types/cne.rb +27 -0
  43. data/lib/pipehat/types/cnn.rb +16 -0
  44. data/lib/pipehat/types/cp.rb +11 -0
  45. data/lib/pipehat/types/cq.rb +7 -0
  46. data/lib/pipehat/types/cwe.rb +27 -0
  47. data/lib/pipehat/types/cx.rb +17 -0
  48. data/lib/pipehat/types/dld.rb +7 -0
  49. data/lib/pipehat/types/dt.rb +4 -0
  50. data/lib/pipehat/types/dtm.rb +4 -0
  51. data/lib/pipehat/types/ei.rb +9 -0
  52. data/lib/pipehat/types/eip.rb +7 -0
  53. data/lib/pipehat/types/erl.rb +11 -0
  54. data/lib/pipehat/types/fn.rb +10 -0
  55. data/lib/pipehat/types/hd.rb +8 -0
  56. data/lib/pipehat/types/id.rb +4 -0
  57. data/lib/pipehat/types/is.rb +4 -0
  58. data/lib/pipehat/types/mo.rb +7 -0
  59. data/lib/pipehat/types/moc.rb +7 -0
  60. data/lib/pipehat/types/msg.rb +8 -0
  61. data/lib/pipehat/types/ndl.rb +16 -0
  62. data/lib/pipehat/types/nm.rb +4 -0
  63. data/lib/pipehat/types/pl.rb +16 -0
  64. data/lib/pipehat/types/pln.rb +9 -0
  65. data/lib/pipehat/types/prl.rb +8 -0
  66. data/lib/pipehat/types/pt.rb +7 -0
  67. data/lib/pipehat/types/rc.rb +7 -0
  68. data/lib/pipehat/types/sad.rb +8 -0
  69. data/lib/pipehat/types/si.rb +4 -0
  70. data/lib/pipehat/types/snm.rb +4 -0
  71. data/lib/pipehat/types/st.rb +4 -0
  72. data/lib/pipehat/types/ts.rb +7 -0
  73. data/lib/pipehat/types/tx.rb +4 -0
  74. data/lib/pipehat/types/varies.rb +28 -0
  75. data/lib/pipehat/types/vid.rb +8 -0
  76. data/lib/pipehat/types/xad.rb +28 -0
  77. data/lib/pipehat/types/xcn.rb +30 -0
  78. data/lib/pipehat/types/xon.rb +15 -0
  79. data/lib/pipehat/types/xpn.rb +20 -0
  80. data/lib/pipehat/types/xtn.rb +22 -0
  81. data/lib/pipehat/version.rb +3 -0
  82. data/pipehat.gemspec +27 -0
  83. metadata +127 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipehat
4
+ module Field
5
+ class Base < Pipehat::Node
6
+ def initialize(segment, fnum)
7
+ @segment = segment
8
+ @fnum = fnum
9
+ end
10
+
11
+ attr_reader :segment, :fnum
12
+
13
+ def repeat(rnum)
14
+ repeat_class.new(segment, fnum, rnum)
15
+ end
16
+
17
+ def [](rnum)
18
+ repeat(rnum)
19
+ end
20
+
21
+ def []=(rnum, value)
22
+ repeat(rnum).set(value)
23
+ end
24
+
25
+ def first
26
+ repeat(1)
27
+ end
28
+
29
+ # Set on a field should replace the entire tree at this field
30
+ # This should discard anything under repeats etc
31
+ def set(value)
32
+ segment.set_field(fnum, value)
33
+ end
34
+
35
+ # calling component(n) on a field assumes you mean the first repeat
36
+ def component(cnum, type = Pipehat::Component::Base)
37
+ first.component(cnum, type)
38
+ end
39
+
40
+ def to_hl7
41
+ (segment.tree(fnum) || []).map do |repeat|
42
+ (repeat || []).map do |component|
43
+ (component || []).join(parser.subcomponent_sep)
44
+ end.join(parser.component_sep)
45
+ end.join(parser.repetition_sep)
46
+ end
47
+
48
+ def inspect
49
+ inspect_node(fnum)
50
+ end
51
+
52
+ private
53
+
54
+ def repeat_class
55
+ Object.const_get(self.class.name.sub(/Field/, "Repeat"))
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipehat
4
+ class Message
5
+ def initialize(hl7 = "", parser: Pipehat::DEFAULT_PARSER)
6
+ @parser = parser
7
+ @segments = parser.parse(hl7)
8
+ end
9
+
10
+ attr_reader :parser
11
+
12
+ def to_hl7
13
+ segments.map(&:to_hl7).join(parser.segment_sep)
14
+ end
15
+
16
+ def <<(segment)
17
+ @segments << segment
18
+ end
19
+
20
+ # Returns an enumerator over the message's segments
21
+ # The optional parameter limits it to the given type
22
+ def segments(type = nil)
23
+ return to_enum(:segments, type) unless block_given?
24
+
25
+ @segments.each do |segment|
26
+ next if type && segment.segment_name != type.to_s
27
+
28
+ yield segment
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipehat
4
+ # Parent class of all accessor types (Field, Repeat, (Sub)Component
5
+ # providing common methods
6
+ class Node
7
+ def fnum
8
+ 1
9
+ end
10
+
11
+ def rnum
12
+ 1
13
+ end
14
+
15
+ def cnum
16
+ 1
17
+ end
18
+
19
+ def snum
20
+ 1
21
+ end
22
+
23
+ def to_s
24
+ segment.parser.unescape(unescaped)
25
+ end
26
+
27
+ def unescaped
28
+ segment.get(fnum, rnum, cnum, snum)
29
+ end
30
+
31
+ def component_names
32
+ self.class.component_names
33
+ end
34
+
35
+ def self.component_names
36
+ @component_names ||= []
37
+ end
38
+
39
+ def parser
40
+ segment.parser
41
+ end
42
+
43
+ private
44
+
45
+ def inspect_node(*nums)
46
+ s = "#<#{self.class} #{segment.segment_name}(#{nums.join(",")}) "
47
+ maxlen = 76 - s.length
48
+ fragment = to_hl7
49
+ fragment = fragment[0, maxlen - 3] + "..." if fragment.length > maxlen
50
+ "#{s}#{fragment}>"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Pipehat
6
+ class Parser
7
+ def segment_sep
8
+ "\r"
9
+ end
10
+
11
+ def field_sep
12
+ "|"
13
+ end
14
+
15
+ def repetition_sep
16
+ "~"
17
+ end
18
+
19
+ def component_sep
20
+ "^"
21
+ end
22
+
23
+ def subcomponent_sep
24
+ "&"
25
+ end
26
+
27
+ def escape_char
28
+ "\\"
29
+ end
30
+
31
+ def escaped_field_sep
32
+ "#{escape_char}F#{escape_char}"
33
+ end
34
+
35
+ def escaped_repetition_sep
36
+ "#{escape_char}R#{escape_char}"
37
+ end
38
+
39
+ def escaped_component_sep
40
+ "#{escape_char}S#{escape_char}"
41
+ end
42
+
43
+ def escaped_subcomponent_sep
44
+ "#{escape_char}T#{escape_char}"
45
+ end
46
+
47
+ def escaped_escape_char
48
+ "#{escape_char}E#{escape_char}"
49
+ end
50
+
51
+ def escaped_field_sep_regex
52
+ @escaped_field_sep_regex ||= Regexp.new Regexp.quote escaped_field_sep
53
+ end
54
+
55
+ def escaped_repetition_sep_regex
56
+ @escaped_repetition_sep_regex ||= Regexp.new Regexp.quote escaped_repetition_sep
57
+ end
58
+
59
+ def escaped_component_sep_regex
60
+ @escaped_component_sep_regex ||= Regexp.new Regexp.quote escaped_component_sep
61
+ end
62
+
63
+ def escaped_subcomponent_sep_regex
64
+ @escaped_subcomponent_sep_regex ||= Regexp.new Regexp.quote escaped_subcomponent_sep
65
+ end
66
+
67
+ def escaped_escape_char_regex
68
+ @escaped_escape_char_regex ||= Regexp.new Regexp.quote escaped_escape_char
69
+ end
70
+
71
+ def escaped_hex_regex
72
+ @escaped_hex_regex ||= Regexp.new "#{Regexp.quote escape_char}X\\h+#{Regexp.quote escape_char}"
73
+ end
74
+
75
+ def msh2
76
+ @msh2 ||= [
77
+ component_sep,
78
+ repetition_sep,
79
+ escape_char,
80
+ subcomponent_sep
81
+ ].join
82
+ end
83
+
84
+ def escape(string)
85
+ return "" if string.nil?
86
+
87
+ string.each_char.map do |chr|
88
+ case chr
89
+ when field_sep then escaped_field_sep
90
+ when repetition_sep then escaped_repetition_sep
91
+ when component_sep then escaped_component_sep
92
+ when subcomponent_sep then escaped_subcomponent_sep
93
+ when escape_char then escaped_escape_char
94
+ else
95
+ if (32.chr..127.chr).cover?(chr)
96
+ chr
97
+ else
98
+ format("\\X%02X\\", chr.ord)
99
+ end
100
+ end
101
+ end.join
102
+ end
103
+
104
+ def unescape(string)
105
+ return "" if string.nil?
106
+ return string unless string.include?(escape_char)
107
+
108
+ s = StringScanner.new(string)
109
+ out = String.new
110
+ until s.eos?
111
+ if s.scan(escaped_field_sep_regex)
112
+ out << field_sep
113
+ elsif s.scan(escaped_repetition_sep_regex)
114
+ out << repetition_sep
115
+ elsif s.scan(escaped_component_sep_regex)
116
+ out << component_sep
117
+ elsif s.scan(escaped_subcomponent_sep_regex)
118
+ out << subcomponent_sep
119
+ elsif s.scan(escaped_escape_char_regex)
120
+ out << escape_char
121
+ elsif s.scan(escaped_hex_regex)
122
+ s.matched[2..-2].scan(/../).each { |hex| out << hex.to_i(16).chr }
123
+ else
124
+ out << s.getch
125
+ end
126
+ end
127
+ out
128
+ end
129
+
130
+ # Returns an array of Pipehat::Segments of the right class
131
+ def parse(hl7)
132
+ hl7.split(segment_sep).map do |seg|
133
+ name = seg[0, seg.index("|")]
134
+ klass = if Pipehat::Segment.const_defined?(name)
135
+ Pipehat::Segment.const_get(name)
136
+ else
137
+ Pipehat::Segment::Base
138
+ end
139
+ klass.new(seg, parser: self)
140
+ end
141
+ end
142
+
143
+ def inspect
144
+ "#<#{self.class} #{field_sep}#{msh2}>"
145
+ end
146
+ end
147
+
148
+ DEFAULT_PARSER = Parser.new
149
+ end
@@ -0,0 +1,31 @@
1
+ module Pipehat
2
+ module Repeat
3
+ class Base < Pipehat::Node
4
+ def initialize(segment, fnum, rnum)
5
+ @segment = segment
6
+ @fnum = fnum
7
+ @rnum = rnum
8
+ end
9
+
10
+ attr_reader :segment, :fnum, :rnum
11
+
12
+ def component(cnum, type = Pipehat::Component::Base)
13
+ type.new(segment, fnum, rnum, cnum)
14
+ end
15
+
16
+ def set(value)
17
+ segment.set_repeat(fnum, rnum, value)
18
+ end
19
+
20
+ def to_hl7
21
+ (segment.tree(fnum, rnum) || []).map do |component|
22
+ (component || []).join(parser.subcomponent_sep)
23
+ end.join(parser.component_sep)
24
+ end
25
+
26
+ def inspect
27
+ inspect_node(fnum, rnum)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipehat
4
+ module Segment
5
+ # Appointment Information - Personnel Resource
6
+ class AIP < Base
7
+ add_field :set_id, :SI
8
+ add_field :segment_action_code, :ID
9
+ add_field :personnel_resource_id, :XCN
10
+ add_field :resource_type, :CWE
11
+ add_field :resource_group, :CWE
12
+ add_field :start_date_time, :DTM
13
+ add_field :start_date_time_offset, :NM
14
+ add_field :start_date_time_offset_units, :CNE
15
+ add_field :duration, :NM
16
+ add_field :duration_units, :CNE
17
+ add_field :allow_substitution_code, :CWE
18
+ add_field :filler_status_code, :CWE
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipehat
4
+ module Segment
5
+ # Appointment Information
6
+ class AIS < Base
7
+ add_field :set_id, :SI
8
+ add_field :segment_action_code, :ID
9
+ add_field :universal_service_identifier, :CWE
10
+ add_field :start_date_time, :DTM
11
+ add_field :start_date_time_offset, :NM
12
+ add_field :start_date_time_offset_units, :CNE
13
+ add_field :duration, :NM
14
+ add_field :duration_units, :CNE
15
+ add_field :allow_substitution_code, :CWE
16
+ add_field :filler_status_code, :CWE
17
+ add_field :placer_supplemental_service_information, :CWE
18
+ add_field :filler_supplemental_service_information, :CWE
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipehat
4
+ module Segment
5
+ # Patient Allergy Information
6
+ class AL1 < Base
7
+ add_field :set_id, :SI
8
+ add_field :allergen_type_code, :CE
9
+ add_field :allergen_code, :CE
10
+ add_field :allergy_severity_code, :CE
11
+ add_field :allergy_reaction_code, :ST
12
+ add_field :identification_date, :DT
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipehat
4
+ module Segment
5
+ class Base
6
+ def initialize(string = nil, parser: Pipehat::DEFAULT_PARSER)
7
+ string ||= self.class.name.split("::").last
8
+ # storage is nested array (to depth of subcomponents). The unescaped
9
+ # strings is stored (escape/unescape occurs in accessors).
10
+ @data = string.split(parser.field_sep).map do |field|
11
+ field.split(parser.repetition_sep).map do |repeat|
12
+ repeat.split(parser.component_sep).map do |component|
13
+ component.split(parser.subcomponent_sep)
14
+ end
15
+ end
16
+ end
17
+ @parser = parser
18
+ end
19
+
20
+ # Returns a reference to a field by number
21
+ def field(fnum, type = Pipehat::Field::Base)
22
+ type.new(self, fnum)
23
+ end
24
+
25
+ # Return the raw (unescaped) string at the specified position.
26
+ # If the position doesn't exist, returns nil.
27
+ #
28
+ # Note this works by assuming @data always has arrays nesting to
29
+ # the subcomponent position
30
+ def get(fnum, rnum, cnum, snum)
31
+ tree(fnum, rnum, cnum, snum)
32
+ end
33
+
34
+ def tree(fnum, rnum = nil, cnum = nil, snum = nil)
35
+ tmp = @data.dig(fnum)
36
+ tmp = tmp&.dig(rnum - 1) if rnum
37
+ tmp = tmp&.dig(cnum - 1) if cnum
38
+ tmp = tmp&.dig(snum - 1) if snum
39
+ tmp
40
+ end
41
+
42
+ # TODO: set_* currently assume a string only. Would be good to accept
43
+ # and insert arrays (for nodes higher than subcomponents)
44
+ # There should also be a way to avoid the escaping (eg, FT values use
45
+ # escape sequences that shouldn't be escaped again).
46
+
47
+ def set_field(fnum, value)
48
+ @data[fnum] = [[[parser.escape(value)]]]
49
+ end
50
+
51
+ def set_repeat(fnum, rnum, value)
52
+ @data[fnum] ||= [[[]]]
53
+ @data[fnum][rnum - 1] = [[parser.escape(value)]]
54
+ end
55
+
56
+ def set_component(fnum, rnum, cnum, value)
57
+ @data[fnum] ||= [[[]]]
58
+ @data[fnum][rnum - 1] ||= [[]]
59
+ @data[fnum][rnum - 1][cnum - 1] = [parser.escape(value)]
60
+ end
61
+
62
+ def set_subcomponent(fnum, rnum, cnum, snum, value)
63
+ @data[fnum] ||= [[[]]]
64
+ @data[fnum][rnum - 1] ||= [[]]
65
+ @data[fnum][rnum - 1][cnum - 1] ||= []
66
+ @data[fnum][rnum - 1][cnum - 1][snum - 1] = parser.escape(value)
67
+ end
68
+
69
+ def field_names
70
+ self.class.field_names
71
+ end
72
+
73
+ def segment_name
74
+ field(0).to_s
75
+ end
76
+
77
+ attr_accessor :parser
78
+
79
+ def to_hl7
80
+ @data.map do |field|
81
+ (field || []).map do |repeat|
82
+ (repeat || []).map do |component|
83
+ (component || []).join(parser.subcomponent_sep)
84
+ end.join(parser.component_sep)
85
+ end.join(parser.repetition_sep)
86
+ end.join(parser.field_sep)
87
+ end
88
+
89
+ class << self
90
+ # returns a list of the fields define on this segment as symbols
91
+ def field_names
92
+ @field_names ||= []
93
+ end
94
+
95
+ def add_field(name, type, options = {})
96
+ field_names << name
97
+ count = field_names.size
98
+ klass = Object.const_get("Pipehat::Field::#{type}")
99
+
100
+ invalid_options = options.keys - %i[setter]
101
+ raise "Invalid options: #{invalid_options.join(", ")}" if invalid_options.any?
102
+
103
+ define_method name do
104
+ field(count, klass)
105
+ end
106
+
107
+ return unless options.fetch(:setter, true)
108
+
109
+ define_method "#{name}=" do |value|
110
+ send(name).set(value)
111
+ end
112
+ end
113
+ end
114
+
115
+ def inspect
116
+ s = "#<#{self.class} "
117
+ maxlen = 76 - s.length
118
+ fragment = to_hl7
119
+ fragment = fragment[0, maxlen - 3] + "..." if fragment.length > maxlen
120
+ "#{s}#{fragment}>"
121
+ end
122
+ end
123
+ end
124
+ end