fhir_scorecard 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +56 -0
- data/.simplecov +16 -0
- data/.travis.yml +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +63 -0
- data/LICENSE +201 -0
- data/README.md +94 -0
- data/Rakefile +25 -0
- data/fhir_scorecard.gemspec +15 -0
- data/lib/fhir_scorecard.rb +15 -0
- data/lib/rubrics/codes_exist.rb +72 -0
- data/lib/rubrics/codes_umls.rb +105 -0
- data/lib/rubrics/codes_umls_preferred_descriptions.rb +105 -0
- data/lib/rubrics/completeness.rb +61 -0
- data/lib/rubrics/cvx_immunizations.rb +32 -0
- data/lib/rubrics/cvx_meds.rb +73 -0
- data/lib/rubrics/datetimes_iso8601.rb +73 -0
- data/lib/rubrics/labs_loinc.rb +38 -0
- data/lib/rubrics/quantities_ucum.rb +69 -0
- data/lib/rubrics/references_resolve.rb +86 -0
- data/lib/rubrics/rxnorm_meds.rb +73 -0
- data/lib/rubrics/smoking_status.rb +48 -0
- data/lib/rubrics/snomed_conditions.rb +32 -0
- data/lib/rubrics/vital_signs.rb +64 -0
- data/lib/rubrics.rb +23 -0
- data/lib/scorecard.rb +46 -0
- data/lib/tasks/tasks.rake +207 -0
- data/lib/terminology.rb +135 -0
- data/logs/.keep +0 -0
- data/terminology/.keep +0 -0
- data/test/test_helper.rb +9 -0
- data/test/unit/basic_test.rb +52 -0
- data/test/unit/codes_exist_test.rb +169 -0
- data/test/unit/terminology_test.rb +91 -0
- metadata +90 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
module FHIR
|
2
|
+
class CodesUmls < FHIR::Rubrics
|
3
|
+
|
4
|
+
# SNOMED, LOINC, RxNorm, ICD9, and ICD10 codes validate against UMLS
|
5
|
+
rubric :codes_umls do |record|
|
6
|
+
results = {
|
7
|
+
:eligible_fields => 0,
|
8
|
+
:validated_fields => 0
|
9
|
+
}
|
10
|
+
resources = record.entry.map{|e|e.resource}
|
11
|
+
resources.each do |resource|
|
12
|
+
results.merge!(check_fields(resource)){|k,a,b|a+b}
|
13
|
+
end
|
14
|
+
|
15
|
+
percentage = results[:validated_fields].to_f / results[:eligible_fields].to_f
|
16
|
+
percentage = 0.0 if percentage.nan?
|
17
|
+
points = 10.0 * percentage
|
18
|
+
message = "#{(100 * percentage).to_i}% (#{results[:validated_fields]}/#{results[:eligible_fields]}) of SNOMED, LOINC, RxNorm, ICD9, and ICD10 validated against UMLS. Maximum of 10 points."
|
19
|
+
response(points.to_i,message)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.check_fields(fhir_model)
|
23
|
+
results = {
|
24
|
+
:eligible_fields => 0,
|
25
|
+
:validated_fields => 0
|
26
|
+
}
|
27
|
+
# check each codeable field
|
28
|
+
fhir_model.class::METADATA.each do |key, meta|
|
29
|
+
field_name = meta['local_name'] || key
|
30
|
+
declared_binding = eligible_binding(meta)
|
31
|
+
supposed_to_be_umls = !declared_binding.nil?
|
32
|
+
value = fhir_model.instance_variable_get("@#{field_name}")
|
33
|
+
|
34
|
+
if meta['type']=='Coding'
|
35
|
+
if value.is_a?(Array)
|
36
|
+
value.each do |v|
|
37
|
+
results.merge!(check_coding(v,supposed_to_be_umls,declared_binding)){|k,a,b|a+b}
|
38
|
+
end
|
39
|
+
else
|
40
|
+
results.merge!(check_coding(value,supposed_to_be_umls,declared_binding)){|k,a,b|a+b}
|
41
|
+
end
|
42
|
+
elsif meta['type']=='CodeableConcept'
|
43
|
+
if value.is_a?(Array)
|
44
|
+
value.each do |cc|
|
45
|
+
cc.coding.each do |c|
|
46
|
+
results.merge!(check_coding(c,supposed_to_be_umls,declared_binding)){|k,a,b|a+b}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
else
|
50
|
+
if value.nil?
|
51
|
+
results[:eligible_fields] += 1 if supposed_to_be_umls
|
52
|
+
else
|
53
|
+
value.coding.each do |c|
|
54
|
+
results.merge!(check_coding(c,supposed_to_be_umls,declared_binding)){|k,a,b|a+b}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
elsif !value.nil?
|
59
|
+
if value.is_a?(Array)
|
60
|
+
value.each{|v| results.merge!(check_fields(v)){|k,a,b|a+b} if v.is_a?(FHIR::Model)}
|
61
|
+
else # not an Array
|
62
|
+
results.merge!(check_fields(value)){|k,a,b|a+b} if value.is_a?(FHIR::Model)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
results
|
67
|
+
end
|
68
|
+
|
69
|
+
# This method checks whether or not the coding is eligible and validates.
|
70
|
+
# A coding is eligible if it is supposed to be UMLS (as declared in the
|
71
|
+
# resource metadata) or it is locally declared as one of the UMLS systems.
|
72
|
+
# If the coding is eligible, then it is validated.
|
73
|
+
def self.check_coding(coding,supposed_to_be_umls,declared_binding)
|
74
|
+
result = {
|
75
|
+
:eligible_fields => 0,
|
76
|
+
:validated_fields => 0
|
77
|
+
}
|
78
|
+
if coding.nil?
|
79
|
+
result[:eligible_fields] += 1 if supposed_to_be_umls
|
80
|
+
else
|
81
|
+
local_binding = coding.system || declared_binding
|
82
|
+
if supposed_to_be_umls || FHIR::Terminology::CODE_SYSTEMS[local_binding]
|
83
|
+
result[:eligible_fields] += 1
|
84
|
+
in_umls = !FHIR::Terminology.get_description(FHIR::Terminology::CODE_SYSTEMS[local_binding],coding.code).nil?
|
85
|
+
result[:validated_fields] +=1 if in_umls
|
86
|
+
end
|
87
|
+
end
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
# This method checks the metadata on this field and returns the binding key (e.g. 'SNOMED')
|
92
|
+
# if one of the UMLS code systems was specified. Otherwise, it returns nil.
|
93
|
+
def self.eligible_binding(metadata)
|
94
|
+
vs_binding = metadata['binding']
|
95
|
+
matching_binding = FHIR::Terminology::CODE_SYSTEMS[vs_binding['uri']] if !vs_binding.nil? && vs_binding['uri']
|
96
|
+
|
97
|
+
valid_codes = metadata['valid_codes']
|
98
|
+
matching_code_system = valid_codes.keys.find{|k| FHIR::Terminology::CODE_SYSTEMS[k]} if !valid_codes.nil?
|
99
|
+
matching_code_system = FHIR::Terminology::CODE_SYSTEMS[matching_code_system] if matching_code_system
|
100
|
+
|
101
|
+
matching_binding || matching_code_system
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module FHIR
|
2
|
+
class CodesUmlsPreferredDescriptions < FHIR::Rubrics
|
3
|
+
|
4
|
+
# SNOMED, LOINC, RxNorm, ICD9, and ICD10 codes in UMLS use preferred descriptions
|
5
|
+
rubric :descriptions do |record|
|
6
|
+
results = {
|
7
|
+
:eligible_fields => 0,
|
8
|
+
:correct_descriptions => 0
|
9
|
+
}
|
10
|
+
resources = record.entry.map{|e|e.resource}
|
11
|
+
resources.each do |resource|
|
12
|
+
results.merge!(check_fields(resource)){|k,a,b|a+b}
|
13
|
+
end
|
14
|
+
|
15
|
+
percentage = results[:correct_descriptions].to_f / results[:eligible_fields].to_f
|
16
|
+
percentage = 0.0 if percentage.nan?
|
17
|
+
points = 10.0 * percentage
|
18
|
+
message = "#{(100 * percentage).to_i}% (#{results[:correct_descriptions]}/#{results[:eligible_fields]}) of SNOMED, LOINC, RxNorm, ICD9, and ICD10 codes use preferred descriptions. Maximum of 10 points."
|
19
|
+
response(points.to_i,message)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.check_fields(fhir_model)
|
23
|
+
results = {
|
24
|
+
:eligible_fields => 0,
|
25
|
+
:correct_descriptions => 0
|
26
|
+
}
|
27
|
+
# check each codeable field
|
28
|
+
fhir_model.class::METADATA.each do |key, meta|
|
29
|
+
field_name = meta['local_name'] || key
|
30
|
+
declared_binding = eligible_binding(meta)
|
31
|
+
supposed_to_be_umls = !declared_binding.nil?
|
32
|
+
value = fhir_model.instance_variable_get("@#{field_name}")
|
33
|
+
|
34
|
+
if meta['type']=='Coding'
|
35
|
+
if value.is_a?(Array)
|
36
|
+
value.each do |v|
|
37
|
+
results.merge!(check_coding(v,supposed_to_be_umls,declared_binding)){|k,a,b|a+b}
|
38
|
+
end
|
39
|
+
else
|
40
|
+
results.merge!(check_coding(value,supposed_to_be_umls,declared_binding)){|k,a,b|a+b}
|
41
|
+
end
|
42
|
+
elsif meta['type']=='CodeableConcept'
|
43
|
+
if value.is_a?(Array)
|
44
|
+
value.each do |cc|
|
45
|
+
cc.coding.each do |c|
|
46
|
+
results.merge!(check_coding(c,supposed_to_be_umls,declared_binding)){|k,a,b|a+b}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
else
|
50
|
+
if value.nil?
|
51
|
+
results[:eligible_fields] += 1 if supposed_to_be_umls
|
52
|
+
else
|
53
|
+
value.coding.each do |c|
|
54
|
+
results.merge!(check_coding(c,supposed_to_be_umls,declared_binding)){|k,a,b|a+b}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
elsif !value.nil?
|
59
|
+
if value.is_a?(Array)
|
60
|
+
value.each{|v| results.merge!(check_fields(v)){|k,a,b|a+b} if v.is_a?(FHIR::Model)}
|
61
|
+
else # not an Array
|
62
|
+
results.merge!(check_fields(value)){|k,a,b|a+b} if value.is_a?(FHIR::Model)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
results
|
67
|
+
end
|
68
|
+
|
69
|
+
# This method checks whether or not the coding is eligible and checks the description.
|
70
|
+
# A coding is eligible if it is supposed to be UMLS (as declared in the
|
71
|
+
# resource metadata) or it is locally declared as one of the UMLS systems.
|
72
|
+
# If the coding is eligible, then the description is checked against the preferred description.
|
73
|
+
def self.check_coding(coding,supposed_to_be_umls,declared_binding)
|
74
|
+
result = {
|
75
|
+
:eligible_fields => 0,
|
76
|
+
:correct_descriptions => 0
|
77
|
+
}
|
78
|
+
if coding.nil?
|
79
|
+
result[:eligible_fields] += 1 if supposed_to_be_umls
|
80
|
+
else
|
81
|
+
local_binding = coding.system || declared_binding
|
82
|
+
if supposed_to_be_umls || FHIR::Terminology::CODE_SYSTEMS[local_binding]
|
83
|
+
result[:eligible_fields] += 1
|
84
|
+
preferred = FHIR::Terminology.get_description(FHIR::Terminology::CODE_SYSTEMS[local_binding],coding.code)
|
85
|
+
result[:correct_descriptions] +=1 if preferred==coding.display
|
86
|
+
end
|
87
|
+
end
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
# This method checks the metadata on this field and returns the binding key (e.g. 'SNOMED')
|
92
|
+
# if one of the UMLS code systems was specified. Otherwise, it returns nil.
|
93
|
+
def self.eligible_binding(metadata)
|
94
|
+
vs_binding = metadata['binding']
|
95
|
+
matching_binding = FHIR::Terminology::CODE_SYSTEMS[vs_binding['uri']] if !vs_binding.nil? && vs_binding['uri']
|
96
|
+
|
97
|
+
valid_codes = metadata['valid_codes']
|
98
|
+
matching_code_system = valid_codes.keys.find{|k| FHIR::Terminology::CODE_SYSTEMS[k]} if !valid_codes.nil?
|
99
|
+
matching_code_system = FHIR::Terminology::CODE_SYSTEMS[matching_code_system] if matching_code_system
|
100
|
+
|
101
|
+
matching_binding || matching_code_system
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module FHIR
|
2
|
+
class Completeness < FHIR::Rubrics
|
3
|
+
|
4
|
+
REQUIRED = [
|
5
|
+
'AllergyIntolerance','Condition','CarePlan','Immunization',
|
6
|
+
'Observation','Encounter'
|
7
|
+
]
|
8
|
+
|
9
|
+
EXPECTED = [
|
10
|
+
'FamilyMemberHistory','DiagnosticReport','ImagingStudy','VisionPrescription',
|
11
|
+
'Practitioner','Organization','Communication','Appointment','DeviceUseStatement',
|
12
|
+
'QuestionnaireResponse','Coverage'
|
13
|
+
]
|
14
|
+
|
15
|
+
MEDICATIONS = [ 'MedicationStatement','MedicationDispense','MedicationAdministration','MedicationOrder' ]
|
16
|
+
|
17
|
+
# A Patient Record is not complete without certain required items and medications.
|
18
|
+
rubric :completeness do |record|
|
19
|
+
|
20
|
+
missing_required = REQUIRED.clone
|
21
|
+
missing_expected = EXPECTED.clone
|
22
|
+
missing_meds = MEDICATIONS.clone
|
23
|
+
|
24
|
+
resources = record.entry.map{|e|e.resource}
|
25
|
+
resources.each do |resource|
|
26
|
+
if !resource.nil?
|
27
|
+
missing_required.delete(resource.resourceType)
|
28
|
+
missing_expected.delete(resource.resourceType)
|
29
|
+
missing_meds.delete(resource.resourceType)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# 15 points for required resources
|
34
|
+
numerator = (REQUIRED.length.to_f - missing_required.length.to_f)
|
35
|
+
numerator += 1 if (missing_meds.length < MEDICATIONS.length)
|
36
|
+
denominator = REQUIRED.length.to_f + 1.0 # add one for medications
|
37
|
+
percentage_required = ( numerator / denominator )
|
38
|
+
percentage_required = 0.0 if percentage_required.nan?
|
39
|
+
points = 20.0 * percentage_required
|
40
|
+
|
41
|
+
# 5 points for expected resources
|
42
|
+
numerator = (EXPECTED.length.to_f - missing_expected.length.to_f)
|
43
|
+
denominator = EXPECTED.length.to_f
|
44
|
+
percentage_expected = ( numerator / denominator )
|
45
|
+
percentage_expected = 0.0 if percentage_expected.nan?
|
46
|
+
points += (5.0 * percentage_expected)
|
47
|
+
|
48
|
+
message = "#{(100 * percentage_required).to_i}% of REQUIRED Resources and #{(100 * percentage_expected).to_i}% of EXPECTED Resources were present. Maximum of 20 points."
|
49
|
+
response(points.to_i,message)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.get_vital_code(codeableconcept)
|
53
|
+
return nil if codeableconcept.nil? || codeableconcept.coding.nil?
|
54
|
+
|
55
|
+
coding = codeableconcept.coding.find{|x| x.system=='http://loinc.org' && VITAL_SIGNS.has_key?(x.code)}
|
56
|
+
code = coding.code if coding
|
57
|
+
code
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module FHIR
|
2
|
+
class CVXImmunizations < FHIR::Rubrics
|
3
|
+
|
4
|
+
# Immunizations should be coded with CVX
|
5
|
+
rubric :cvx_immunizations do |record|
|
6
|
+
results = {
|
7
|
+
:eligible_fields => 0,
|
8
|
+
:validated_fields => 0
|
9
|
+
}
|
10
|
+
|
11
|
+
resources = record.entry.map{|e|e.resource}
|
12
|
+
resources.each do |resource|
|
13
|
+
if resource.is_a?(FHIR::Immunization)
|
14
|
+
results[:eligible_fields] += 1
|
15
|
+
results[:validated_fields] += 1 if cvx?(resource.vaccineCode)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
percentage = results[:validated_fields].to_f / results[:eligible_fields].to_f
|
20
|
+
percentage = 0.0 if percentage.nan?
|
21
|
+
points = 10.0 * percentage
|
22
|
+
message = "#{(100 * percentage).to_i}% (#{results[:validated_fields]}/#{results[:eligible_fields]}) of Immunization vaccine codes use CVX. Maximum of 10 points."
|
23
|
+
response(points.to_i,message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.cvx?(codeableconcept)
|
27
|
+
return false if codeableconcept.nil? || codeableconcept.coding.nil?
|
28
|
+
codeableconcept.coding.any?{|x| x.system=='http://hl7.org/fhir/sid/cvx' && FHIR::Terminology.get_description('CVX',x.code)}
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module FHIR
|
2
|
+
class CVXMeds < FHIR::Rubrics
|
3
|
+
|
4
|
+
# Medications should *NOT* be coded with CVX
|
5
|
+
rubric :cvx_medications do |record|
|
6
|
+
results = {
|
7
|
+
:eligible_fields => 0,
|
8
|
+
:validated_fields => 0
|
9
|
+
}
|
10
|
+
# Medication.code (CodeableConcept)
|
11
|
+
# MedicationAdministration.medicationCodeableConcept / medicationReference
|
12
|
+
# MedicationDispense.medicationCodeableConcept / medicationReference
|
13
|
+
# MedicationOrder.medicationCodeableConcept / medicationReference
|
14
|
+
# MedicationStatement.medicationCodeableConcept / medicationReference
|
15
|
+
|
16
|
+
resources = record.entry.map{|e|e.resource}
|
17
|
+
resources.each do |resource|
|
18
|
+
if resource.is_a?(FHIR::Medication)
|
19
|
+
results[:eligible_fields] += 1
|
20
|
+
results[:validated_fields] += 1 if cvx?(resource.code)
|
21
|
+
elsif (
|
22
|
+
resource.is_a?(FHIR::MedicationOrder) ||
|
23
|
+
resource.is_a?(FHIR::MedicationDispense) ||
|
24
|
+
resource.is_a?(FHIR::MedicationAdministration) ||
|
25
|
+
resource.is_a?(FHIR::MedicationStatement) )
|
26
|
+
if resource.medicationCodeableConcept
|
27
|
+
results[:eligible_fields] += 1
|
28
|
+
results[:validated_fields] += 1 if cvx?(resource.medicationCodeableConcept)
|
29
|
+
elsif resource.medicationReference
|
30
|
+
results[:eligible_fields] += 1
|
31
|
+
results[:validated_fields] += 1 if local_cvx_reference?(resource.medicationReference,record,resource.contained)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
percentage = results[:validated_fields].to_f / results[:eligible_fields].to_f
|
37
|
+
percentage = 0.0 if percentage.nan?
|
38
|
+
points = -10.0 * percentage
|
39
|
+
message = "#{(100 * percentage).to_i}% (#{results[:validated_fields]}/#{results[:eligible_fields]}) of Medication[x] Resource codes use CVX. Maximum of 10 point penalty."
|
40
|
+
response(points.to_i,message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.cvx?(codeableconcept)
|
44
|
+
return false if codeableconcept.nil? || codeableconcept.coding.nil?
|
45
|
+
codeableconcept.coding.any?{|x| x.system=='http://hl7.org/fhir/sid/cvx' && FHIR::Terminology.get_description('CVX',x.code)}
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.local_cvx_reference?(reference,record,contained)
|
49
|
+
if contained && reference.reference && reference.reference.start_with?('#')
|
50
|
+
contained.each do |resource|
|
51
|
+
return true if resource.is_a?(FHIR::Medication) && reference.reference[1..-1]==resource.id
|
52
|
+
end
|
53
|
+
end
|
54
|
+
record.entry.each do |entry|
|
55
|
+
if entry.resource.is_a?(FHIR::Medication) && reference_matchs?(reference,entry)
|
56
|
+
return true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.reference_matchs?(reference,entry)
|
63
|
+
if reference.reference.start_with?('urn:uuid:')
|
64
|
+
(reference.reference == entry.fullUrl)
|
65
|
+
elsif reference.reference.include?('Medication/')
|
66
|
+
(reference.reference.split('Medication/').last.split('/').first == entry.id)
|
67
|
+
else
|
68
|
+
false # unable to verify reference points to CVX
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module FHIR
|
2
|
+
class DateTimesIso8601 < FHIR::Rubrics
|
3
|
+
|
4
|
+
# Codes are present for all codes, Codings, and CodeableConcepts
|
5
|
+
rubric :iso8601_dates do |record|
|
6
|
+
results = {
|
7
|
+
:datetime_fields => 0,
|
8
|
+
:iso8601_fields => 0
|
9
|
+
}
|
10
|
+
resources = record.entry.map{|e|e.resource}
|
11
|
+
resources.each do |resource|
|
12
|
+
results.merge!(check_metadata(resource)){|k,a,b|a+b}
|
13
|
+
end
|
14
|
+
|
15
|
+
percentage = results[:iso8601_fields].to_f / results[:datetime_fields].to_f
|
16
|
+
percentage = 0.0 if percentage.nan?
|
17
|
+
points = 10.0 * percentage
|
18
|
+
message = "#{(100 * percentage).to_i}% (#{results[:iso8601_fields]}/#{results[:datetime_fields]}) of date/time/dateTime/instant fields were populated with reasonable iso8601 values. Maximum of 10 points."
|
19
|
+
response(points.to_i,message)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.check_metadata(fhir_model)
|
23
|
+
results = {
|
24
|
+
:datetime_fields => 0,
|
25
|
+
:iso8601_fields => 0
|
26
|
+
}
|
27
|
+
# check for metadata
|
28
|
+
# count all codeable fields
|
29
|
+
fhir_model.class::METADATA.each do |key, meta|
|
30
|
+
field_name = meta['local_name'] || key
|
31
|
+
if ['date','time','dateTime','instant'].include?(meta['type'])
|
32
|
+
results[:datetime_fields] += 1
|
33
|
+
value = fhir_model.instance_variable_get("@#{field_name}")
|
34
|
+
# check that the field is actually the correct type
|
35
|
+
if !value.nil?
|
36
|
+
if value.is_a?(Array)
|
37
|
+
if !value.empty? # the Array has at least one value
|
38
|
+
results[:iso8601_fields] += 1 if value.any?{|x|type_okay(meta['type'],x)}
|
39
|
+
end
|
40
|
+
else # not an Array
|
41
|
+
results[:iso8601_fields] += 1 if type_okay(meta['type'],value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
else
|
45
|
+
value = fhir_model.instance_variable_get("@#{field_name}")
|
46
|
+
if !value.nil?
|
47
|
+
if value.is_a?(Array)
|
48
|
+
value.each{|v| results.merge!(check_metadata(v)){|k,a,b|a+b} if v.is_a?(FHIR::Model)}
|
49
|
+
else # not an Array
|
50
|
+
results.merge!(check_metadata(value)){|k,a,b|a+b} if value.is_a?(FHIR::Model)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
results
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.type_okay(type,item)
|
59
|
+
okay = false
|
60
|
+
begin
|
61
|
+
time = Time.iso8601(item)
|
62
|
+
okay = true
|
63
|
+
if time.year
|
64
|
+
okay = (time.year > 1900) && (time.year < (Time.now + (10 * 365 * 24 * 60 * 60)).year)
|
65
|
+
end
|
66
|
+
rescue Exception => e
|
67
|
+
okay = false
|
68
|
+
end
|
69
|
+
okay
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module FHIR
|
2
|
+
class LabsLoinc < FHIR::Rubrics
|
3
|
+
|
4
|
+
# Lab Results should be coded with LOINC's top 2000 codes
|
5
|
+
rubric :loinc_labs do |record|
|
6
|
+
results = {
|
7
|
+
:eligible_fields => 0,
|
8
|
+
:validated_fields => 0
|
9
|
+
}
|
10
|
+
resources = record.entry.map{|e|e.resource}
|
11
|
+
resources.each do |resource|
|
12
|
+
if resource.is_a?(FHIR::DiagnosticReport) || resource.is_a?(FHIR::Observation)
|
13
|
+
results[:eligible_fields] += 1
|
14
|
+
if resource.code
|
15
|
+
results[:validated_fields] += 1 if resource.code.coding.any?{|x| x.system=='http://loinc.org' && FHIR::Terminology.is_top_lab_code?(x.code)}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
if resource.is_a?(FHIR::Observation)
|
19
|
+
if resource.component
|
20
|
+
resource.component.each do |comp|
|
21
|
+
results[:eligible_fields] += 1
|
22
|
+
if comp.code
|
23
|
+
results[:validated_fields] += 1 if comp.code.coding.any?{|x| x.system=='http://loinc.org' && FHIR::Terminology.is_top_lab_code?(x.code)}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
percentage = results[:validated_fields].to_f / results[:eligible_fields].to_f
|
31
|
+
percentage = 0.0 if percentage.nan?
|
32
|
+
points = 10.0 * percentage
|
33
|
+
message = "#{(100 * percentage).to_i}% (#{results[:validated_fields]}/#{results[:eligible_fields]}) of Observations and DiagnosticReports validated against the LOINC Top 2000. Maximum of 10 points."
|
34
|
+
response(points.to_i,message)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module FHIR
|
2
|
+
class QuantitiesUcum < FHIR::Rubrics
|
3
|
+
|
4
|
+
IGNORE = [
|
5
|
+
'TestScript','Task','StructureDefinition','SearchParameter','Questionnaire','QuestionnaireResponse','Parameters','OperationDefinition',
|
6
|
+
'Group','ExplanationOfBenefit','Contract','Conformance','Claim','ActivityDefinition'
|
7
|
+
]
|
8
|
+
|
9
|
+
CHECK = [
|
10
|
+
'VisionPrescription','SupplyDelivery','Substance','Specimen','Sequence','Observation','NutritionRequest',
|
11
|
+
'MedicationStatement','MedicationOrder','MedicationDispense','MedicationAdministration','Medication','Immunization','CarePlan'
|
12
|
+
]
|
13
|
+
|
14
|
+
# Physical quantities should use UCUM
|
15
|
+
rubric :ucum_quantities do |record|
|
16
|
+
results = {
|
17
|
+
:eligible_fields => 0,
|
18
|
+
:validated_fields => 0
|
19
|
+
}
|
20
|
+
resources = record.entry.map{|e|e.resource}
|
21
|
+
resources.each do |resource|
|
22
|
+
results.merge!(check_fields(resource)){|k,a,b|a+b} if CHECK.include?(resource.resourceType)
|
23
|
+
end
|
24
|
+
|
25
|
+
percentage = results[:validated_fields].to_f / results[:eligible_fields].to_f
|
26
|
+
percentage = 0.0 if percentage.nan?
|
27
|
+
points = 10.0 * percentage
|
28
|
+
message = "#{(100 * percentage).to_i}% (#{results[:validated_fields]}/#{results[:eligible_fields]}) of physical quantities used or declared UCUM. Maximum of 10 points."
|
29
|
+
response(points.to_i,message)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.check_fields(fhir_model)
|
33
|
+
results = {
|
34
|
+
:eligible_fields => 0,
|
35
|
+
:validated_fields => 0
|
36
|
+
}
|
37
|
+
# check each codeable field
|
38
|
+
fhir_model.class::METADATA.each do |key, meta|
|
39
|
+
field_name = meta['local_name'] || key
|
40
|
+
value = fhir_model.instance_variable_get("@#{field_name}")
|
41
|
+
|
42
|
+
if meta['type']=='Quantity'
|
43
|
+
if value.is_a?(Array)
|
44
|
+
value.each do |v|
|
45
|
+
results.merge!(check_quantity(v)){|k,a,b|a+b}
|
46
|
+
end
|
47
|
+
else
|
48
|
+
results.merge!(check_quantity(value)){|k,a,b|a+b}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
results
|
53
|
+
end
|
54
|
+
|
55
|
+
# This method checks whether or not the quantity uses UCUM.
|
56
|
+
def self.check_quantity(quantity)
|
57
|
+
result = {
|
58
|
+
:eligible_fields => 1,
|
59
|
+
:validated_fields => 0
|
60
|
+
}
|
61
|
+
if quantity && quantity.system && quantity.system.start_with?('http://unitsofmeasure.org')
|
62
|
+
result[:validated_fields] +=1
|
63
|
+
elsif quantity && quantity.unit
|
64
|
+
result[:validated_fields] +=1 if FHIR::Terminology.is_known_ucum?(quantity.unit)
|
65
|
+
end
|
66
|
+
result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module FHIR
|
2
|
+
class ReferencesResolve < FHIR::Rubrics
|
3
|
+
|
4
|
+
# All References should resolve to Resources within the Bundle.
|
5
|
+
rubric :references_resolve do |record|
|
6
|
+
results = {
|
7
|
+
:eligible_fields => 0,
|
8
|
+
:validated_fields => 0
|
9
|
+
}
|
10
|
+
|
11
|
+
resources = record.entry.map{|e|e.resource}
|
12
|
+
resources.each do |resource|
|
13
|
+
results.merge!(check_metadata(resource,record)){|k,a,b|a+b}
|
14
|
+
end
|
15
|
+
|
16
|
+
percentage = results[:validated_fields].to_f / results[:eligible_fields].to_f
|
17
|
+
percentage = 0.0 if percentage.nan?
|
18
|
+
points = 10.0 * percentage
|
19
|
+
message = "#{(100 * percentage).to_i}% (#{results[:validated_fields]}/#{results[:eligible_fields]}) of all possible References resolved locally within the Bundle. Maximum of 10 points."
|
20
|
+
response(points.to_i,message)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.check_metadata(fhir_model,record)
|
24
|
+
results = {
|
25
|
+
:eligible_fields => 0,
|
26
|
+
:validated_fields => 0
|
27
|
+
}
|
28
|
+
# check for metadata
|
29
|
+
# examine all References
|
30
|
+
fhir_model.class::METADATA.each do |key, meta|
|
31
|
+
field_name = meta['local_name'] || key
|
32
|
+
if meta['type']=='Reference'
|
33
|
+
results[:eligible_fields] += 1
|
34
|
+
value = fhir_model.instance_variable_get("@#{field_name}")
|
35
|
+
# check that the field is actually the correct type
|
36
|
+
if !value.nil?
|
37
|
+
if value.is_a?(Array)
|
38
|
+
if !value.empty? # the Array has at least one value
|
39
|
+
results[:eligible_fields] += (value.length-1)
|
40
|
+
value.each do |ref|
|
41
|
+
results[:validated_fields] += 1 if local_reference?(ref,record,fhir_model.contained)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
else # not an Array
|
45
|
+
results[:validated_fields] += 1 if local_reference?(value,record,fhir_model.contained)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
else
|
49
|
+
value = fhir_model.instance_variable_get("@#{field_name}")
|
50
|
+
if !value.nil?
|
51
|
+
if value.is_a?(Array)
|
52
|
+
value.each{|v| results.merge!(check_metadata(v,record)){|k,a,b|a+b} if v.is_a?(FHIR::Model)}
|
53
|
+
else # not an Array
|
54
|
+
results.merge!(check_metadata(value,record)){|k,a,b|a+b} if value.is_a?(FHIR::Model)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
results
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.local_reference?(reference,record,contained)
|
63
|
+
if contained && reference.reference && reference.reference.start_with?('#')
|
64
|
+
contained.each do |resource|
|
65
|
+
return true if reference.reference[1..-1]==resource.id
|
66
|
+
end
|
67
|
+
end
|
68
|
+
record.entry.each do |entry|
|
69
|
+
return true if reference_matchs?(reference,entry)
|
70
|
+
end
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.reference_matchs?(reference,entry)
|
75
|
+
return false if (reference.nil? || reference.reference.nil?)
|
76
|
+
if reference.reference.start_with?('urn:uuid:')
|
77
|
+
(reference.reference == entry.fullUrl)
|
78
|
+
else
|
79
|
+
# some weaknesses here:
|
80
|
+
# 'Patient/20' will match both 'http://foo/Patient/20/_history/1' and 'http://foo/Patient/20303/_history/1'
|
81
|
+
((entry.fullUrl =~ /#{reference.reference}/) || (entry.resource && entry.resource.id =~ /#{reference.reference}/ ))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|