hqmf-parser 1.0.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 (49) hide show
  1. data/Gemfile +23 -0
  2. data/README.md +903 -0
  3. data/Rakefile +19 -0
  4. data/VERSION +1 -0
  5. data/lib/hqmf-generator/hqmf-generator.rb +308 -0
  6. data/lib/hqmf-model/attribute.rb +35 -0
  7. data/lib/hqmf-model/data_criteria.rb +322 -0
  8. data/lib/hqmf-model/document.rb +172 -0
  9. data/lib/hqmf-model/population_criteria.rb +90 -0
  10. data/lib/hqmf-model/precondition.rb +85 -0
  11. data/lib/hqmf-model/types.rb +318 -0
  12. data/lib/hqmf-model/utilities.rb +52 -0
  13. data/lib/hqmf-parser.rb +54 -0
  14. data/lib/hqmf-parser/1.0/attribute.rb +68 -0
  15. data/lib/hqmf-parser/1.0/comparison.rb +34 -0
  16. data/lib/hqmf-parser/1.0/data_criteria.rb +105 -0
  17. data/lib/hqmf-parser/1.0/document.rb +209 -0
  18. data/lib/hqmf-parser/1.0/expression.rb +52 -0
  19. data/lib/hqmf-parser/1.0/population_criteria.rb +79 -0
  20. data/lib/hqmf-parser/1.0/precondition.rb +89 -0
  21. data/lib/hqmf-parser/1.0/range.rb +65 -0
  22. data/lib/hqmf-parser/1.0/restriction.rb +157 -0
  23. data/lib/hqmf-parser/1.0/utilities.rb +41 -0
  24. data/lib/hqmf-parser/2.0/data_criteria.rb +319 -0
  25. data/lib/hqmf-parser/2.0/document.rb +165 -0
  26. data/lib/hqmf-parser/2.0/population_criteria.rb +53 -0
  27. data/lib/hqmf-parser/2.0/precondition.rb +44 -0
  28. data/lib/hqmf-parser/2.0/types.rb +223 -0
  29. data/lib/hqmf-parser/2.0/utilities.rb +30 -0
  30. data/lib/hqmf-parser/converter/pass1/data_criteria_converter.rb +254 -0
  31. data/lib/hqmf-parser/converter/pass1/document_converter.rb +183 -0
  32. data/lib/hqmf-parser/converter/pass1/population_criteria_converter.rb +135 -0
  33. data/lib/hqmf-parser/converter/pass1/precondition_converter.rb +164 -0
  34. data/lib/hqmf-parser/converter/pass1/precondition_extractor.rb +159 -0
  35. data/lib/hqmf-parser/converter/pass1/simple_data_criteria.rb +35 -0
  36. data/lib/hqmf-parser/converter/pass1/simple_operator.rb +89 -0
  37. data/lib/hqmf-parser/converter/pass1/simple_population_criteria.rb +10 -0
  38. data/lib/hqmf-parser/converter/pass1/simple_precondition.rb +63 -0
  39. data/lib/hqmf-parser/converter/pass1/simple_restriction.rb +64 -0
  40. data/lib/hqmf-parser/converter/pass2/comparison_converter.rb +91 -0
  41. data/lib/hqmf-parser/converter/pass2/operator_converter.rb +169 -0
  42. data/lib/hqmf-parser/converter/pass3/specific_occurrence_converter.rb +86 -0
  43. data/lib/hqmf-parser/converter/pass3/specific_occurrence_converter_bak.rb +70 -0
  44. data/lib/hqmf-parser/parser.rb +22 -0
  45. data/lib/hqmf-parser/value_sets/value_set_parser.rb +206 -0
  46. data/lib/tasks/coverme.rake +8 -0
  47. data/lib/tasks/hqmf.rake +141 -0
  48. data/lib/tasks/value_sets.rake +23 -0
  49. metadata +159 -0
@@ -0,0 +1,52 @@
1
+
2
+ module HQMF
3
+ module Conversion
4
+ module Utilities
5
+ def build_hash(source, elements)
6
+ hash = {}
7
+ elements.each do |element|
8
+ value = source.send(element)
9
+ hash[element] = value unless value.nil?
10
+ end
11
+ hash
12
+ end
13
+
14
+ def json_array(elements)
15
+ return nil if elements.nil?
16
+ array = []
17
+ elements.each do |element|
18
+ if (element.is_a? OpenStruct)
19
+ array << openstruct_to_json(element)
20
+ else
21
+ array << element.to_json
22
+ end
23
+ end
24
+ array.compact!
25
+ (array.empty?) ? nil : array
26
+ end
27
+
28
+ def openstruct_to_json(element)
29
+ json = {}
30
+ element.marshal_dump.each do |key,value|
31
+ if value.is_a? OpenStruct
32
+ json[key] = openstruct_to_json(value)
33
+ elsif (value.class.to_s.split("::").first.start_with? 'HQMF')
34
+ json[key] = value.to_json
35
+ else
36
+ json[key] = value
37
+ end
38
+ end
39
+ json
40
+ end
41
+
42
+ def check_equality(left,right)
43
+ same = true
44
+ left.instance_variables.each do |variable|
45
+ same &&= left.instance_variable_get(variable) == right.instance_variable_get(variable)
46
+ end
47
+ same
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,54 @@
1
+ # require
2
+ require 'nokogiri'
3
+ require 'pry'
4
+ require 'json'
5
+ require 'ostruct'
6
+ require 'health-data-standards'
7
+
8
+ # require_relative
9
+ require_relative 'hqmf-model/utilities.rb'
10
+
11
+ require_relative 'hqmf-parser/1.0/utilities'
12
+ require_relative 'hqmf-parser/1.0/range'
13
+ require_relative 'hqmf-parser/1.0/document'
14
+ require_relative 'hqmf-parser/1.0/data_criteria'
15
+ require_relative 'hqmf-parser/1.0/attribute'
16
+ require_relative 'hqmf-parser/1.0/population_criteria'
17
+ require_relative 'hqmf-parser/1.0/precondition'
18
+ require_relative 'hqmf-parser/1.0/restriction'
19
+ require_relative 'hqmf-parser/1.0/comparison'
20
+ require_relative 'hqmf-parser/1.0/expression'
21
+
22
+ require_relative 'hqmf-parser/2.0/utilities'
23
+ require_relative 'hqmf-parser/2.0/types'
24
+ require_relative 'hqmf-parser/2.0/document'
25
+ require_relative 'hqmf-parser/2.0/data_criteria'
26
+ require_relative 'hqmf-parser/2.0/population_criteria'
27
+ require_relative 'hqmf-parser/2.0/precondition'
28
+
29
+ require_relative 'hqmf-model/data_criteria.rb'
30
+ require_relative 'hqmf-model/document.rb'
31
+ require_relative 'hqmf-model/population_criteria.rb'
32
+ require_relative 'hqmf-model/precondition.rb'
33
+ require_relative 'hqmf-model/types.rb'
34
+ require_relative 'hqmf-model/attribute.rb'
35
+
36
+ require_relative 'hqmf-parser/converter/pass1/document_converter'
37
+ require_relative 'hqmf-parser/converter/pass1/data_criteria_converter'
38
+ require_relative 'hqmf-parser/converter/pass1/population_criteria_converter'
39
+ require_relative 'hqmf-parser/converter/pass1/precondition_converter'
40
+ require_relative 'hqmf-parser/converter/pass1/precondition_extractor'
41
+ require_relative 'hqmf-parser/converter/pass1/simple_restriction'
42
+ require_relative 'hqmf-parser/converter/pass1/simple_operator'
43
+ require_relative 'hqmf-parser/converter/pass1/simple_precondition'
44
+ require_relative 'hqmf-parser/converter/pass1/simple_data_criteria'
45
+ require_relative 'hqmf-parser/converter/pass1/simple_population_criteria'
46
+
47
+ require_relative 'hqmf-parser/converter/pass2/comparison_converter'
48
+ require_relative 'hqmf-parser/converter/pass2/operator_converter'
49
+
50
+ require_relative 'hqmf-parser/value_sets/value_set_parser'
51
+
52
+ require_relative 'hqmf-parser/parser'
53
+
54
+ require_relative 'hqmf-generator/hqmf-generator'
@@ -0,0 +1,68 @@
1
+ module HQMF1
2
+ # Represents a HQMF measure attribute
3
+ class Attribute
4
+
5
+ include HQMF1::Utilities
6
+
7
+ # Create a new instance based on the supplied HQMF
8
+ # @param [Nokogiri::XML::Element] entry the measure attribute element
9
+ def initialize(entry)
10
+ @entry = entry
11
+ end
12
+
13
+ # Get the attribute code
14
+ # @return [String] the code
15
+ def code
16
+ if (@entry.at_xpath('./cda:code/@code'))
17
+ @entry.at_xpath('./cda:code/@code').value
18
+ elsif @entry.at_xpath('./cda:code/@nullFlavor')
19
+ @entry.at_xpath('./cda:code/@nullFlavor').value
20
+ end
21
+ end
22
+
23
+ # Get the attribute name
24
+ # @return [String] the name
25
+ def name
26
+ if (@entry.at_xpath('./cda:code/@displayName'))
27
+ @entry.at_xpath('./cda:code/@displayName').value
28
+ elsif @entry.at_xpath('cda:code/cda:originalText')
29
+ @entry.at_xpath('cda:code/cda:originalText').text
30
+ end
31
+ end
32
+
33
+ # Get the attribute id, used elsewhere in the document to refer to the attribute
34
+ # @return [String] the id
35
+ def id
36
+ attr_val('./cda:id/@root')
37
+ end
38
+
39
+ # Get the attribute value
40
+ # @return [String] the value
41
+ def value
42
+ val = attr_val('./cda:value/@value')
43
+ val ||= attr_val('./cda:value/@extension')
44
+ if val
45
+ val
46
+ else
47
+ @entry.at_xpath('./cda:value').inner_text
48
+ end
49
+ end
50
+
51
+ # Get the unit of the attribute value or nil if none is defined
52
+ # @return [String] the unit
53
+ def unit
54
+ attr_val('./cda:value/@unit')
55
+ end
56
+
57
+ # Get a JS friendly constant name for this measure attribute
58
+ def const_name
59
+ components = name.gsub(/\W/,' ').split.collect {|word| word.strip.upcase }
60
+ components.join '_'
61
+ end
62
+
63
+ def to_json
64
+ {self.const_name => build_hash(self, [:code,:value,:unit,:name,:id])}
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,34 @@
1
+ module HQMF1
2
+ class Comparison
3
+
4
+ include HQMF1::Utilities
5
+
6
+ attr_reader :restrictions, :data_criteria_id, :title, :subset
7
+
8
+ def initialize(data_criteria_id, entry, parent, doc)
9
+ @doc = doc
10
+ @data_criteria_id = data_criteria_id
11
+ @entry = entry
12
+ title_def = @entry.at_xpath('./*/cda:title')
13
+ if title_def
14
+ @title = title_def.inner_text
15
+ end
16
+ @restrictions = []
17
+ restriction_def = @entry.at_xpath('./*/cda:sourceOf')
18
+ if restriction_def
19
+ @entry.xpath('./*/cda:sourceOf').each do |restriction|
20
+ @restrictions << Restriction.new(restriction, self, @doc)
21
+ end
22
+ end
23
+ end
24
+
25
+ def to_json
26
+
27
+ json = build_hash(self, [:data_criteria_id,:title,:subset])
28
+ json[:restrictions] = json_array(@restrictions)
29
+ json
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,105 @@
1
+ module HQMF1
2
+ # Represents a data criteria specification
3
+ class DataCriteria
4
+
5
+ include HQMF1::Utilities
6
+
7
+ attr_accessor :code_list_id, :derived_from, :definition, :status, :negation, :specific_occurrence, :specific_occurrence_const
8
+
9
+ # Create a new instance based on the supplied HQMF entry
10
+ # @param [Nokogiri::XML::Element] entry the parsed HQMF entry
11
+ def initialize(entry)
12
+ @entry = entry
13
+
14
+ template_map = HQMF::DataCriteria.get_template_id_map()
15
+ oid_xpath_file = File.expand_path('../data_criteria_oid_xpath.json', __FILE__)
16
+ oid_xpath_map = JSON.parse(File.read(oid_xpath_file))
17
+ template_id = attr_val('cda:act/cda:templateId/@root') || attr_val('cda:observation/cda:templateId/@root')
18
+
19
+ # check to see if this is a derived data criteria. These are used for multiple occurrences.
20
+ derived_entry = @entry.at_xpath('./*/cda:sourceOf[@typeCode="DRIV"]')
21
+ if derived_entry
22
+ derived = derived_entry.at_xpath('cda:act/cda:id/@root') || derived_entry.at_xpath('cda:observation/cda:id/@root')
23
+ @derived_from = derived.value
24
+ @@occurrences[@derived_from] ||= Counter.new
25
+ @occurrence_key = @@occurrences[@derived_from].next
26
+ @specific_occurrence = "#{('A'..'ZZ').to_a[@occurrence_key]}"
27
+ end
28
+
29
+ template = template_map[template_id]
30
+ if template
31
+ @negation=template["negation"]
32
+ @definition=template["definition"]
33
+ @status=template["status"]
34
+ @key=@definition+(@status.empty? ? '' : "_#{@status}")
35
+ else
36
+ raise "Unknown data criteria template identifier [#{template_id}]"
37
+ end
38
+
39
+ # Get the code list OID of the criteria, used as an index to the code list database
40
+ @code_list_id = attr_val(oid_xpath_map[@key]['oid_xpath'])
41
+ unless @code_list_id
42
+ puts "\tcode list id not found, getting default" if !@derived_from
43
+ @code_list_id = attr_val('cda:act/cda:sourceOf//cda:code/@code')
44
+ end
45
+
46
+ puts "\tno oid defined for data criteria: #{@key}" if !@code_list_id and !@derived_from
47
+
48
+ end
49
+
50
+ # Get the identifier of the criteria, used elsewhere within the document for referencing
51
+ # @return [String] the identifier of this data criteria
52
+ def id
53
+ attr_val('cda:act/cda:id/@root') || attr_val('cda:observation/cda:id/@root')
54
+ end
55
+
56
+ # Get the title of the criteria, provides a human readable description
57
+ # @return [String] the title of this data criteria
58
+ def title
59
+ title = description
60
+ title = "Occurrence #{@specific_occurrence}: #{title}" if @derived_from
61
+ title
62
+ end
63
+
64
+ def description
65
+ if (@entry.at_xpath('.//cda:title'))
66
+ description = @entry.at_xpath('.//cda:title').inner_text
67
+ else
68
+ description = @entry.at_xpath('.//cda:localVariableName').inner_text
69
+ end
70
+ description
71
+ end
72
+
73
+ # Get a JS friendly constant name for this measure attribute
74
+ def const_name
75
+ components = title.gsub(/\W/,' ').split.collect {|word| word.strip.upcase }
76
+ if @derived_from
77
+ components << @@id.next
78
+ @specific_occurrence_const = (description.gsub(/\W/,' ').split.collect {|word| word.strip.upcase }).join '_'
79
+ end
80
+ components.join '_'
81
+ end
82
+
83
+ def to_json
84
+ json = build_hash(self, [:id,:title,:code_list_id,:derived_from,:description, :definition, :status, :negation, :specific_occurrence,:specific_occurrence_const])
85
+ {
86
+ self.const_name => json
87
+ }
88
+ end
89
+
90
+ # Simple class to issue monotonically increasing integer identifiers
91
+ class Counter
92
+ def initialize
93
+ @count = -1
94
+ end
95
+
96
+ def next
97
+ @count+=1
98
+ end
99
+ end
100
+ @@id = Counter.new
101
+ @@occurrences = {}
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,209 @@
1
+ module HQMF1
2
+ # Class representing an HQMF document
3
+ class Document
4
+
5
+ include HQMF1::Utilities
6
+
7
+ attr_reader :hqmf_id, :hqmf_set_id, :hqmf_version_number
8
+
9
+ # Create a new HQMF1::Document instance by parsing the supplied contents
10
+ # @param [String] hqmf_contents the contents of an HQMF v1.0 document
11
+ def initialize(hqmf_contents)
12
+ @doc = Document.parse(hqmf_contents)
13
+ @data_criteria = @doc.xpath('//cda:section[cda:code/@code="57025-9"]/cda:entry').collect do |entry|
14
+ DataCriteria.new(entry)
15
+ end
16
+
17
+ backfill_derived_code_lists
18
+
19
+ @attributes = @doc.xpath('//cda:subjectOf/cda:measureAttribute').collect do |attr|
20
+ Attribute.new(attr)
21
+ end
22
+ @population_criteria = @doc.xpath('//cda:section[cda:code/@code="57026-7"]/cda:entry').collect do |attr|
23
+ PopulationCriteria.new(attr, self)
24
+ end
25
+
26
+ @stratification = @doc.xpath('//cda:section[cda:code/@code="69669-0"]/cda:entry').collect do |attr|
27
+ PopulationCriteria.new(attr, self)
28
+ end
29
+
30
+ if (@stratification and !@stratification.empty?)
31
+ initial_populations = @population_criteria.select {|pc| pc.code.starts_with? 'IPP'}
32
+ initial_populations.each do |population|
33
+
34
+ @stratification.each do |stratification|
35
+ new_population = HQMF1::PopulationCriteria.new(population.entry, population.doc)
36
+ new_population.hqmf_id = new_population.id
37
+ new_population.stratification_id = stratification.id
38
+ new_population.id = "#{new_population.id}_#{stratification.id}"
39
+ ids = stratification.preconditions.map(&:id)
40
+ new_population.preconditions.delete_if {|precondition| ids.include? precondition.id}
41
+ new_population.preconditions.concat(stratification.preconditions)
42
+ new_population.preconditions.rotate!(-1*stratification.preconditions.size)
43
+ @population_criteria << new_population
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ @hqmf_set_id = @doc.at_xpath('//cda:setId/@root').value.upcase
51
+ @hqmf_id = @doc.at_xpath('//cda:id/@root').value.upcase
52
+ @hqmf_version_number = @doc.at_xpath('//cda:versionNumber/@value').value.to_i
53
+
54
+ end
55
+
56
+ # Get the title of the measure
57
+ # @return [String] the title
58
+ def title
59
+ @doc.at_xpath('cda:QualityMeasureDocument/cda:title').inner_text
60
+ end
61
+
62
+ # Get the description of the measure
63
+ # @return [String] the description
64
+ def description
65
+ @doc.at_xpath('cda:QualityMeasureDocument/cda:text').inner_text
66
+ end
67
+
68
+ # Get all the attributes defined by the measure
69
+ # @return [Array] an array of HQMF1::Attribute
70
+ def all_attributes
71
+ @attributes
72
+ end
73
+
74
+ # Get a specific attribute by id.
75
+ # @param [String] id the attribute identifier
76
+ # @return [HQMF1::Attribute] the matching attribute, raises an Exception if not found
77
+ def attribute(id)
78
+ find(@attributes, :id, id)
79
+ end
80
+
81
+ # Get a specific attribute by code.
82
+ # @param [String] code the attribute code
83
+ # @return [HQMF1::Attribute] the matching attribute, raises an Exception if not found
84
+ def attribute_for_code(code)
85
+ find(@attributes, :code, code)
86
+ end
87
+
88
+ # Get all the population criteria defined by the measure
89
+ # @return [Array] an array of HQMF1::PopulationCriteria
90
+ def all_population_criteria
91
+ @population_criteria
92
+ end
93
+
94
+ def stratification
95
+ @stratification
96
+ end
97
+
98
+ # Get a specific population criteria by id.
99
+ # @param [String] id the population identifier
100
+ # @return [HQMF1::PopulationCriteria] the matching criteria, raises an Exception if not found
101
+ def population_criteria(id)
102
+ find(@population_criteria, :id, id)
103
+ end
104
+
105
+ # Get a specific population criteria by code.
106
+ # @param [String] code the population criteria code
107
+ # @return [HQMF1::PopulationCriteria] the matching criteria, raises an Exception if not found
108
+ def population_criteria_for_code(code)
109
+ find(@population_criteria, :code, code)
110
+ end
111
+
112
+ # Get all the data criteria defined by the measure
113
+ # @return [Array] an array of HQMF1::DataCriteria describing the data elements used by the measure
114
+ def all_data_criteria
115
+ @data_criteria
116
+ end
117
+
118
+ # Get a specific data criteria by id.
119
+ # @param [String] id the data criteria identifier
120
+ # @return [HQMF1::DataCriteria] the matching data criteria, raises an Exception if not found
121
+ def data_criteria(id)
122
+ val = find(@data_criteria, :id, id) || raise("unknown data criteria #{id}")
123
+ end
124
+
125
+ # Parse an XML document from the supplied contents
126
+ # @return [Nokogiri::XML::Document]
127
+ def self.parse(hqmf_contents)
128
+ doc = Nokogiri::XML(hqmf_contents)
129
+ doc.root.add_namespace_definition('cda', 'urn:hl7-org:v3')
130
+ doc
131
+ end
132
+
133
+ # if the data criteria is derived from another criteria, then we want to grab the properties from the derived criteria
134
+ # this is typically the case with Occurrence A, Occurrence B type data criteria
135
+ def backfill_derived_code_lists
136
+ data_criteria_by_id = {}
137
+ @data_criteria.each {|criteria| data_criteria_by_id[criteria.id] = criteria}
138
+ @data_criteria.each do |criteria|
139
+ if (criteria.derived_from)
140
+ derived_from = data_criteria_by_id[criteria.derived_from]
141
+ criteria.definition = derived_from.definition
142
+ criteria.status = derived_from.status
143
+ criteria.code_list_id = derived_from.code_list_id
144
+ end
145
+ end
146
+ end
147
+
148
+ def to_json
149
+ json = build_hash(self, [:title, :description, :hqmf_id, :hqmf_set_id, :hqmf_version_number])
150
+
151
+ json[:data_criteria] = {}
152
+ @data_criteria.each do |criteria|
153
+ criteria_json = criteria.to_json
154
+ # check if the key already exists... if it does redefine the key
155
+ if (json[:data_criteria][criteria_json.keys.first])
156
+ criteria_json = {"#{criteria_json.keys.first}_#{@@ids.next}" => criteria_json.values.first}
157
+ end
158
+ json[:data_criteria].merge! criteria_json
159
+ end
160
+
161
+ json[:metadata] = {}
162
+ json[:attributes] = {}
163
+ @attributes.each do |attribute|
164
+ if (attribute.id)
165
+ json[:attributes].merge! attribute.to_json
166
+ else
167
+ json[:metadata].merge! attribute.to_json
168
+ end
169
+
170
+ end
171
+
172
+ json[:logic] = {}
173
+ counters = {}
174
+ @population_criteria.each do |population|
175
+ population_json = population.to_json
176
+ key = population_json.keys.first
177
+ if json[:logic][key]
178
+ counters[key] ||= 0
179
+ counters[key] += 1
180
+ population_json["#{key}_#{counters[key]}"] = population_json[key]
181
+ population_json.delete(key)
182
+ end
183
+ json[:logic].merge! population_json
184
+ end
185
+
186
+ clean_json_recursive(json)
187
+ json
188
+ end
189
+
190
+ private
191
+
192
+ def find(collection, attribute, value)
193
+ collection.find {|e| e.send(attribute)==value}
194
+ end
195
+
196
+ # Simple class to issue monotonically increasing integer identifiers
197
+ class Counter
198
+ def initialize
199
+ @count = 0
200
+ end
201
+
202
+ def next
203
+ @count+=1
204
+ end
205
+ end
206
+ @@ids = Counter.new
207
+
208
+ end
209
+ end