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.
- checksums.yaml +5 -5
- data/Gemfile +7 -3
- data/README.md +57 -5
- data/Rakefile +1 -0
- data/lib/{hqmf-parser.rb → cqm-parsers.rb} +13 -45
- data/lib/ext/data_element.rb +1 -1
- data/lib/hqmf-parser/2.0/document.rb +1 -1
- data/lib/hqmf-parser/2.0/population_criteria.rb +1 -1
- data/lib/hqmf-parser/cql/document_helpers/doc_population_helper.rb +11 -6
- data/lib/measure-loader/cql_loader.rb +165 -0
- data/lib/measure-loader/elm_dependency_finder.rb +72 -0
- data/lib/measure-loader/elm_parser.rb +67 -0
- data/lib/measure-loader/exceptions.rb +10 -0
- data/lib/measure-loader/helpers.rb +11 -0
- data/lib/measure-loader/hqmf_measure_loader.rb +170 -0
- data/lib/measure-loader/mat_measure_files.rb +138 -0
- data/lib/measure-loader/source_data_criteria_loader.rb +75 -0
- data/lib/measure-loader/value_set_helpers.rb +68 -0
- data/lib/measure-loader/vsac_value_set_loader.rb +97 -0
- data/lib/tasks/hqmf.rake +1 -1
- data/lib/util/util.rb +23 -0
- data/lib/util/vsac_api.rb +166 -103
- metadata +49 -116
- data/lib/ext/code.rb +0 -10
- data/lib/qrda-export/catI-r5/_code.mustache +0 -1
- data/lib/qrda-export/catI-r5/_codes.mustache +0 -10
- data/lib/qrda-export/catI-r5/_header.mustache +0 -28
- data/lib/qrda-export/catI-r5/_measure_section.mustache +0 -59
- data/lib/qrda-export/catI-r5/_reporting_period.mustache +0 -23
- data/lib/qrda-export/catI-r5/_values.mustache +0 -10
- data/lib/qrda-export/catI-r5/qrda1_r5.mustache +0 -137
- data/lib/qrda-export/catI-r5/qrda1_r5.rb +0 -125
- data/lib/qrda-export/catI-r5/qrda_header/_author.mustache +0 -24
- data/lib/qrda-export/catI-r5/qrda_header/_custodian.mustache +0 -43
- data/lib/qrda-export/catI-r5/qrda_header/_documentation_of_service_event.mustache +0 -82
- data/lib/qrda-export/catI-r5/qrda_header/_information_recipient.mustache +0 -7
- data/lib/qrda-export/catI-r5/qrda_header/_legal_authenticator.mustache +0 -25
- data/lib/qrda-export/catI-r5/qrda_header/_participant.mustache +0 -7
- data/lib/qrda-export/catI-r5/qrda_header/_record_target.mustache +0 -28
- data/lib/qrda-export/catI-r5/qrda_templates/adverse_event.mustache +0 -28
- data/lib/qrda-export/catI-r5/qrda_templates/allergy_intolerance.mustache +0 -28
- data/lib/qrda-export/catI-r5/qrda_templates/assessment_performed.mustache +0 -25
- data/lib/qrda-export/catI-r5/qrda_templates/communication_from_patient_to_provider.mustache +0 -29
- data/lib/qrda-export/catI-r5/qrda_templates/communication_from_provider_to_patient.mustache +0 -24
- data/lib/qrda-export/catI-r5/qrda_templates/communication_from_provider_to_provider.mustache +0 -31
- data/lib/qrda-export/catI-r5/qrda_templates/device_applied.mustache +0 -32
- data/lib/qrda-export/catI-r5/qrda_templates/device_ordered.mustache +0 -31
- data/lib/qrda-export/catI-r5/qrda_templates/diagnosis.mustache +0 -38
- data/lib/qrda-export/catI-r5/qrda_templates/diagnostic_study_ordered.mustache +0 -19
- data/lib/qrda-export/catI-r5/qrda_templates/diagnostic_study_performed.mustache +0 -29
- data/lib/qrda-export/catI-r5/qrda_templates/encounter_ordered.mustache +0 -24
- data/lib/qrda-export/catI-r5/qrda_templates/encounter_performed.mustache +0 -40
- data/lib/qrda-export/catI-r5/qrda_templates/immunization_administered.mustache +0 -29
- data/lib/qrda-export/catI-r5/qrda_templates/insurance_provider.mustache +0 -11
- data/lib/qrda-export/catI-r5/qrda_templates/intervention_ordered.mustache +0 -18
- data/lib/qrda-export/catI-r5/qrda_templates/intervention_performed.mustache +0 -25
- data/lib/qrda-export/catI-r5/qrda_templates/lab_test_ordered.mustache +0 -18
- data/lib/qrda-export/catI-r5/qrda_templates/lab_test_performed.mustache +0 -22
- data/lib/qrda-export/catI-r5/qrda_templates/medication_active.mustache +0 -35
- data/lib/qrda-export/catI-r5/qrda_templates/medication_administered.mustache +0 -31
- data/lib/qrda-export/catI-r5/qrda_templates/medication_discharge.mustache +0 -55
- data/lib/qrda-export/catI-r5/qrda_templates/medication_dispensed.mustache +0 -39
- data/lib/qrda-export/catI-r5/qrda_templates/medication_ordered.mustache +0 -38
- data/lib/qrda-export/catI-r5/qrda_templates/patient_characteristic_expired.mustache +0 -16
- data/lib/qrda-export/catI-r5/qrda_templates/physical_exam_performed.mustache +0 -25
- data/lib/qrda-export/catI-r5/qrda_templates/procedure_ordered.mustache +0 -19
- data/lib/qrda-export/catI-r5/qrda_templates/procedure_performed.mustache +0 -44
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_admission_source.mustache +0 -6
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_anatomical_location_site.mustache +0 -1
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_author.mustache +0 -7
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_author_participation.mustache +0 -7
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_component.mustache +0 -11
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_encounter_diagnosis.mustache +0 -19
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_encounter_facility_location.mustache +0 -16
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_mediation_frequency.mustache +0 -3
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_medication_details.mustache +0 -11
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_ordinality.mustache +0 -1
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_principal_diagnosis.mustache +0 -8
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_reason.mustache +0 -12
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_related_to.mustache +0 -6
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_results.mustache +0 -21
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_severity.mustache +0 -8
- data/lib/qrda-export/helper/cat_1_view_helper.rb +0 -142
- data/lib/qrda-export/helper/code_system_helper.rb +0 -77
- data/lib/qrda-export/helper/date_helper.rb +0 -85
- data/lib/qrda-import/base-importers/demographics_importer.rb +0 -43
- data/lib/qrda-import/base-importers/medication_importer.rb +0 -23
- data/lib/qrda-import/base-importers/section_importer.rb +0 -196
- data/lib/qrda-import/cda_identifier.rb +0 -19
- data/lib/qrda-import/data-element-importers/adverse_event_importer.rb +0 -24
- data/lib/qrda-import/data-element-importers/allergy_intolerance_importer.rb +0 -22
- data/lib/qrda-import/data-element-importers/assessment_performed_importer.rb +0 -24
- data/lib/qrda-import/data-element-importers/communication_from_patient_to_provider_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/communication_from_provider_to_patient_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/communication_from_provider_to_provider_importer.rb +0 -21
- data/lib/qrda-import/data-element-importers/device_applied_importer.rb +0 -24
- data/lib/qrda-import/data-element-importers/device_order_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/diagnosis_importer.rb +0 -24
- data/lib/qrda-import/data-element-importers/diagnostic_study_order_importer.rb +0 -21
- data/lib/qrda-import/data-element-importers/diagnostic_study_performed_importer.rb +0 -31
- data/lib/qrda-import/data-element-importers/encounter_order_importer.rb +0 -21
- data/lib/qrda-import/data-element-importers/encounter_performed_importer.rb +0 -42
- data/lib/qrda-import/data-element-importers/immunization_administered_importer.rb +0 -18
- data/lib/qrda-import/data-element-importers/intervention_order_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/intervention_performed_importer.rb +0 -23
- data/lib/qrda-import/data-element-importers/laboratory_test_order_importer.rb +0 -21
- data/lib/qrda-import/data-element-importers/laboratory_test_performed_importer.rb +0 -29
- data/lib/qrda-import/data-element-importers/medication_active_importer.rb +0 -17
- data/lib/qrda-import/data-element-importers/medication_administered_importer.rb +0 -17
- data/lib/qrda-import/data-element-importers/medication_discharge_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/medication_dispensed_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/medication_order_importer.rb +0 -16
- data/lib/qrda-import/data-element-importers/patient_characteristic_expired.rb +0 -22
- data/lib/qrda-import/data-element-importers/physical_exam_performed_importer.rb +0 -27
- data/lib/qrda-import/data-element-importers/procedure_order_importer.rb +0 -27
- data/lib/qrda-import/data-element-importers/procedure_performed_importer.rb +0 -35
- data/lib/qrda-import/data-element-importers/substance_administered_importer.rb +0 -16
- data/lib/qrda-import/entry_finder.rb +0 -20
- data/lib/qrda-import/entry_package.rb +0 -16
- data/lib/qrda-import/narrative_reference_handler.rb +0 -33
- 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,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
|