fhir_scorecard 1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|