health-data-standards 3.5.3 → 3.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -3
- data/README.md +10 -2
- data/lib/health-data-standards.rb +5 -28
- data/lib/health-data-standards/export/cat_1.rb +3 -2
- data/lib/health-data-standards/export/cat_1_r2.rb +11 -0
- data/lib/health-data-standards/ext/node.rb +1 -2
- data/lib/health-data-standards/import/bulk_record_importer.rb +4 -4
- data/lib/health-data-standards/import/bundle/importer.rb +62 -0
- data/lib/health-data-standards/import/c32/insurance_provider_importer.rb +10 -8
- data/lib/health-data-standards/import/cat1/patient_importer.rb +3 -3
- data/lib/health-data-standards/import/cat1/procedure_importer.rb +42 -0
- data/lib/health-data-standards/import/cda/medication_importer.rb +11 -11
- data/lib/health-data-standards/import/cda/section_importer.rb +13 -7
- data/lib/health-data-standards/models/cqm/aggregate_objects.rb +5 -1
- data/lib/health-data-standards/models/cqm/bundle.rb +5 -3
- data/lib/health-data-standards/models/cqm/measure.rb +1 -1
- data/lib/health-data-standards/models/entry.rb +5 -1
- data/lib/health-data-standards/models/record.rb +10 -0
- data/lib/health-data-standards/models/reference.rb +23 -0
- data/lib/health-data-standards/models/svs/value_set.rb +4 -3
- data/lib/health-data-standards/railtie.rb +1 -1
- data/lib/health-data-standards/tasks.rb +1 -0
- data/lib/health-data-standards/tasks/bundle.rake +84 -2
- data/lib/health-data-standards/util/vs_api.rb +40 -7
- data/lib/health-data-standards/validate/base_validator.rb +23 -0
- data/lib/health-data-standards/validate/data_validator.rb +85 -0
- data/lib/health-data-standards/validate/measure_validator.rb +127 -0
- data/lib/health-data-standards/validate/performance_rate_validator.rb +94 -0
- data/lib/health-data-standards/validate/reported_result_extractor.rb +170 -0
- data/lib/health-data-standards/validate/schema_validator.rb +24 -0
- data/lib/health-data-standards/validate/schematron/c_processor.rb +28 -0
- data/lib/health-data-standards/validate/schematron/java_processor.rb +93 -0
- data/lib/health-data-standards/validate/schematron_validator.rb +34 -0
- data/lib/health-data-standards/validate/validation_error.rb +10 -0
- data/lib/health-data-standards/validate/validators.rb +80 -0
- data/lib/hqmf-generator/fulfills.xml.erb +7 -0
- data/lib/hqmf-generator/hqmf-generator.rb +8 -3
- data/lib/hqmf-model/data_criteria.rb +3 -0
- data/lib/hqmf-model/types.rb +29 -0
- data/lib/hqmf-parser/2.0/data_criteria.rb +7 -0
- data/lib/hqmf-parser/2.0/types.rb +24 -0
- data/resources/schema/infrastructure/cda/CDA_SDTC.xsd +44 -0
- data/resources/schema/infrastructure/cda/POCD_MT000040_SDTC.xsd +1500 -0
- data/resources/schema/infrastructure/cda/SDTC.xsd +210 -0
- data/resources/schema/processable/coreschemas/NarrativeBlock.xsd +557 -0
- data/resources/schema/processable/coreschemas/datatypes-base_SDTC.xsd +1850 -0
- data/resources/schema/processable/coreschemas/datatypes.xsd +1375 -0
- data/resources/schema/processable/coreschemas/infrastructureRoot.xsd +27 -0
- data/resources/schema/processable/coreschemas/voc.xsd +2124 -0
- data/resources/schematron/iso-schematron-xslt1/ExtractSchFromRNG.xsl +75 -0
- data/resources/schematron/iso-schematron-xslt1/ExtractSchFromXSD.xsl +77 -0
- data/resources/schematron/iso-schematron-xslt1/iso_abstract_expand.xsl +297 -0
- data/resources/schematron/iso-schematron-xslt1/iso_dsdl_include.xsl +1509 -0
- data/resources/schematron/iso-schematron-xslt1/iso_schematron_message.xsl +55 -0
- data/resources/schematron/iso-schematron-xslt1/iso_schematron_skeleton_for_xslt1.xsl +1844 -0
- data/resources/schematron/iso-schematron-xslt1/iso_svrl_for_xslt1.xsl +605 -0
- data/resources/schematron/iso-schematron-xslt1/readme.txt +101 -0
- data/resources/schematron/iso-schematron-xslt1/schematron-skeleton-api.htm +723 -0
- data/resources/schematron/iso-schematron-xslt2/ExtractSchFromRNG-2.xsl +75 -0
- data/resources/schematron/iso-schematron-xslt2/ExtractSchFromXSD-2.xsl +77 -0
- data/resources/schematron/iso-schematron-xslt2/iso_abstract_expand.xsl +297 -0
- data/resources/schematron/iso-schematron-xslt2/iso_dsdl_include.xsl +1508 -0
- data/resources/schematron/iso-schematron-xslt2/iso_schematron_message_xslt2.xsl +55 -0
- data/resources/schematron/iso-schematron-xslt2/iso_schematron_skeleton_for_saxon.xsl +2299 -0
- data/resources/schematron/iso-schematron-xslt2/iso_svrl_for_xslt2.xsl +684 -0
- data/resources/schematron/iso-schematron-xslt2/readme.txt +100 -0
- data/resources/schematron/iso-schematron-xslt2/sch-messages-cs.xhtml +56 -0
- data/resources/schematron/iso-schematron-xslt2/sch-messages-de.xhtml +57 -0
- data/resources/schematron/iso-schematron-xslt2/sch-messages-en.xhtml +57 -0
- data/resources/schematron/iso-schematron-xslt2/sch-messages-fr.xhtml +54 -0
- data/resources/schematron/iso-schematron-xslt2/sch-messages-nl.xhtml +58 -0
- data/resources/schematron/iso-schematron-xslt2/schematron-skeleton-api.htm +723 -0
- data/resources/schematron/qrda/cat_1/CDAR2_QRDA_I_R1_D3_2015MAY_Schematron.sch +4676 -0
- data/resources/schematron/qrda/cat_1/voc.xml +1177 -0
- data/resources/schematron/qrda/cat_1_r2/QRDA Category I Release 2.sch +4069 -0
- data/resources/schematron/qrda/cat_1_r2/voc.xml +1065 -0
- data/resources/schematron/qrda/cat_3/QRDA Category III.sch +675 -0
- data/resources/schematron/qrda/cat_3/voc.xml +21 -0
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.26.cat1.erb +18 -0
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.32.cat1.erb +4 -0
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.38.cat1.erb +5 -1
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.4.cat1.erb +1 -0
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.64.cat1.erb +20 -0
- data/templates/cat1/_fulfills.cat1.erb +14 -0
- data/templates/cat1/_organization.cat1.erb +2 -1
- data/templates/cat1/_patient_data.cat1.erb +1 -1
- data/templates/cat1/show.cat1.erb +5 -4
- data/templates/cat3/_performance_rate.cat3.erb +5 -1
- data/templates/cat3/show.cat3.erb +1 -1
- metadata +128 -109
- data/lib/health-data-standards/export/ccr.rb +0 -417
- data/lib/health-data-standards/export/green_c32/entry.rb +0 -18
- data/lib/health-data-standards/export/green_c32/export_generator.rb +0 -23
- data/lib/health-data-standards/export/green_c32/record.rb +0 -18
- data/lib/health-data-standards/export/helper/gc32_view_helper.rb +0 -39
- data/lib/health-data-standards/import/ccr/patient_importer.rb +0 -238
- data/lib/health-data-standards/import/ccr/product_importer.rb +0 -60
- data/lib/health-data-standards/import/ccr/provider_importer.rb +0 -49
- data/lib/health-data-standards/import/ccr/result_importer.rb +0 -49
- data/lib/health-data-standards/import/ccr/section_importer.rb +0 -135
- data/lib/health-data-standards/import/ccr/simple_importer.rb +0 -30
- data/lib/health-data-standards/import/green_c32/advance_directive_importer.rb +0 -14
- data/lib/health-data-standards/import/green_c32/allergy_importer.rb +0 -20
- data/lib/health-data-standards/import/green_c32/care_goal_importer.rb +0 -26
- data/lib/health-data-standards/import/green_c32/condition_importer.rb +0 -38
- data/lib/health-data-standards/import/green_c32/encounter_importer.rb +0 -33
- data/lib/health-data-standards/import/green_c32/immunization_importer.rb +0 -23
- data/lib/health-data-standards/import/green_c32/medical_equipment_importer.rb +0 -24
- data/lib/health-data-standards/import/green_c32/medication_importer.rb +0 -68
- data/lib/health-data-standards/import/green_c32/patient_importer.rb +0 -14
- data/lib/health-data-standards/import/green_c32/procedure_importer.rb +0 -27
- data/lib/health-data-standards/import/green_c32/result_importer.rb +0 -43
- data/lib/health-data-standards/import/green_c32/section_importer.rb +0 -186
- data/lib/health-data-standards/import/green_c32/social_history_importer.rb +0 -13
- data/lib/health-data-standards/import/green_c32/support_importer.rb +0 -22
- data/lib/health-data-standards/import/green_c32/vital_sign_importer.rb +0 -21
- data/templates/gc32/_address.gc32.erb +0 -9
- data/templates/gc32/_advance_directive.gc32.erb +0 -5
- data/templates/gc32/_allergy.gc32.erb +0 -12
- data/templates/gc32/_care_goal.gc32.erb +0 -8
- data/templates/gc32/_condition.gc32.erb +0 -10
- data/templates/gc32/_encounter.gc32.erb +0 -28
- data/templates/gc32/_entry.gc32.erb +0 -3
- data/templates/gc32/_entry_attributes.gc32.erb +0 -10
- data/templates/gc32/_immunization.gc32.erb +0 -9
- data/templates/gc32/_insurance_provider.gc32.erb +0 -28
- data/templates/gc32/_medical_equipment.gc32.erb +0 -6
- data/templates/gc32/_medication.gc32.erb +0 -91
- data/templates/gc32/_name.gc32.erb +0 -11
- data/templates/gc32/_organization.gc32.erb +0 -10
- data/templates/gc32/_person_attributes.gc32.erb +0 -7
- data/templates/gc32/_procedure.gc32.erb +0 -9
- data/templates/gc32/_provider.gc32.erb +0 -9
- data/templates/gc32/_result.gc32.erb +0 -12
- data/templates/gc32/_social_history.gc32.erb +0 -6
- data/templates/gc32/_support.gc32.erb +0 -15
- data/templates/gc32/_telecom.gc32.erb +0 -1
- data/templates/gc32/_vital_sign.gc32.erb +0 -4
- data/templates/gc32/record.gc32.erb +0 -97
@@ -0,0 +1,127 @@
|
|
1
|
+
module HealthDataStandards
|
2
|
+
module Validate
|
3
|
+
class MeasureValidator
|
4
|
+
include BaseValidator
|
5
|
+
|
6
|
+
def initialize(template_oid)
|
7
|
+
@template_oid = template_oid
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate(file, data={})
|
12
|
+
@errors = []
|
13
|
+
@doc = get_document(file)
|
14
|
+
@doc.root.add_namespace_definition('cda', 'urn:hl7-org:v3')
|
15
|
+
measure_ids = HealthDataStandards::CQM::Measure.all.map(&:hqmf_id)
|
16
|
+
doc_measure_ids = @doc.xpath(measure_selector).map(&:value).map(&:upcase)
|
17
|
+
#list of all of the set ids in the QRDA
|
18
|
+
doc_neutral_ids = @doc.xpath(neutral_measure_selector).map(&:value).map(&:upcase).sort
|
19
|
+
#list of all of the setids in the QRDA that are also in the bundle, includes duplicates if code appears twice in document
|
20
|
+
bundle_neutral_ids = HealthDataStandards::CQM::Measure.distinct(:hqmf_set_id)
|
21
|
+
doc_bundle_neutral_ids = doc_neutral_ids - (doc_neutral_ids - bundle_neutral_ids)
|
22
|
+
validate_measure_ids(doc_measure_ids, measure_ids, data)
|
23
|
+
validate_set_ids(doc_neutral_ids, doc_bundle_neutral_ids, data)
|
24
|
+
if validate_no_repeating_measure_population_ids(data)
|
25
|
+
validate_measure_ids_set_ids_usage(doc_bundle_neutral_ids, doc_measure_ids, data)
|
26
|
+
end
|
27
|
+
|
28
|
+
@errors
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
#returns true if there are no repeating measures, check to see that the measure id usage is correct
|
34
|
+
def validate_no_repeating_measure_population_ids(data={})
|
35
|
+
noDuplicateMeasures = true
|
36
|
+
doc_population_ids = @doc.xpath(measure_population_selector).map(&:value).map(&:upcase).sort
|
37
|
+
duplicates = doc_population_ids.group_by{ |e| e }.select { |k, v| v.size > 1 }.map(&:first)
|
38
|
+
duplicates.each do |duplicate|
|
39
|
+
measureId = @doc.xpath(find_measure_node_for_population(duplicate)).at_xpath("cda:reference/cda:externalDocument/cda:id[./@root='2.16.840.1.113883.4.738']/@extension")
|
40
|
+
@errors << build_error("Population #{duplicate} for Measure #{measureId.value} reported more than once", "/", data[:file_name])
|
41
|
+
noDuplicateMeasures = false
|
42
|
+
end
|
43
|
+
return noDuplicateMeasures
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_measure_ids(doc_measure_ids, measure_ids, data={})
|
47
|
+
(doc_measure_ids - measure_ids).map do |hqmf_id|
|
48
|
+
@errors << build_error("Invalid HQMF ID Found: #{hqmf_id}", "/", data[:file_name])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_set_ids(doc_neutral_ids, doc_bundle_neutral_ids, data={})
|
53
|
+
#an error will be returned for all of the setids that are in the QRDA that aren't in the bundle
|
54
|
+
(doc_neutral_ids - doc_bundle_neutral_ids).map do |hqmf_set_id|
|
55
|
+
@errors << build_error("Invalid HQMF Set ID Found: #{hqmf_set_id}", "/", data[:file_name])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#does not work if the same measure is reported more than once, nested under the repeating measures test
|
60
|
+
def validate_measure_ids_set_ids_usage(doc_bundle_neutral_ids, doc_measure_ids, data={})
|
61
|
+
#for each of the setIds that are in the bundle, check that they are for the correct measure id
|
62
|
+
entries_start_position = @doc.xpath(first_entry)
|
63
|
+
previous = ""
|
64
|
+
index = 1
|
65
|
+
doc_bundle_neutral_ids.each do |hqmf_set_id|
|
66
|
+
#selects the measure id that is in the same entry as the set id
|
67
|
+
#iterates through multiple instances of the same setId
|
68
|
+
if previous == hqmf_set_id
|
69
|
+
index = index + 1
|
70
|
+
else
|
71
|
+
index = 1
|
72
|
+
end
|
73
|
+
measure_id_entry = doc_measure_ids[(@doc.xpath(location_of_set_id(hqmf_set_id,index)) - entries_start_position)]
|
74
|
+
previous = hqmf_set_id
|
75
|
+
#queries database to see if there is a measure with the combindation of setId and measureId
|
76
|
+
if HealthDataStandards::CQM::Measure.where(hqmf_id: measure_id_entry, hqmf_set_id: hqmf_set_id).length() == 0
|
77
|
+
@errors << build_error("Invalid HQMF Set ID Found: #{hqmf_set_id} for HQMF ID: #{measure_id_entry}", "/", data[:file_name])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def measure_selector
|
83
|
+
"/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
|
84
|
+
"/cda:organizer[./cda:templateId[@root='#{@template_oid}']]/cda:reference[@typeCode='REFR']" +
|
85
|
+
"/cda:externalDocument[@classCode='DOC']/cda:id[@root='2.16.840.1.113883.4.738']/@extension"
|
86
|
+
end
|
87
|
+
|
88
|
+
#finds all of the setIds in the QRDA document
|
89
|
+
def neutral_measure_selector
|
90
|
+
"/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
|
91
|
+
"/cda:organizer[./cda:templateId[@root='#{@template_oid}']]/cda:reference[@typeCode='REFR']" +
|
92
|
+
"/cda:externalDocument[@classCode='DOC']/cda:setId/@root"
|
93
|
+
end
|
94
|
+
|
95
|
+
#finds the node index of the first entry element in the measure template
|
96
|
+
def first_entry
|
97
|
+
"count(//cda:entry[cda:organizer[./cda:templateId[@root='#{@template_oid}']]" +
|
98
|
+
"/cda:reference[@typeCode='REFR']/cda:externalDocument[@classCode='DOC']" +
|
99
|
+
"/cda:id[@root='2.16.840.1.113883.4.738']][1]/preceding-sibling::*)+1"
|
100
|
+
end
|
101
|
+
|
102
|
+
#finds the node index of the extry that the specified setId is in, index is used if the same setId appears twice
|
103
|
+
def location_of_set_id(set_id,index)
|
104
|
+
"count(//cda:entry[cda:organizer[./cda:templateId[@root='#{@template_oid}']]" +
|
105
|
+
"/cda:reference[@typeCode='REFR']/cda:externalDocument[@classCode='DOC']" +
|
106
|
+
"/cda:setId[@root[contains(translate(.,'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLOMNOPQRSTUVWXYZ')" +
|
107
|
+
",'#{set_id}')]]][#{index}]/preceding-sibling::*)+1"
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
def measure_population_selector
|
112
|
+
"/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
|
113
|
+
"/cda:organizer[./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.1']]/cda:component" +
|
114
|
+
"/cda:observation[./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.5']]/cda:reference" +
|
115
|
+
"/cda:externalObservation/cda:id/@root"
|
116
|
+
end
|
117
|
+
|
118
|
+
def find_measure_node_for_population(id)
|
119
|
+
"/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
|
120
|
+
"/cda:organizer[ ./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.1']" +
|
121
|
+
"and ./cda:component/cda:observation[./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.5']]/cda:reference" +
|
122
|
+
"/cda:externalObservation/cda:id[@root='#{id.upcase}']]"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative "reported_result_extractor"
|
2
|
+
module HealthDataStandards
|
3
|
+
module Validate
|
4
|
+
class PerformanceRateValidator
|
5
|
+
include ReportedResultExtractor
|
6
|
+
include BaseValidator
|
7
|
+
|
8
|
+
|
9
|
+
def initialize()
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
# Nothing to see here - Move along
|
16
|
+
def validate(file, data = {})
|
17
|
+
errorsList = []
|
18
|
+
document = get_document(file)
|
19
|
+
#grab measure IDs from QRDA file
|
20
|
+
measure_ids = document.xpath(measure_selector).map(&:value).map(&:upcase)
|
21
|
+
measure_ids.each do |measure_id|
|
22
|
+
measures = HealthDataStandards::CQM::Measure.where(id: measure_id)
|
23
|
+
measures.each do |measure|
|
24
|
+
result_key = measure["population_ids"].dup
|
25
|
+
reported_result, errors = extract_results_by_ids(measure['id'], result_key, document)
|
26
|
+
#only check performace rate when there is one
|
27
|
+
if reported_result['PR'] != nil
|
28
|
+
error = check_performance_rates(reported_result, result_key, measure['id'], data)
|
29
|
+
if error != nil
|
30
|
+
errorsList << error
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
errorsList
|
36
|
+
end
|
37
|
+
|
38
|
+
def calculate_performance_rates(reported_result)
|
39
|
+
#Just in case a measure does not report these populations
|
40
|
+
denex = 0
|
41
|
+
denexcep = 0
|
42
|
+
denom = 0
|
43
|
+
numer = 0
|
44
|
+
if reported_result['DENEX'] != nil
|
45
|
+
denex = reported_result['DENEX']
|
46
|
+
end
|
47
|
+
if reported_result['DENEXCEP'] != nil
|
48
|
+
denexcep = reported_result['DENEXCEP']
|
49
|
+
end
|
50
|
+
if reported_result['DENOM'] != nil
|
51
|
+
denom = reported_result['DENOM']
|
52
|
+
end
|
53
|
+
if reported_result['NUMER'] != nil
|
54
|
+
numer = reported_result['NUMER']
|
55
|
+
end
|
56
|
+
denom = denom - denex - denexcep
|
57
|
+
pr = 0
|
58
|
+
if denom == 0
|
59
|
+
pr = "NA"
|
60
|
+
else
|
61
|
+
pr = numer / denom.to_f
|
62
|
+
end
|
63
|
+
return pr
|
64
|
+
end
|
65
|
+
|
66
|
+
def check_performance_rates(reported_result, population_ids, measure_id, data = {})
|
67
|
+
expected = calculate_performance_rates(reported_result)
|
68
|
+
_ids = population_ids
|
69
|
+
if expected == "NA"
|
70
|
+
if reported_result['PR']['nullFlavor'] != "NA"
|
71
|
+
return build_error("Reported Performance Rate for Numerator #{_ids['NUMER']} should be NA", "/", data[:file_name])
|
72
|
+
end
|
73
|
+
else
|
74
|
+
if reported_result['PR']['nullFlavor'] == "NA"
|
75
|
+
return build_error("Reported Performance Rate for Numerator #{_ids['NUMER']} should not be NA", "/", data[:file_name])
|
76
|
+
else
|
77
|
+
if (reported_result['PR']['value'].split('.',2).last.size > 6)
|
78
|
+
return build_error("Reported Performance Rate SHALL not have a precision greater than .000001 ", "/", data[:file_name])
|
79
|
+
elsif (reported_result['PR']['value'].to_f - expected.round(6)).abs > 0.0000001
|
80
|
+
return build_error("Reported Performance Rate of #{reported_result['PR']['value']} for Numerator #{_ids['NUMER']} does not match expected value of #{expected.to_s[0,8]}.", "/", data[:file_name])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def measure_selector
|
87
|
+
"/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
|
88
|
+
"/cda:organizer[./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.1']]/cda:reference[@typeCode='REFR']" +
|
89
|
+
"/cda:externalDocument[@classCode='DOC']/cda:id[@root='2.16.840.1.113883.4.738']/@extension"
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module HealthDataStandards
|
2
|
+
module Validate
|
3
|
+
module ReportedResultExtractor
|
4
|
+
|
5
|
+
#takes a document and a list of 1 or more id hashes, e.g.:
|
6
|
+
#[{measure_id:"8a4d92b2-36af-5758-0136-ea8c43244986", set_id:"03876d69-085b-415c-ae9d-9924171040c2", ipp:"D77106C4-8ED0-4C5D-B29E-13DBF255B9FF", den:"8B0FA80F-8FFE-494C-958A-191C1BB36DBF", num:"9363135E-A816-451F-8022-96CDA7E540DD"}]
|
7
|
+
#returns nil if nothing matching is found
|
8
|
+
# returns a hash with the values of the populations filled out along with the population_ids added to the result
|
9
|
+
|
10
|
+
|
11
|
+
def extract_results_by_ids(measure_id, ids, doc)
|
12
|
+
results = nil
|
13
|
+
_ids = ids.dup
|
14
|
+
stratification = _ids.delete("stratification")
|
15
|
+
stratification ||= _ids.delete("STRAT")
|
16
|
+
errors = []
|
17
|
+
nodes = find_measure_node(measure_id, doc)
|
18
|
+
|
19
|
+
if nodes.nil? || nodes.empty?
|
20
|
+
# short circuit and return nil
|
21
|
+
return {}
|
22
|
+
end
|
23
|
+
|
24
|
+
nodes.each do |n|
|
25
|
+
results = get_measure_components(n, _ids, stratification)
|
26
|
+
break if (results != nil || (results != nil && !results.empty?))
|
27
|
+
end
|
28
|
+
return nil if results.nil?
|
29
|
+
results[:population_ids] = ids.dup
|
30
|
+
results
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_measure_node(id, doc)
|
34
|
+
xpath_measures = %Q{/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section
|
35
|
+
/cda:entry/cda:organizer[ ./cda:templateId[@root = "2.16.840.1.113883.10.20.27.3.1"]
|
36
|
+
and ./cda:reference/cda:externalDocument/cda:id[#{translate("@extension")}='#{id.upcase}' and #{translate("@root")}='2.16.840.1.113883.4.738']]}
|
37
|
+
return doc.xpath(xpath_measures)
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_measure_components(n,ids, stratification)
|
41
|
+
results = {:supplemental_data =>{}}
|
42
|
+
ids.each_pair do |k,v|
|
43
|
+
val = nil
|
44
|
+
sup = nil
|
45
|
+
if (k == 'OBSERV')
|
46
|
+
msrpopl = ids['MSRPOPL']
|
47
|
+
val, sup = extract_cv_value(n,v,msrpopl, stratification)
|
48
|
+
else
|
49
|
+
val,sup,pr =extract_component_value(n,k,v,stratification)
|
50
|
+
end
|
51
|
+
if !val.nil?
|
52
|
+
results[k.to_s] = val
|
53
|
+
results[:supplemental_data][k] = sup
|
54
|
+
else
|
55
|
+
# return nil
|
56
|
+
end
|
57
|
+
if !pr.nil?
|
58
|
+
results["PR"] = pr
|
59
|
+
end
|
60
|
+
end
|
61
|
+
results
|
62
|
+
end
|
63
|
+
|
64
|
+
def extract_cv_value(node, id, msrpopl, strata = nil)
|
65
|
+
xpath_observation = %{ cda:component/cda:observation[./cda:value[@code = "MSRPOPL"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{msrpopl.upcase}']]}
|
66
|
+
cv = node.at_xpath(xpath_observation)
|
67
|
+
return nil unless cv
|
68
|
+
val = nil
|
69
|
+
if strata
|
70
|
+
strata_path = %{ cda:entryRelationship[@typeCode="COMP"]/cda:observation[./cda:templateId[@root = "2.16.840.1.113883.10.20.27.3.4"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{strata.upcase}']]}
|
71
|
+
n = cv.xpath(strata_path)
|
72
|
+
val = get_cv_value(n,id)
|
73
|
+
else
|
74
|
+
val = get_cv_value(cv,id)
|
75
|
+
end
|
76
|
+
return val, (strata.nil? ? extract_supplemental_data(cv) : nil)
|
77
|
+
end
|
78
|
+
|
79
|
+
def extract_component_value(node, code, id, strata = nil)
|
80
|
+
xpath_observation = %{ cda:component/cda:observation[./cda:value[@code = "#{code}"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{id.upcase}']]}
|
81
|
+
cv = node.at_xpath(xpath_observation)
|
82
|
+
return nil unless cv
|
83
|
+
val = nil
|
84
|
+
if strata
|
85
|
+
strata_path = %{ cda:entryRelationship[@typeCode="COMP"]/cda:observation[./cda:templateId[@root = "2.16.840.1.113883.10.20.27.3.4"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{strata.upcase}']]}
|
86
|
+
n = cv.xpath(strata_path)
|
87
|
+
val = get_aggregate_count(n) if n
|
88
|
+
else
|
89
|
+
val = get_aggregate_count(cv)
|
90
|
+
end
|
91
|
+
#Performance rate is only applicable for unstratified values
|
92
|
+
if code == "NUMER" && strata == nil
|
93
|
+
pref_rate_value = extract_performance_rate(node,code,id)
|
94
|
+
end
|
95
|
+
return val,(strata.nil? ? extract_supplemental_data(cv) : nil),pref_rate_value
|
96
|
+
end
|
97
|
+
|
98
|
+
def extract_performance_rate(node,code,id)
|
99
|
+
xpath_perf_rate = %{ cda:component/cda:observation[./cda:templateId[@root = "2.16.840.1.113883.10.20.27.3.14"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{id.upcase}']]/cda:value}
|
100
|
+
perf_rate = node.at_xpath(xpath_perf_rate)
|
101
|
+
pref_rate_value = {}
|
102
|
+
if perf_rate != nil
|
103
|
+
if perf_rate.at_xpath("./@nullFlavor")
|
104
|
+
pref_rate_value["nullFlavor"] = "NA"
|
105
|
+
return pref_rate_value
|
106
|
+
else
|
107
|
+
pref_rate_value["value"] = perf_rate.at_xpath("./@value").value
|
108
|
+
return pref_rate_value
|
109
|
+
end
|
110
|
+
end
|
111
|
+
return nil
|
112
|
+
end
|
113
|
+
# convert numbers in value nodes to Int / Float as necessary TODO add more types other than 'REAL'
|
114
|
+
def convert_value(value_node)
|
115
|
+
if value_node.nil?
|
116
|
+
return
|
117
|
+
end
|
118
|
+
if value_node['type'] == 'REAL' || value_node['value'].include?('.')
|
119
|
+
return value_node['value'].to_f
|
120
|
+
else
|
121
|
+
return value_node['value'].to_i
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
#given an observation node with an aggregate count node, return the reported and expected value within the count node
|
126
|
+
def get_cv_value(node, cv_id)
|
127
|
+
xpath_value = %{cda:entryRelationship/cda:observation[./cda:templateId[@root="2.16.840.1.113883.10.20.27.3.2"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{cv_id.upcase}']]/cda:value}
|
128
|
+
|
129
|
+
value_node = node.at_xpath(xpath_value)
|
130
|
+
value = convert_value(value_node) if value_node
|
131
|
+
value
|
132
|
+
end
|
133
|
+
|
134
|
+
#given an observation node with an aggregate count node, return the reported and expected value within the count node
|
135
|
+
def get_aggregate_count(node)
|
136
|
+
xpath_value = 'cda:entryRelationship/cda:observation[./cda:templateId[@root="2.16.840.1.113883.10.20.27.3.3"]]/cda:value'
|
137
|
+
value_node = node.at_xpath(xpath_value)
|
138
|
+
value = convert_value(value_node) if value_node
|
139
|
+
value
|
140
|
+
end
|
141
|
+
|
142
|
+
def extract_supplemental_data(cv)
|
143
|
+
ret = {}
|
144
|
+
supplemental_data_mapping = {"RACE"=> "2.16.840.1.113883.10.20.27.3.8",
|
145
|
+
"ETHNICITY" => "2.16.840.1.113883.10.20.27.3.7",
|
146
|
+
"SEX" => "2.16.840.1.113883.10.20.27.3.6",
|
147
|
+
"PAYER" => "2.16.840.1.113883.10.20.27.3.9"}
|
148
|
+
supplemental_data_mapping.each_pair do |supp, id|
|
149
|
+
key_hash = {}
|
150
|
+
xpath = "cda:entryRelationship/cda:observation[cda:templateId[@root='#{id}']]"
|
151
|
+
(cv.xpath(xpath) || []).each do |node|
|
152
|
+
value = node.at_xpath('cda:value')
|
153
|
+
count = get_aggregate_count(node)
|
154
|
+
if value.at_xpath("./@nullFlavor")
|
155
|
+
key_hash["UNK"] = count
|
156
|
+
else
|
157
|
+
key_hash[value['code']] = count
|
158
|
+
end
|
159
|
+
end
|
160
|
+
ret[supp.to_s] = key_hash
|
161
|
+
end
|
162
|
+
ret
|
163
|
+
end
|
164
|
+
|
165
|
+
def translate(id)
|
166
|
+
%{translate(#{id}, "abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ")}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module HealthDataStandards
|
2
|
+
module Validate
|
3
|
+
module Schema
|
4
|
+
class Validator
|
5
|
+
include BaseValidator
|
6
|
+
|
7
|
+
def initialize(name, schema_file)
|
8
|
+
@name = name
|
9
|
+
@schema_file = schema_file
|
10
|
+
@xsd = Nokogiri::XML::Schema(File.new(@schema_file))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Validate the document against the configured schema
|
14
|
+
def validate(document,data={})
|
15
|
+
@xsd.errors.clear
|
16
|
+
doc = get_document(document)
|
17
|
+
@xsd.validate(doc).map do |error|
|
18
|
+
build_error(error.message, "/", data[:file_name])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module HealthDataStandards
|
2
|
+
module Validate
|
3
|
+
module Schematron
|
4
|
+
module CProcessor
|
5
|
+
|
6
|
+
|
7
|
+
def get_errors(document)
|
8
|
+
document = get_document(document)
|
9
|
+
processor.transform(document)
|
10
|
+
end
|
11
|
+
|
12
|
+
def processor
|
13
|
+
return @processor if @processor
|
14
|
+
doc = Nokogiri::XML(File.open(@schematron_file))
|
15
|
+
doc.root["defaultPhase"] = ("errors")
|
16
|
+
|
17
|
+
xslt = Nokogiri::XSLT(File.open(ISO_SCHEMATRON))
|
18
|
+
|
19
|
+
result = xslt.transform(doc)
|
20
|
+
#this is stupid but needs to be done to assocaite the xslt file with a dirctory
|
21
|
+
result = Nokogiri::XML(result.to_s,@schematron_file)
|
22
|
+
@processor = Nokogiri::XSLT::Stylesheet.parse_stylesheet_doc(result)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'java'
|
2
|
+
require_relative '../../../../java/saxon9he.jar'
|
3
|
+
|
4
|
+
java_import "java.io.StringReader"
|
5
|
+
java_import "java.io.StringWriter"
|
6
|
+
|
7
|
+
java_import "javax.xml.parsers.DocumentBuilder"
|
8
|
+
java_import "javax.xml.parsers.DocumentBuilderFactory"
|
9
|
+
|
10
|
+
java_import "javax.xml.transform.TransformerFactory"
|
11
|
+
java_import "javax.xml.transform.Transformer"
|
12
|
+
java_import "javax.xml.transform.dom.DOMSource"
|
13
|
+
java_import "javax.xml.transform.stream.StreamSource"
|
14
|
+
java_import "javax.xml.transform.stream.StreamResult"
|
15
|
+
|
16
|
+
java_import "org.w3c.dom.Document"
|
17
|
+
|
18
|
+
|
19
|
+
TRANSFORMER_FACTORY_IMPL = "net.sf.saxon.TransformerFactoryImpl"
|
20
|
+
|
21
|
+
module HealthDataStandards
|
22
|
+
module Validate
|
23
|
+
module Schematron
|
24
|
+
module JavaProcessor
|
25
|
+
|
26
|
+
ISO_SCHEMATRON2 = File.join(DIR, 'resources/schematron/iso-schematron-xslt2/iso_svrl_for_xslt2.xsl')
|
27
|
+
|
28
|
+
class HdsUrlResolver
|
29
|
+
include javax.xml.transform.URIResolver
|
30
|
+
|
31
|
+
def initialize(schematron)
|
32
|
+
@file = schematron
|
33
|
+
end
|
34
|
+
|
35
|
+
def resolve(href, base)
|
36
|
+
path = File.join(File.dirname(@file), href)
|
37
|
+
return StreamSource.new(java.io.File.new(path))
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_errors(document)
|
43
|
+
document_j = get_document_j(document)
|
44
|
+
output = build_transformer(StringReader.new(processor), StreamSource.new(document_j), true)
|
45
|
+
Nokogiri::XML(output)
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_document_j(doc)
|
49
|
+
case doc
|
50
|
+
when File
|
51
|
+
java.io.File.new(doc.path)
|
52
|
+
else
|
53
|
+
StringReader.new(doc.to_s)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def processor
|
58
|
+
@processor ||= build_transformer(java.io.File.new(ISO_SCHEMATRON2), schematron_file)
|
59
|
+
end
|
60
|
+
|
61
|
+
def schematron_file
|
62
|
+
# this allows us to run the validation utility app in jBoss/TorqueBox
|
63
|
+
# for some reason it breaks the first time you call DocumentBuilderFactory,
|
64
|
+
# so the solution is to catch the error and retry
|
65
|
+
# TODO: pull this out when the above is no longer the case.
|
66
|
+
begin
|
67
|
+
dbf = DocumentBuilderFactory.new_instance
|
68
|
+
rescue Exception => ex
|
69
|
+
retry
|
70
|
+
end
|
71
|
+
dbf.setIgnoringElementContentWhitespace(true);
|
72
|
+
db = dbf.new_document_builder
|
73
|
+
document = db.parse(java.io.File.new(@schematron_file))
|
74
|
+
|
75
|
+
root = document.document_element
|
76
|
+
phase = root.set_attribute("defaultPhase", "errors")
|
77
|
+
|
78
|
+
DOMSource.new(root)
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_transformer(xslt, input_file, url=false)
|
82
|
+
factory = TransformerFactory.newInstance(TRANSFORMER_FACTORY_IMPL, nil)
|
83
|
+
factory.uri_resolver = HdsUrlResolver.new(@schematron_file) if url
|
84
|
+
transformer = factory.new_transformer(StreamSource.new(xslt))
|
85
|
+
sw = StringWriter.new
|
86
|
+
output = StreamResult.new(sw)
|
87
|
+
transformer.transform(input_file, output)
|
88
|
+
sw.to_s
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|