diff --git a/.rubocop.yml b/.rubocop.yml index 6cdb1f12d..616f0c5dc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ AllCops: - 'spec/**/*' - 'lib/fhir_models/fhir/**/*' - 'lib/fhir_models/fluentpath/evaluate.rb' + - 'lib/**/*.rake' - 'tmp/**/*' - '*.gemspec' - 'bin/*' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fe82df15e..7628afd12 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -27,7 +27,7 @@ Metrics/BlockLength: # Offense count: 1 # Configuration parameters: CountBlocks. Metrics/BlockNesting: - Max: 4 + Max: 8 # Offense count: 5 # Configuration parameters: CountComments. @@ -41,7 +41,7 @@ Metrics/CyclomaticComplexity: # Offense count: 40 # Configuration parameters: CountComments. Metrics/MethodLength: - Max: 109 + Max: 150 # Offense count: 1 # Configuration parameters: CountComments. diff --git a/.travis.yml b/.travis.yml index 8e3e4ee66..cfb1608ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,10 @@ language: ruby env: - TESTMEMORY=0 GCDELAY=2.0 rvm: - - 2.2 - 2.3 - 2.4 - 2.5 + - 2.6 script: - bundle exec rake - bundle exec codeclimate-test-reporter diff --git a/fhir_models.gemspec b/fhir_models.gemspec index f483aa888..9755bc6b0 100644 --- a/fhir_models.gemspec +++ b/fhir_models.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'nokogiri', '>= 1.8.2' spec.add_dependency 'date_time_precision', '>= 0.8' spec.add_dependency 'bcp47', '>= 0.3' - spec.add_dependency 'mime-types', '>= 1.16', '< 3' + spec.add_dependency 'mime-types', '>= 3.0' spec.add_development_dependency 'bundler' spec.add_development_dependency 'rake' diff --git a/lib/fhir_models/bootstrap/hashable.rb b/lib/fhir_models/bootstrap/hashable.rb index 0ecc3042d..84171a145 100644 --- a/lib/fhir_models/bootstrap/hashable.rb +++ b/lib/fhir_models/bootstrap/hashable.rb @@ -51,7 +51,7 @@ def from_hash(hash) if !klass.nil? && !value.nil? # handle array of objects if value.is_a?(Array) - value.map! do |child| + value = value.map do |child| obj = child unless [FHIR::RESOURCES, FHIR::TYPES].flatten.include? child.class.name.gsub('FHIR::', '') obj = make_child(child, klass) @@ -68,7 +68,7 @@ def from_hash(hash) FHIR.logger.error("Unhandled and unrecognized class/type: #{meta['type']}") elsif value.is_a?(Array) # array of primitives - value.map! { |child| convert_primitive(child, meta) } + value = value.map { |child| convert_primitive(child, meta) } instance_variable_set("@#{local_name}", value) else # single primitive @@ -111,7 +111,7 @@ def convert_primitive(value, meta) elsif FHIR::PRIMITIVES.include?(meta['type']) primitive_meta = FHIR::PRIMITIVES[meta['type']] if primitive_meta['type'] == 'number' - rval = BigDecimal.new(value.to_s) + rval = BigDecimal(value.to_s) rval = rval.frac.zero? ? rval.to_i : rval.to_f end # primitive is number end # boolean else diff --git a/lib/fhir_models/bootstrap/model.rb b/lib/fhir_models/bootstrap/model.rb index 2c31be680..637ba2bc3 100644 --- a/lib/fhir_models/bootstrap/model.rb +++ b/lib/fhir_models/bootstrap/model.rb @@ -200,8 +200,8 @@ def validate_field(field, value, contained_here, meta, errors) else errors[field] << "#{meta['path']}: expected Reference, found #{klassname}" end - # if the data type is a particular resource or complex type - elsif FHIR::RESOURCES.include?(datatype) || FHIR::TYPES.include?(datatype) + # if the data type is a particular resource or complex type or BackBone element within this resource + elsif FHIR::RESOURCES.include?(datatype) || FHIR::TYPES.include?(datatype) || v.class.name.start_with?(self.class.name) if datatype == klassname validation = v.validate(contained_here) errors[field] << validation unless validation.empty? diff --git a/lib/fhir_models/examples/json/capabilitystatement-example.json b/lib/fhir_models/examples/json/capabilitystatement-example.json index 787c70c14..34eabd27c 100644 --- a/lib/fhir_models/examples/json/capabilitystatement-example.json +++ b/lib/fhir_models/examples/json/capabilitystatement-example.json @@ -216,7 +216,7 @@ "mode": "consumer", "documentation": "Basic rules for all documents in the EHR system", "profile": { - "reference": "http://fhir.hl7.org/base/Profilebc054d23-75e1-4dc6-aca5-838b6b1ac81d/_history/b5fdd9fc-b021-4ea1-911a-721a60663796" + "reference": "http://fhir.hl7.org/StructureDefinition/Profilebc054d23-75e1-4dc6-aca5-838b6b1ac81d/_history/b5fdd9fc-b021-4ea1-911a-721a60663796" } } ] diff --git a/lib/fhir_models/examples/json/claim-example-oral-contained-identifier.json b/lib/fhir_models/examples/json/claim-example-oral-contained-identifier.json index 8d1b50510..6b61d6c5e 100644 --- a/lib/fhir_models/examples/json/claim-example-oral-contained-identifier.json +++ b/lib/fhir_models/examples/json/claim-example-oral-contained-identifier.json @@ -109,7 +109,7 @@ "sequence": 1, "focal": true, "coverage": { - "reference": "http://www.jurisdiction.com/nationalplan/123AB345" + "reference": "http://www.jurisdiction.com/Coverage/123AB345" } } ], diff --git a/lib/fhir_models/examples/json/dataelement-example.json b/lib/fhir_models/examples/json/dataelement-example.json index b48c80803..e9280319a 100644 --- a/lib/fhir_models/examples/json/dataelement-example.json +++ b/lib/fhir_models/examples/json/dataelement-example.json @@ -186,7 +186,7 @@ "mapping": [ { "identity": "fhir", - "language": "application/xquery", + "language": "application/xml", "map": "return f:/Patient/f:gender" } ] diff --git a/lib/fhir_models/examples/json/endpoint-examples-general-template.json b/lib/fhir_models/examples/json/endpoint-examples-general-template.json index 092536cf4..f5c117a72 100644 --- a/lib/fhir_models/examples/json/endpoint-examples-general-template.json +++ b/lib/fhir_models/examples/json/endpoint-examples-general-template.json @@ -199,7 +199,7 @@ } ], "payloadMimeType": [ - "PDF" + "application/pdf" ], "address": "https://sqlonfhir-dstu2.azurewebsites.net/fhir" } @@ -232,7 +232,7 @@ } ], "payloadMimeType": [ - "HL7v2" + "application/fhir+json" ], "address": "127.0.0.1" } @@ -265,7 +265,7 @@ } ], "payloadMimeType": [ - "XDA/XDS" + "application/xml" ], "address": "https://open.epic.com/Interface/XDS.b" } @@ -298,7 +298,7 @@ } ], "payloadMimeType": [ - "DICOM WADO-RS" + "application/dicom" ], "address": "https://pacs.hospital.org/dicomweb" } @@ -331,7 +331,7 @@ } ], "payloadMimeType": [ - "DICOM QIDO-RS" + "application/dicom" ], "address": "https://pacs.hospital.org/dicomweb" } @@ -364,7 +364,7 @@ } ], "payloadMimeType": [ - "DICOM STOW-RS" + "application/dicom" ], "address": "https://pacs.hospital.org/dicomweb" } @@ -397,7 +397,7 @@ } ], "payloadMimeType": [ - "DICOM STOW-RS" + "application/dicom" ], "address": "https://pacs.hospital.org/dicomweb" } @@ -430,7 +430,7 @@ } ], "payloadMimeType": [ - "DICOM WADO-URI" + "application/dicom" ], "address": "https://pacs.hospital.org/wadoUri" } @@ -463,7 +463,7 @@ } ], "payloadMimeType": [ - "IHE IID" + "application/dicom" ], "address": "https://pacs.hospital.org/IHEInvokeImageDisplay" } diff --git a/lib/fhir_models/examples/json/provenance-example-sig.json b/lib/fhir_models/examples/json/provenance-example-sig.json index 6bd54cd71..2fcee86e2 100644 --- a/lib/fhir_models/examples/json/provenance-example-sig.json +++ b/lib/fhir_models/examples/json/provenance-example-sig.json @@ -35,7 +35,7 @@ ] } ], - "whoUri": "mailto://hhd@ssa.gov" + "whoUri": "mailto:hhd@ssa.gov" } ], "signature": [ diff --git a/lib/fhir_models/examples/xml/capabilitystatement-example.xml b/lib/fhir_models/examples/xml/capabilitystatement-example.xml index 189adb180..88b7b7704 100644 --- a/lib/fhir_models/examples/xml/capabilitystatement-example.xml +++ b/lib/fhir_models/examples/xml/capabilitystatement-example.xml @@ -211,7 +211,7 @@ - + \ No newline at end of file diff --git a/lib/fhir_models/examples/xml/claim-example-oral-contained-identifier.xml b/lib/fhir_models/examples/xml/claim-example-oral-contained-identifier.xml index 917475e29..516acbcd4 100644 --- a/lib/fhir_models/examples/xml/claim-example-oral-contained-identifier.xml +++ b/lib/fhir_models/examples/xml/claim-example-oral-contained-identifier.xml @@ -99,7 +99,7 @@ - + diff --git a/lib/fhir_models/examples/xml/dataelement-example.xml b/lib/fhir_models/examples/xml/dataelement-example.xml index d98d2bb19..ad7aa65c0 100644 --- a/lib/fhir_models/examples/xml/dataelement-example.xml +++ b/lib/fhir_models/examples/xml/dataelement-example.xml @@ -254,7 +254,7 @@ - + diff --git a/lib/fhir_models/examples/xml/endpoint-examples-general-template.xml b/lib/fhir_models/examples/xml/endpoint-examples-general-template.xml index 97e55d9f1..cbce7b24d 100644 --- a/lib/fhir_models/examples/xml/endpoint-examples-general-template.xml +++ b/lib/fhir_models/examples/xml/endpoint-examples-general-template.xml @@ -209,7 +209,7 @@ - +
@@ -244,7 +244,7 @@ - +
@@ -279,7 +279,7 @@ - +
@@ -314,7 +314,7 @@ - +
@@ -349,7 +349,7 @@ - +
@@ -384,7 +384,7 @@ - +
@@ -419,7 +419,7 @@ - +
@@ -454,7 +454,7 @@ - +
@@ -489,7 +489,7 @@ - +
diff --git a/lib/fhir_models/examples/xml/provenance-example-sig.xml b/lib/fhir_models/examples/xml/provenance-example-sig.xml index e41bcf9d5..e7a870067 100644 --- a/lib/fhir_models/examples/xml/provenance-example-sig.xml +++ b/lib/fhir_models/examples/xml/provenance-example-sig.xml @@ -36,7 +36,7 @@ - + diff --git a/lib/fhir_models/fhir_ext/structure_definition.rb b/lib/fhir_models/fhir_ext/structure_definition.rb index bbf3ac3e4..138af6ecc 100644 --- a/lib/fhir_models/fhir_ext/structure_definition.rb +++ b/lib/fhir_models/fhir_ext/structure_definition.rb @@ -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 @@ -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 @@ -169,7 +184,7 @@ def verify_element(element, json) temp_messages += @errors @errors = temp end - if verified_extension || verified_data_type + 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? @@ -183,11 +198,45 @@ def verify_element(element, json) 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 + + 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 + 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 + 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 diff --git a/lib/fhir_models/tasks/tasks.rake b/lib/fhir_models/tasks/tasks.rake index cd423807e..ec535382c 100644 --- a/lib/fhir_models/tasks/tasks.rake +++ b/lib/fhir_models/tasks/tasks.rake @@ -38,6 +38,54 @@ namespace :fhir do end end + desc 'shrink implementation guide' + task :shrinkig, [:file_path] do |_t, args| + file_path = args[:file_path] + files = Dir.glob(File.join(file_path, '*.json')) + files.each do |filename| + # Load each file + start = File.size(filename) + json = File.open(filename, 'r:UTF-8', &:read) + hash = JSON.parse(json) + + # process each file + FHIR::Boot::Preprocess.pre_process_structuredefinition(hash) if 'StructureDefinition' == hash['resourceType'] + FHIR::Boot::Preprocess.pre_process_valueset(hash) if 'ValueSet' == hash['resourceType'] + FHIR::Boot::Preprocess.pre_process_codesystem(hash) if 'CodeSystem' == hash['resourceType'] + FHIR::Boot::Preprocess.pre_process_searchparam(hash) if 'SearchParameter' == hash['resourceType'] + FHIR::Boot::Preprocess.remove_fhir_comments(hash) + + # if BlueButton, fix URLs + if 'StructureDefinition' == hash['resourceType'] + # hash['url'].gsub!('http://hl7.org/fhir/StructureDefinition/','https://bluebutton.cms.gov/assets/ig/StructureDefinition-') + fix_blue_button_urls(hash) + end + + # Output the post processed file + f = File.open(filename, 'w:UTF-8') + f.write(JSON.pretty_unparse(hash)) + f.close + finish = File.size(filename) + puts " Removed #{(start - finish) / 1024} KB" if start != finish + end + end + + def self.fix_blue_button_urls(hash) + hash.each do |key, value| + if value.is_a?(String) + if value.start_with?('http://hl7.org/fhir/StructureDefinition/bluebutton') + hash[key] = value.gsub('http://hl7.org/fhir/StructureDefinition/bluebutton', 'https://bluebutton.cms.gov/assets/ig/StructureDefinition-bluebutton') + end + elsif value.is_a?(Hash) + fix_blue_button_urls(value) + elsif value.is_a?(Array) + value.each do |v| + fix_blue_button_urls(v) if v.is_a?(Hash) + end + end + end + end + desc 'copy artifacts from FHIR build' task :update, [:fhir_build_path] do |_t, args| fhir_build_path = args[:fhir_build_path] diff --git a/test/unit/json_validation_test.rb b/test/unit/json_validation_test.rb index 8842d4556..dbecc6023 100644 --- a/test/unit/json_validation_test.rb +++ b/test/unit/json_validation_test.rb @@ -27,7 +27,7 @@ def run_json_validation_test(example_file, example_name) File.open("#{ERROR_DIR}/#{example_name}.err", 'w:UTF-8') { |file| file.write(JSON.pretty_unparse(errors)) } File.open("#{ERROR_DIR}/#{example_name}.json", 'w:UTF-8') { |file| file.write(input_json) } end - assert errors.empty?, 'Resource failed to validate.' + assert errors.empty?, "Resource failed to validate: #{errors}" # check memory before = check_memory resource = nil diff --git a/test/unit/profile_validation_test.rb b/test/unit/profile_validation_test.rb index 7727905f9..7378d91fe 100644 --- a/test/unit/profile_validation_test.rb +++ b/test/unit/profile_validation_test.rb @@ -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)