cqm-parsers 0.2.2 → 3.0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +7 -3
  3. data/README.md +57 -5
  4. data/Rakefile +1 -0
  5. data/lib/{hqmf-parser.rb → cqm-parsers.rb} +13 -45
  6. data/lib/ext/data_element.rb +1 -1
  7. data/lib/hqmf-parser/2.0/document.rb +1 -1
  8. data/lib/hqmf-parser/2.0/population_criteria.rb +1 -1
  9. data/lib/hqmf-parser/cql/document_helpers/doc_population_helper.rb +11 -6
  10. data/lib/measure-loader/cql_loader.rb +165 -0
  11. data/lib/measure-loader/elm_dependency_finder.rb +72 -0
  12. data/lib/measure-loader/elm_parser.rb +67 -0
  13. data/lib/measure-loader/exceptions.rb +10 -0
  14. data/lib/measure-loader/helpers.rb +11 -0
  15. data/lib/measure-loader/hqmf_measure_loader.rb +170 -0
  16. data/lib/measure-loader/mat_measure_files.rb +138 -0
  17. data/lib/measure-loader/source_data_criteria_loader.rb +75 -0
  18. data/lib/measure-loader/value_set_helpers.rb +68 -0
  19. data/lib/measure-loader/vsac_value_set_loader.rb +97 -0
  20. data/lib/tasks/hqmf.rake +1 -1
  21. data/lib/util/util.rb +23 -0
  22. data/lib/util/vsac_api.rb +166 -103
  23. metadata +49 -116
  24. data/lib/ext/code.rb +0 -10
  25. data/lib/qrda-export/catI-r5/_code.mustache +0 -1
  26. data/lib/qrda-export/catI-r5/_codes.mustache +0 -10
  27. data/lib/qrda-export/catI-r5/_header.mustache +0 -28
  28. data/lib/qrda-export/catI-r5/_measure_section.mustache +0 -59
  29. data/lib/qrda-export/catI-r5/_reporting_period.mustache +0 -23
  30. data/lib/qrda-export/catI-r5/_values.mustache +0 -10
  31. data/lib/qrda-export/catI-r5/qrda1_r5.mustache +0 -137
  32. data/lib/qrda-export/catI-r5/qrda1_r5.rb +0 -125
  33. data/lib/qrda-export/catI-r5/qrda_header/_author.mustache +0 -24
  34. data/lib/qrda-export/catI-r5/qrda_header/_custodian.mustache +0 -43
  35. data/lib/qrda-export/catI-r5/qrda_header/_documentation_of_service_event.mustache +0 -82
  36. data/lib/qrda-export/catI-r5/qrda_header/_information_recipient.mustache +0 -7
  37. data/lib/qrda-export/catI-r5/qrda_header/_legal_authenticator.mustache +0 -25
  38. data/lib/qrda-export/catI-r5/qrda_header/_participant.mustache +0 -7
  39. data/lib/qrda-export/catI-r5/qrda_header/_record_target.mustache +0 -28
  40. data/lib/qrda-export/catI-r5/qrda_templates/adverse_event.mustache +0 -28
  41. data/lib/qrda-export/catI-r5/qrda_templates/allergy_intolerance.mustache +0 -28
  42. data/lib/qrda-export/catI-r5/qrda_templates/assessment_performed.mustache +0 -25
  43. data/lib/qrda-export/catI-r5/qrda_templates/communication_from_patient_to_provider.mustache +0 -29
  44. data/lib/qrda-export/catI-r5/qrda_templates/communication_from_provider_to_patient.mustache +0 -24
  45. data/lib/qrda-export/catI-r5/qrda_templates/communication_from_provider_to_provider.mustache +0 -31
  46. data/lib/qrda-export/catI-r5/qrda_templates/device_applied.mustache +0 -32
  47. data/lib/qrda-export/catI-r5/qrda_templates/device_ordered.mustache +0 -31
  48. data/lib/qrda-export/catI-r5/qrda_templates/diagnosis.mustache +0 -38
  49. data/lib/qrda-export/catI-r5/qrda_templates/diagnostic_study_ordered.mustache +0 -19
  50. data/lib/qrda-export/catI-r5/qrda_templates/diagnostic_study_performed.mustache +0 -29
  51. data/lib/qrda-export/catI-r5/qrda_templates/encounter_ordered.mustache +0 -24
  52. data/lib/qrda-export/catI-r5/qrda_templates/encounter_performed.mustache +0 -40
  53. data/lib/qrda-export/catI-r5/qrda_templates/immunization_administered.mustache +0 -29
  54. data/lib/qrda-export/catI-r5/qrda_templates/insurance_provider.mustache +0 -11
  55. data/lib/qrda-export/catI-r5/qrda_templates/intervention_ordered.mustache +0 -18
  56. data/lib/qrda-export/catI-r5/qrda_templates/intervention_performed.mustache +0 -25
  57. data/lib/qrda-export/catI-r5/qrda_templates/lab_test_ordered.mustache +0 -18
  58. data/lib/qrda-export/catI-r5/qrda_templates/lab_test_performed.mustache +0 -22
  59. data/lib/qrda-export/catI-r5/qrda_templates/medication_active.mustache +0 -35
  60. data/lib/qrda-export/catI-r5/qrda_templates/medication_administered.mustache +0 -31
  61. data/lib/qrda-export/catI-r5/qrda_templates/medication_discharge.mustache +0 -55
  62. data/lib/qrda-export/catI-r5/qrda_templates/medication_dispensed.mustache +0 -39
  63. data/lib/qrda-export/catI-r5/qrda_templates/medication_ordered.mustache +0 -38
  64. data/lib/qrda-export/catI-r5/qrda_templates/patient_characteristic_expired.mustache +0 -16
  65. data/lib/qrda-export/catI-r5/qrda_templates/physical_exam_performed.mustache +0 -25
  66. data/lib/qrda-export/catI-r5/qrda_templates/procedure_ordered.mustache +0 -19
  67. data/lib/qrda-export/catI-r5/qrda_templates/procedure_performed.mustache +0 -44
  68. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_admission_source.mustache +0 -6
  69. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_anatomical_location_site.mustache +0 -1
  70. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_author.mustache +0 -7
  71. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_author_participation.mustache +0 -7
  72. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_component.mustache +0 -11
  73. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_encounter_diagnosis.mustache +0 -19
  74. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_encounter_facility_location.mustache +0 -16
  75. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_mediation_frequency.mustache +0 -3
  76. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_medication_details.mustache +0 -11
  77. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_ordinality.mustache +0 -1
  78. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_principal_diagnosis.mustache +0 -8
  79. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_reason.mustache +0 -12
  80. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_related_to.mustache +0 -6
  81. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_results.mustache +0 -21
  82. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_severity.mustache +0 -8
  83. data/lib/qrda-export/helper/cat_1_view_helper.rb +0 -142
  84. data/lib/qrda-export/helper/code_system_helper.rb +0 -77
  85. data/lib/qrda-export/helper/date_helper.rb +0 -85
  86. data/lib/qrda-import/base-importers/demographics_importer.rb +0 -43
  87. data/lib/qrda-import/base-importers/medication_importer.rb +0 -23
  88. data/lib/qrda-import/base-importers/section_importer.rb +0 -196
  89. data/lib/qrda-import/cda_identifier.rb +0 -19
  90. data/lib/qrda-import/data-element-importers/adverse_event_importer.rb +0 -24
  91. data/lib/qrda-import/data-element-importers/allergy_intolerance_importer.rb +0 -22
  92. data/lib/qrda-import/data-element-importers/assessment_performed_importer.rb +0 -24
  93. data/lib/qrda-import/data-element-importers/communication_from_patient_to_provider_importer.rb +0 -19
  94. data/lib/qrda-import/data-element-importers/communication_from_provider_to_patient_importer.rb +0 -19
  95. data/lib/qrda-import/data-element-importers/communication_from_provider_to_provider_importer.rb +0 -21
  96. data/lib/qrda-import/data-element-importers/device_applied_importer.rb +0 -24
  97. data/lib/qrda-import/data-element-importers/device_order_importer.rb +0 -19
  98. data/lib/qrda-import/data-element-importers/diagnosis_importer.rb +0 -24
  99. data/lib/qrda-import/data-element-importers/diagnostic_study_order_importer.rb +0 -21
  100. data/lib/qrda-import/data-element-importers/diagnostic_study_performed_importer.rb +0 -31
  101. data/lib/qrda-import/data-element-importers/encounter_order_importer.rb +0 -21
  102. data/lib/qrda-import/data-element-importers/encounter_performed_importer.rb +0 -42
  103. data/lib/qrda-import/data-element-importers/immunization_administered_importer.rb +0 -18
  104. data/lib/qrda-import/data-element-importers/intervention_order_importer.rb +0 -19
  105. data/lib/qrda-import/data-element-importers/intervention_performed_importer.rb +0 -23
  106. data/lib/qrda-import/data-element-importers/laboratory_test_order_importer.rb +0 -21
  107. data/lib/qrda-import/data-element-importers/laboratory_test_performed_importer.rb +0 -29
  108. data/lib/qrda-import/data-element-importers/medication_active_importer.rb +0 -17
  109. data/lib/qrda-import/data-element-importers/medication_administered_importer.rb +0 -17
  110. data/lib/qrda-import/data-element-importers/medication_discharge_importer.rb +0 -19
  111. data/lib/qrda-import/data-element-importers/medication_dispensed_importer.rb +0 -19
  112. data/lib/qrda-import/data-element-importers/medication_order_importer.rb +0 -16
  113. data/lib/qrda-import/data-element-importers/patient_characteristic_expired.rb +0 -22
  114. data/lib/qrda-import/data-element-importers/physical_exam_performed_importer.rb +0 -27
  115. data/lib/qrda-import/data-element-importers/procedure_order_importer.rb +0 -27
  116. data/lib/qrda-import/data-element-importers/procedure_performed_importer.rb +0 -35
  117. data/lib/qrda-import/data-element-importers/substance_administered_importer.rb +0 -16
  118. data/lib/qrda-import/entry_finder.rb +0 -20
  119. data/lib/qrda-import/entry_package.rb +0 -16
  120. data/lib/qrda-import/narrative_reference_handler.rb +0 -33
  121. data/lib/qrda-import/patient_importer.rb +0 -111
@@ -0,0 +1,67 @@
1
+ module Measures
2
+ class ElmParser
3
+ # Fields are combined with the refId to find elm node that corrosponds to the current annotation node.
4
+ @fields = ['expression', 'operand', 'suchThat']
5
+
6
+ def self.parse(doc)
7
+ localid_to_type_map = generate_localid_to_type_map(doc)
8
+ ret = {
9
+ statements: [],
10
+ identifier: {}
11
+ }
12
+ # extract library identifier data
13
+ ret[:identifier][:id] = doc.css("identifier").attr("id").value
14
+ ret[:identifier][:version] = doc.css("identifier").attr("version").value
15
+
16
+ # extracts the fields of type "annotation" and their children.
17
+ annotations = doc.css("annotation")
18
+ annotations.each do |node|
19
+ node, define_name = parse_node(node, localid_to_type_map)
20
+ unless define_name.nil?
21
+ node[:define_name] = define_name
22
+ ret[:statements] << node
23
+ end
24
+ end
25
+ ret
26
+ end
27
+
28
+ # Recursive function that traverses the annotation tree and constructs a representation
29
+ # that will be compatible with the front end.
30
+ def self.parse_node(node, localid_to_type_map)
31
+ ret = {
32
+ children: []
33
+ }
34
+ define_name = nil
35
+ node.children.each do |child|
36
+ if child.is_a?(Nokogiri::XML::Text) # leaf node
37
+ clause_text = child.content.gsub(/\t/, " ")
38
+ clause = {
39
+ text: clause_text
40
+ }
41
+ clause[:ref_id] = child['r'] unless child['r'].nil?
42
+ ret[:children] << clause
43
+ define_name = clause_text.split("\"")[1] if clause_text.strip.starts_with?("define")
44
+ else
45
+ node_type = localid_to_type_map[child['r']] unless child['r'].nil?
46
+ # Parses the current child recursively. child_define_name will bubble up to indicate which
47
+ # statement is currently being traversed.
48
+ node, child_define_name = parse_node(child, localid_to_type_map)
49
+ node[:node_type] = node_type unless node_type.nil?
50
+ node[:ref_id] = child['r'] unless child['r'].nil?
51
+ ret[:children] << node
52
+ define_name = child_define_name unless child_define_name.nil?
53
+ end
54
+ end
55
+ return ret, define_name
56
+ end
57
+
58
+ def self.generate_localid_to_type_map(doc)
59
+ localid_to_type_map = {}
60
+ @fields.each do |field|
61
+ nodes = doc.css(field + '[localId][xsi|type]')
62
+ nodes.each {|node| localid_to_type_map[node['localId']] = node['xsi:type']}
63
+ end
64
+ return localid_to_type_map
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,10 @@
1
+ module Measures
2
+ class ValueSetException < StandardError
3
+ end
4
+ class HQMFException < StandardError
5
+ end
6
+ class MeasureLoadingInvalidPackageException < StandardError
7
+ end
8
+ class MeasureLoadingException < StandardError
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module Measures
2
+ module Helpers
3
+ def self.elm_id(elm)
4
+ return elm['library']['identifier']['id']
5
+ end
6
+
7
+ def self.elm_version(elm)
8
+ return elm['library']['identifier']['version']
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,170 @@
1
+ require 'cqm/models'
2
+
3
+ module Measures
4
+ module HQMFMeasureLoader
5
+ class << self
6
+
7
+ def extract_fields(hqmf_xml)
8
+ qmd = hqmf_xml.at_css('/QualityMeasureDocument')
9
+ hqmf_id = qmd.at_css('/id')['root'].upcase
10
+ hqmf_set_id = qmd.at_css('/setId')['root'].upcase
11
+ title = qmd.at_css('/title')['value']
12
+ description = qmd.at_css('/text')['value']
13
+ cms_identifier = extract_cms_identifier(qmd)
14
+ hqmf_version_number = qmd.at_css('/versionNumber')['value']
15
+ cms_id = "CMS#{cms_identifier}v#{hqmf_version_number.to_i}"
16
+ main_cql_library = qmd.at_css('/component/populationCriteriaSection/component/initialPopulationCriteria/*/*/id')['extension'].split('.').first
17
+ measure_scoring = extract_measure_scoring(qmd)
18
+ population_sets = extract_population_set_models(qmd, measure_scoring)
19
+
20
+ return {
21
+ hqmf_id: hqmf_id,
22
+ hqmf_set_id: hqmf_set_id,
23
+ title: title,
24
+ description: description,
25
+ main_cql_library: main_cql_library,
26
+ hqmf_version_number: hqmf_version_number,
27
+ cms_id: cms_id,
28
+ measure_scoring: measure_scoring,
29
+ population_sets: population_sets
30
+ }
31
+ end
32
+
33
+ def add_fields_from_hqmf_model_hash(measure, hqmf_model_hash)
34
+ measure.measure_attributes = hqmf_model_hash[:attributes]
35
+ measure.measure_period = hqmf_model_hash[:measure_period]
36
+ measure.population_criteria = hqmf_model_hash[:population_criteria]
37
+
38
+ # add observation info, note population_sets needs to have been added to the measure by now
39
+ (hqmf_model_hash[:observations] || []).each do |observation|
40
+ observation = CQM::Observation.new(
41
+ hqmf_id: observation[:function_hqmf_oid],
42
+ aggregation_type: observation[:function_aggregation_type],
43
+ observation_function: CQM::StatementReference.new(
44
+ library_name: hqmf_model_hash[:cql_measure_library],
45
+ statement_name: observation[:function_name],
46
+ hqmf_id: observation[:function_hqmf_oid]
47
+ ),
48
+ observation_parameter: CQM::StatementReference.new(
49
+ library_name: hqmf_model_hash[:cql_measure_library],
50
+ statement_name: observation[:parameter],
51
+ hqmf_id: observation[:function_hqmf_oid]
52
+ )
53
+ )
54
+ # add observation to each population set
55
+ measure.population_sets.each { |population_set| population_set.observations << observation }
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def extract_cms_identifier(qmd)
62
+ cms_identifier =
63
+ (qmd.at_xpath('./xmlns:subjectOf/xmlns:measureAttribute[xmlns:code/xmlns:originalText[@value="eCQM Identifier (Measure Authoring Tool)"]]/xmlns:value') ||
64
+ qmd.at_xpath('./xmlns:subjectOf/xmlns:measureAttribute[xmlns:code/xmlns:originalText[@value="eMeasure Identifier (Measure Authoring Tool)"]]/xmlns:value'))
65
+ return cms_identifier['value']
66
+ end
67
+
68
+ def extract_measure_scoring(qmd)
69
+ map_from_hqmf_name_to_full_name = {
70
+ 'PROPOR' => 'PROPORTION',
71
+ 'RATIO' => 'RATIO',
72
+ 'CONTVAR' => 'CONTINUOUS_VARIABLE',
73
+ 'COHORT' => 'COHORT'
74
+ }
75
+ scoring = qmd.at_xpath("./xmlns:subjectOf/xmlns:measureAttribute[xmlns:code/@code='MSRSCORE']/xmlns:value").attr('code')
76
+ scoring_full_name = map_from_hqmf_name_to_full_name[scoring]
77
+ raise StandardError.new("Unknown measure scoring type encountered #{scoring}") if scoring_full_name.nil?
78
+ return scoring_full_name
79
+ end
80
+
81
+ def extract_population_set_models(qmd, measure_scoring)
82
+ populations = qmd.css('/component/populationCriteriaSection')
83
+ return populations.map.with_index do |population, pop_index|
84
+ ps_hash = extract_population_set(population)
85
+ population_set = CQM::PopulationSet.new(
86
+ title: ps_hash[:title],
87
+ population_set_id: "PopulationSet_#{pop_index+1}"
88
+ )
89
+
90
+ population_set.populations = construct_population_map(measure_scoring)
91
+ ps_hash[:populations].each do |pop_code,statement_ref_hash|
92
+ population_set.populations[pop_code] = CQM::StatementReference.new(statement_ref_hash)
93
+ end
94
+
95
+ ps_hash[:supplemental_data_elements].each do |statement_ref_hash|
96
+ population_set.supplemental_data_elements << CQM::StatementReference.new(statement_ref_hash)
97
+ end
98
+
99
+ ps_hash[:stratifications].each_with_index do |statement_ref_hash, index|
100
+ population_set.stratifications << CQM::Stratification.new(
101
+ hqmf_id: statement_ref_hash[:hqmf_id],
102
+ stratification_id: "#{population_set.population_set_id}_Stratification_#{index+1}",
103
+ title: "PopSet#{pop_index+1} Stratification #{index+1}",
104
+ statement: CQM::StatementReference.new(statement_ref_hash)
105
+ )
106
+ end
107
+ population_set
108
+ end
109
+ end
110
+
111
+ def extract_population_set(population_hqmf_node)
112
+ ps = { populations: {}, stratifications: [], supplemental_data_elements: [] }
113
+ ps[:id] = population_hqmf_node.at_css('id').attr('extension')
114
+ ps[:title] = population_hqmf_node.at_css('title').attr('value')
115
+ criteria_components = population_hqmf_node.css('component').flat_map(&:children)
116
+ criteria_components.each do |cc|
117
+ statement_ref = cc.at_css('precondition/criteriaReference/id')
118
+ next if statement_ref.nil?
119
+ statement_ref_hash = { library_name: statement_ref.attr('extension').split('.')[0],
120
+ statement_name: Utilities.remove_enclosing_quotes(statement_ref.attr('extension').split('.')[1]),
121
+ hqmf_id: cc.at_css('id').attr('root') }
122
+ case cc.name
123
+ when 'initialPopulationCriteria'
124
+ ps[:populations][HQMF::PopulationCriteria::IPP] = statement_ref_hash
125
+ when 'denominatorCriteria'
126
+ ps[:populations][HQMF::PopulationCriteria::DENOM] = statement_ref_hash
127
+ when 'numeratorCriteria'
128
+ ps[:populations][HQMF::PopulationCriteria::NUMER] = statement_ref_hash
129
+ when 'numeratorExclusionCriteria'
130
+ ps[:populations][HQMF::PopulationCriteria::NUMEX] = statement_ref_hash
131
+ when 'denominatorExclusionCriteria'
132
+ ps[:populations][HQMF::PopulationCriteria::DENEX] = statement_ref_hash
133
+ when 'denominatorExceptionCriteria'
134
+ ps[:populations][HQMF::PopulationCriteria::DENEXCEP] = statement_ref_hash
135
+ when 'measurePopulationCriteria'
136
+ ps[:populations][HQMF::PopulationCriteria::MSRPOPL] = statement_ref_hash
137
+ when 'measurePopulationExclusionCriteria'
138
+ ps[:populations][HQMF::PopulationCriteria::MSRPOPLEX] = statement_ref_hash
139
+ when 'stratifierCriteria'
140
+ # Ignore Supplemental Data Elements
141
+ next if holds_supplemental_data_elements(cc)
142
+ ps[:stratifications] << statement_ref_hash
143
+ when 'supplementalDataElement'
144
+ ps[:supplemental_data_elements] << statement_ref_hash
145
+ end
146
+ end
147
+ return ps
148
+ end
149
+
150
+ def holds_supplemental_data_elements(criteria_component_node)
151
+ return criteria_component_node.at_css('component[@typeCode="COMP"]/measureAttribute/code[@code="SDE"]').present?
152
+ end
153
+
154
+ def construct_population_map(measure_scoring)
155
+ case measure_scoring
156
+ when 'PROPORTION'
157
+ CQM::ProportionPopulationMap.new
158
+ when 'RATIO'
159
+ CQM::RatioPopulationMap.new
160
+ when 'CONTINUOUS_VARIABLE'
161
+ CQM::ContinuousVariablePopulationMap.new
162
+ when 'COHORT'
163
+ CQM::CohortPopulationMap.new
164
+ else
165
+ raise StandardError.new("Unknown measure scoring type encountered #{measure_scoring}")
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,138 @@
1
+ require 'zip'
2
+
3
+ module Measures
4
+ class MATMeasureFiles
5
+ attr_accessor :hqmf_xml, :cql_libraries, :human_readable, :components
6
+
7
+ class CqlLibraryFiles
8
+ attr_accessor :id, :version, :cql, :elm, :elm_xml
9
+ def initialize(id, version, cql, elm, elm_xml)
10
+ @id = id
11
+ @version = version
12
+ @cql = cql
13
+ @elm = elm
14
+ @elm_xml = elm_xml
15
+ end
16
+ end
17
+
18
+ def initialize(hqmf_xml, cql_libraries, human_readable)
19
+ @hqmf_xml = hqmf_xml
20
+ @cql_libraries = cql_libraries
21
+ @human_readable = human_readable
22
+
23
+ raise MeasureLoadingInvalidPackageException.new("Measure package missing required element: HQMF XML File") if @hqmf_xml.nil?
24
+ raise MeasureLoadingInvalidPackageException.new("Measure package missing required element: Human Readable Document") if @human_readable.nil?
25
+ raise MeasureLoadingInvalidPackageException.new("Measure package missing required element: CQL Libraries") if @cql_libraries.nil? || @cql_libraries.empty?
26
+ end
27
+
28
+ def self.create_from_zip_file(zip_file)
29
+ folders = unzip_measure_zip_into_hash(zip_file).values
30
+ raise MeasureLoadingInvalidPackageException.new("No measure found") if folders.empty?
31
+ folders.sort_by! { |h| h[:depth] }
32
+ raise MeasureLoadingInvalidPackageException.new("Multiple measure folders at top level") if folders[0][:depth] == folders.dig(1,:depth)
33
+
34
+ measure_folder, *component_measure_folders = folders
35
+ measure_assets = make_measure_artifacts(parse_measure_files(measure_folder))
36
+ measure_assets.components = component_measure_folders.collect {|f| make_measure_artifacts(parse_measure_files(f))}
37
+
38
+ return measure_assets
39
+ rescue StandardError => e
40
+ raise MeasureLoadingInvalidPackageException.new("Error processing package file: #{e.message}")
41
+ end
42
+
43
+ def self.valid_zip?(zip_file)
44
+ create_from_zip_file(zip_file)
45
+ return true
46
+ rescue MeasureLoadingInvalidPackageException
47
+ return false
48
+ end
49
+
50
+ class << self
51
+
52
+ private
53
+
54
+ def unzip_measure_zip_into_hash(zip_file)
55
+ folders = Hash.new { |h, k| h[k] = {files: []} }
56
+ Zip::File.open zip_file.path do |file|
57
+ file.each do |f|
58
+ pn = Pathname(f.name)
59
+ next if '__MACOSX'.in? pn.each_filename # ignore anything in a __MACOSX folder
60
+ next unless pn.basename.extname.in? ['.xml','.cql','.json','.html']
61
+ folders[pn.dirname][:files] << { basename: pn.basename, contents: f.get_input_stream.read }
62
+ folders[pn.dirname][:depth] = pn.each_filename.count # this is just a count of how many folders are in the path
63
+ end
64
+ end
65
+ return folders
66
+ end
67
+
68
+ def make_measure_artifacts(measure_files)
69
+ library_count = measure_files[:cqls].length
70
+ unless (measure_files[:elm_xmls].length == library_count && measure_files[:elms].length == library_count)
71
+ raise MeasureLoadingInvalidPackageException.new("Each library must have a CQL file, an ELM JSON file, and an ELM XML file.")
72
+ end
73
+
74
+ cql_libraries = []
75
+ library_count.times do |i|
76
+ elm_annotation = measure_files[:elm_xmls][i]
77
+ elm = measure_files[:elms][i]
78
+ cql = measure_files[:cqls][i]
79
+
80
+ id, version = verify_library_versions_match_and_get_version(cql, elm, elm_annotation)
81
+ cql_libraries << CqlLibraryFiles.new(id, version, cql, elm, elm_annotation)
82
+ end
83
+
84
+ return new(measure_files[:hqmf_xml], cql_libraries, measure_files[:human_readable])
85
+ end
86
+
87
+ def parse_measure_files(folder)
88
+ measure_files = { cqls: [], elms: [], elm_xmls: [] }
89
+ folder[:files].sort_by! { |h| h[:basename] }
90
+ folder[:files].each do |file|
91
+ case file[:basename].extname.to_s
92
+ when '.cql'
93
+ measure_files[:cqls] << file[:contents]
94
+ when '.json'
95
+ begin
96
+ measure_files[:elms] << JSON.parse(file[:contents], max_nesting: 1000)
97
+ rescue StandardError
98
+ raise MeasureLoadingInvalidPackageException.new("Unable to parse json file #{basename}")
99
+ end
100
+ when '.html'
101
+ raise MeasureLoadingInvalidPackageException.new("Multiple HumanReadable docs found in same folder") unless measure_files[:human_readable].nil?
102
+ measure_files[:human_readable] = file[:contents]
103
+ when '.xml'
104
+ begin
105
+ doc = Nokogiri::XML(file[:contents]) { |config| config.noblanks }
106
+ rescue StandardError
107
+ raise MeasureLoadingInvalidPackageException.new("Unable to parse xml file #{basename}")
108
+ end
109
+ if doc.root.name == 'QualityMeasureDocument'
110
+ raise MeasureLoadingInvalidPackageException.new("Multiple QualityMeasureDocuments found in same folder") unless measure_files[:hqmf_xml].nil?
111
+ measure_files[:hqmf_xml] = doc
112
+ elsif doc.root.name == 'library'
113
+ measure_files[:elm_xmls] << doc
114
+ end
115
+ end
116
+ end
117
+ raise MeasureLoadingInvalidPackageException.new("Measure folder found with no hqmf") if measure_files[:hqmf_xml].nil?
118
+ return measure_files
119
+ end
120
+
121
+ def verify_library_versions_match_and_get_version(cql, elm, elm_annotation)
122
+ identifier = elm_annotation.at_xpath('/xmlns:library/xmlns:identifier')
123
+ id = identifier['id']
124
+ version = identifier['version']
125
+
126
+ if (Helpers.elm_id(elm)!= id ||
127
+ Helpers.elm_version(elm) != version ||
128
+ !(cql.include?("library #{id} version '#{version}'") || cql.include?("<library>#{id}</library><version>#{version}</version>"))
129
+ )
130
+ raise MeasureLoadingInvalidPackageException.new("Cql library assets must all have same version.")
131
+ end
132
+
133
+ return id, version
134
+ end
135
+
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,75 @@
1
+ module Measures
2
+ class SourceDataCriteriaLoader
3
+
4
+ def initialize(hqmf_xml, value_sets_from_single_code_references)
5
+ @hqmf_xml = hqmf_xml
6
+ @single_code_concepts = map_single_code_concepts(value_sets_from_single_code_references)
7
+ end
8
+
9
+ def extract_data_criteria
10
+ data_criteria_entries = @hqmf_xml.css('QualityMeasureDocument/component/dataCriteriaSection/entry')
11
+ source_data_criteria = data_criteria_entries.map { |entry| modelize_data_criteria_entry(entry) }
12
+
13
+ # We create the sdc in such a way that "negative" ones look positive in our array by now,
14
+ # so using uniq should give us an array of all positive criteria with no duplicates
15
+ source_data_criteria.uniq! { |sdc| "#{sdc.codeListId}_#{sdc.hqmfOid}" }
16
+ return source_data_criteria
17
+ end
18
+
19
+ private
20
+
21
+ def modelize_data_criteria_entry(entry)
22
+ criteria = entry.at_xpath('./*[xmlns:templateId/xmlns:item]')
23
+ model_fields = if entry.at_css('localVariableName').present?
24
+ extract_fields_from_standard_data_criteria(criteria, entry)
25
+ else
26
+ extract_fields_from_single_code_reference_data_criteria(criteria)
27
+ end
28
+ hqmf_template_oid = criteria.at_css('templateId/item')['root']
29
+ model = QDM::ModelFinder.by_hqmf_oid(hqmf_template_oid)
30
+ raise "No datatype found for oid #{hqmf_template_oid}. Verify the QDM version of the measure package is correct." if model.nil?
31
+ model = model.new(model_fields)
32
+ model.description = model.qdmTitle + ': ' + model.description
33
+ return model
34
+ end
35
+
36
+ def extract_fields_from_standard_data_criteria(criteria, entry)
37
+ entry_variable_name_prefix = entry.at_css('localVariableName')['value'].split('_').first
38
+ node_with_valueset = (criteria.at_css('value[valueSet]') || criteria.at_css('code[valueSet]'))
39
+ code_list_id = node_with_valueset.present? ? node_with_valueset['valueSet'] : nil
40
+ return {
41
+ description: entry_variable_name_prefix,
42
+ codeListId: code_list_id
43
+ }
44
+ end
45
+
46
+ def extract_fields_from_single_code_reference_data_criteria(criteria)
47
+ single_code_reference = criteria.at_css('value[codeSystem][code]') || criteria.at_css('code[codeSystem][code]')
48
+ system_id = "#{single_code_reference['codeSystem']}_#{single_code_reference['codeSystemVersion']}".to_sym
49
+ concept = @single_code_concepts[system_id][single_code_reference['code'].to_sym] || get_concept_from_participation(criteria.at_css('participation'))
50
+ value_set = concept._parent
51
+ return {
52
+ description: concept.display_name,
53
+ codeListId: value_set.oid
54
+ }
55
+ end
56
+
57
+ # If QDM datatype template in MAT has includeSubTemplate, code gets nested into participation
58
+ # this method gets the codes from participation and form the code concept
59
+ def get_concept_from_participation(participation)
60
+ code_element = participation.at_css('code')
61
+ system_id = "#{code_element['codeSystem']}_#{code_element['codeSystemVersion']}".to_sym
62
+ @single_code_concepts[system_id][code_element['code'].to_sym]
63
+ end
64
+
65
+ def map_single_code_concepts(value_sets_from_single_code_references)
66
+ single_code_concepts = {}
67
+ value_sets_from_single_code_references.flat_map(&:concepts).each do |concept|
68
+ system_id = "#{concept.code_system_oid}_#{concept.code_system_version.to_s.sub('urn:hl7:version:','')}".to_sym
69
+ single_code_concepts[system_id] ||= {}
70
+ single_code_concepts[system_id][concept.code.to_sym] = concept
71
+ end
72
+ return single_code_concepts
73
+ end
74
+ end
75
+ end