Skip to content

Commit

Permalink
add valueset and code system validation with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
radamson committed Feb 14, 2019
1 parent 2bede9d commit 6c08227
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 15 deletions.
79 changes: 64 additions & 15 deletions lib/fhir_models/fhir_ext/structure_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ class StructureDefinition
# Profile Validation
# -------------------------------------------------------------------------

class << self; attr_accessor :vs_validators end
@vs_validators = {}
def self.validates_vs(valueset_uri, &validator_fn)
@vs_validators[valueset_uri] = validator_fn
end

def self.clear_validates_vs(valueset_uri)
@vs_validators.delete valueset_uri
end

def self.clear_all_validates_vs
@vs_validators = {}
end

def validates_resource?(resource)
validate_resource(resource).empty?
end
Expand Down Expand Up @@ -147,6 +161,7 @@ def verify_element(element, json)
if !element.type.empty? && element.path != id
# element.type not being empty implies data_type_found != nil, for valid profiles
codeable_concept_pattern = element.pattern && element.pattern.is_a?(FHIR::CodeableConcept)
codeable_concept_binding = element.binding
matching_pattern = false
nodes.each do |value|
matching_type = 0
Expand All @@ -169,26 +184,60 @@ def verify_element(element, json)
temp_messages += @errors
@errors = temp
end
if verified_extension || verified_data_type
matching_type += 1
if data_type_found == 'code' # then check the binding
unless element.binding.nil?
matching_type += check_binding_element(element, value)
if data_type_found && (verified_extension || verified_data_type)
matching_type += 1
if data_type_found == 'code' # then check the binding
unless element.binding.nil?
matching_type += check_binding_element(element, value)
end
elsif data_type_found == 'CodeableConcept' && codeable_concept_pattern
vcc = FHIR::CodeableConcept.new(value)
pattern = element.pattern.coding
pattern.each do |pcoding|
vcc.coding.each do |vcoding|
matching_pattern = true if vcoding.system == pcoding.system && vcoding.code == pcoding.code
end
end
elsif data_type_found == 'CodeableConcept' && codeable_concept_binding
binding_issues =
if element.binding.strength == 'extensible'
@warnings
elsif element.binding.strength == 'required'
@errors
else # e.g., example-strength or unspecified
[] # Drop issues errors on the floor, in throwaway array
end
elsif data_type_found == 'CodeableConcept' && codeable_concept_pattern
vcc = FHIR::CodeableConcept.new(value)
pattern = element.pattern.coding
pattern.each do |pcoding|
vcc.coding.each do |vcoding|
matching_pattern = true if vcoding.system == pcoding.system && vcoding.code == pcoding.code

valueset_uri = element.binding && element.binding.valueSetReference && element.binding.valueSetReference.reference
vcc = FHIR::CodeableConcept.new(value)
if valueset_uri && self.class.vs_validators[valueset_uri]
check_fn = self.class.vs_validators[valueset_uri]
has_valid_code = vcc.coding && vcc.coding.any? { |c| check_fn.call(c) }
unless has_valid_code
binding_issues << "#{describe_element(element)} has no codings from #{valueset_uri}. Codings evaluated: #{vcc.to_json}"
end
end

unless has_valid_code
vcc.coding.each do |c|
check_fn = self.class.vs_validators[c.system]
if check_fn && !check_fn.call(c)
binding_issues << "#{describe_element(element)} has no codings from it's specified system: #{c.system}. "\
"Codings evaluated: #{vcc.to_json}"
end
end
elsif data_type_found == 'String' && !element.maxLength.nil? && (value.size > element.maxLength)
@errors << "#{describe_element(element)} exceed maximum length of #{element.maxLength}: #{value}"
end
else
temp_messages << "#{describe_element(element)} is not a valid #{data_type_found}: '#{value}'"

elsif data_type_found == 'String' && !element.maxLength.nil? && (value.size > element.maxLength)
@errors << "#{describe_element(element)} exceed maximum length of #{element.maxLength}: #{value}"
end
elsif data_type_found
temp_messages << "#{describe_element(element)} is not a valid #{data_type_found}: '#{value}'"
else
# we don't know the data type... so we say "OK"
matching_type += 1
@warnings >> "Unable to guess data type for #{describe_element(element)}"
end

if matching_type <= 0
@errors += temp_messages
Expand Down
60 changes: 60 additions & 0 deletions test/unit/profile_validation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,66 @@ def test_profile_validation
assert_memory(before, after)
end

def validate_vital_sign_resource
example_name = 'sample-us-core-record.json'
patient_record = File.join(FIXTURES_DIR, example_name)
input_json = File.read(patient_record)
bundle = FHIR::Json.from_json(input_json)

vitalsign = bundle.entry.find do |entry|
entry.resource.meta and (entry.resource.meta.profile.first == 'http://hl7.org/fhir/StructureDefinition/vitalsigns')
end
assert vitalsign, 'Unable to find vital sign Observation resource'
profile = PROFILES['http://hl7.org/fhir/StructureDefinition/vitalsigns']
assert profile, 'Failed to find http://hl7.org/fhir/StructureDefinition/vitalsigns profile'
profile.validate_resource(vitalsign.resource)
profile
end

def test_profile_code_system_check
# Clear any registered validators
FHIR::StructureDefinition.clear_all_validates_vs
FHIR::StructureDefinition.validates_vs "http://hl7.org/fhir/ValueSet/observation-vitalsignresult" do |coding|
false # fails so that the code system validation happens
end
FHIR::StructureDefinition.validates_vs "http://loinc.org" do |coding|
false # errors related to code system validation should be present
end
profile = validate_vital_sign_resource
assert profile.errors.empty?, 'Expected no errors.'
assert profile.warnings.detect{|x| x.start_with?('Observation.code has no codings from http://hl7.org/fhir/ValueSet/observation-vitalsignresult')}
assert profile.warnings.detect{|x| x.start_with?("Observation.code has no codings from it's specified system: http://loinc.org")}
# check memory
before = check_memory
resource = nil
profile = nil
wait_for_gc
after = check_memory
assert_memory(before, after)
end

def test_profile_valueset_check
# Clear any registered validators
FHIR::StructureDefinition.clear_all_validates_vs
FHIR::StructureDefinition.validates_vs "http://hl7.org/fhir/ValueSet/observation-vitalsignresult" do |coding|
true # fails so that the code system validation never happens
end
FHIR::StructureDefinition.validates_vs "http://loinc.org" do |coding|
false # no errors related to code system should be present
end
profile = validate_vital_sign_resource
assert profile.errors.empty?, 'Expected no errors.'
assert !profile.warnings.detect{|x| x.start_with?('Observation.code has no codings from http://hl7.org/fhir/ValueSet/observation-vitalsignresult')}
assert !profile.warnings.detect{|x| x.start_with?("Observation.code has no codings from it's specified system: http://loinc.org")}
# check memory
before = check_memory
resource = nil
profile = nil
wait_for_gc
after = check_memory
assert_memory(before, after)
end

def test_invalid_profile_validation
example_name = 'invalid-us-core-record.json'
patient_record = File.join(FIXTURES_DIR, example_name)
Expand Down

0 comments on commit 6c08227

Please sign in to comment.