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.
@@ -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