fhir_scorecard 1.0

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