From 58446f2256837ba1a2c207204695de41f957d167 Mon Sep 17 00:00:00 2001 From: Jaehoon Lee Date: Fri, 15 Nov 2024 14:35:21 -0500 Subject: [PATCH] Rubocop temporarily addressed --- lib/inferno/dsl/fhir_evaluator/config.rb | 38 ++-- .../dsl/fhir_evaluator/data_summary.rb | 20 +- .../dsl/fhir_evaluator/evaluator_util.rb | 40 ++-- .../rules/abstract_has_examples_rule.rb | 27 --- .../all_defined_extensions_have_examples.rb | 44 ---- .../rules/all_extensions_used.rb | 71 ------ .../rules/all_mustsupports_represent.rb | 209 ------------------ .../rules/all_profiles_have_examples.rb | 68 ------ .../rules/all_references_resolve.rb | 53 ----- .../rules/all_resources_reachable.rb | 64 ------ .../all_search_parameters_have_examples.rb | 86 ------- .../differential_content_has_examples.rb | 118 ---------- .../rules/valuesets_demonstrate.rb | 191 ---------------- 13 files changed, 49 insertions(+), 980 deletions(-) delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/abstract_has_examples_rule.rb delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/all_defined_extensions_have_examples.rb delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/all_extensions_used.rb delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/all_mustsupports_represent.rb delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/all_profiles_have_examples.rb delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/all_references_resolve.rb delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/all_resources_reachable.rb delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/all_search_parameters_have_examples.rb delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/differential_content_has_examples.rb delete mode 100644 lib/inferno/dsl/fhir_evaluator/rules/valuesets_demonstrate.rb diff --git a/lib/inferno/dsl/fhir_evaluator/config.rb b/lib/inferno/dsl/fhir_evaluator/config.rb index b10dec96e..0a64cea45 100644 --- a/lib/inferno/dsl/fhir_evaluator/config.rb +++ b/lib/inferno/dsl/fhir_evaluator/config.rb @@ -4,28 +4,28 @@ class Config DEFAULT_FILE = File.join(EVALUATOR_HOME, 'config', 'default.yml') attr_accessor :data - def initialize(config_file = nil) - @data = if config_file.nil? - YAML.load_file(File.absolute_path(DEFAULT_FILE)) - else - YAML.load_file(File.absolute_path(config_file)) - end + # def initialize(config_file = nil) + # @data = if config_file.nil? + # YAML.load_file(File.absolute_path(DEFAULT_FILE)) + # else + # YAML.load_file(File.absolute_path(config_file)) + # end - raise(TypeError, 'Malformed configuration') unless @data.is_a?(Hash) + # raise(TypeError, 'Malformed configuration') unless @data.is_a?(Hash) - def method_missing(name, *args, &) - section = @data[name.to_s] - if section - Section.new(section) - else - super - end - end + # def method_missing(name, *args, &) + # section = @data[name.to_s] + # if section + # Section.new(section) + # else + # super + # end + # end - def respond_to_missing?(name, include_private = false) - @data.key?(name.to_s) || super - end - end + # def respond_to_missing?(name, include_private = false) + # @data.key?(name.to_s) || super + # end + # end class Section def initialize(details) diff --git a/lib/inferno/dsl/fhir_evaluator/data_summary.rb b/lib/inferno/dsl/fhir_evaluator/data_summary.rb index 66248c21a..772ab0421 100644 --- a/lib/inferno/dsl/fhir_evaluator/data_summary.rb +++ b/lib/inferno/dsl/fhir_evaluator/data_summary.rb @@ -33,18 +33,18 @@ def validate(data) end def summarize(data) - @root_resource_ids = data.map { |r| { type: r.resourceType, id: r.id } } - @root_bundle_resource_ids = data.map { |r| { type: r.resourceType, id: r.id } if r.resourceType == 'Bundle' } + # @root_resource_ids = data.map { |r| { type: r.resourceType, id: r.id } } + # @root_bundle_resource_ids = data.map { |r| { type: r.resourceType, id: r.id } if r.resourceType == 'Bundle' } - id_hash = Hash.new { |hash, key| hash[key] = [] } - data.map { |e| resources(e) }.flatten.each do |item| - id_hash[item[:type]] << item[:id] - end - @domain_resource_ids = id_hash.to_a + # id_hash = Hash.new { |hash, key| hash[key] = [] } + # data.map { |e| resources(e) }.flatten.each do |item| + # id_hash[item[:type]] << item[:id] + # end + # @domain_resource_ids = id_hash.to_a - @resource_profile_map = data.map { |e| resources_profiles(e) }.flatten.uniq - @resource_patient_map = data.map { |e| resources_patients(e) }.flatten.uniq - @resource_subject_map = data.map { |e| resources_subjects(e) }.flatten.uniq + # @resource_profile_map = data.map { |e| resources_profiles(e) }.flatten.uniq + # @resource_patient_map = data.map { |e| resources_patients(e) }.flatten.uniq + # @resource_subject_map = data.map { |e| resources_subjects(e) }.flatten.uniq end def resources_ids(resource) diff --git a/lib/inferno/dsl/fhir_evaluator/evaluator_util.rb b/lib/inferno/dsl/fhir_evaluator/evaluator_util.rb index 12859356a..f661a2c3a 100644 --- a/lib/inferno/dsl/fhir_evaluator/evaluator_util.rb +++ b/lib/inferno/dsl/fhir_evaluator/evaluator_util.rb @@ -79,26 +79,26 @@ def extract_ids_references(resources) end def extract_references(resources) - resources.each do |resource| - resource.each_element do |value, metadata, path| - if metadata['type'] == 'Reference' && !value.reference.nil? - if value.reference.start_with?('#') - # skip contained references (not separate resources) - next - elsif value.reference.include? '/' - add_parsed_reference(resource, value, path) - # if type is not specified in the reference, get type from the - elsif path.include? 'Reference' - add_reference_typed_path(resource, value, path) - else - # assumes this is a unique uuid - reference = value.reference - reference = reference[9..] if reference.start_with? 'urn:uuid:' - @references[resource.id] << [path, '', reference] - end - end - end - end + # resources.each do |resource| + # resource.each_element do |value, metadata, path| + # if metadata['type'] == 'Reference' && !value.reference.nil? + # if value.reference.start_with?('#') + # # skip contained references (not separate resources) + # next + # elsif value.reference.include? '/' + # add_parsed_reference(resource, value, path) + # # if type is not specified in the reference, get type from the + # elsif path.include? 'Reference' + # add_reference_typed_path(resource, value, path) + # else + # # assumes this is a unique uuid + # reference = value.reference + # reference = reference[9..] if reference.start_with? 'urn:uuid:' + # @references[resource.id] << [path, '', reference] + # end + # end + # end + # end end def add_parsed_reference(resource, value, path) diff --git a/lib/inferno/dsl/fhir_evaluator/rules/abstract_has_examples_rule.rb b/lib/inferno/dsl/fhir_evaluator/rules/abstract_has_examples_rule.rb deleted file mode 100644 index 32709fe23..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/abstract_has_examples_rule.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require_relative '../evaluator_util' - -module FhirEvaluator - module Rules - class HasExamples < Rule - def check(_context) - '' - end - - def unused_resource_urls - @unused_resource_urls ||= [] - end - - def used_resources - @used_resources ||= [] - end - - def get_unused_resource_urls(ig_data, &resource_filter) - ig_data.each do |resource| - unused_resource_urls.push resource.url unless resource_filter.call(resource) - end - end - end - end -end diff --git a/lib/inferno/dsl/fhir_evaluator/rules/all_defined_extensions_have_examples.rb b/lib/inferno/dsl/fhir_evaluator/rules/all_defined_extensions_have_examples.rb deleted file mode 100644 index 82e3e4b5c..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/all_defined_extensions_have_examples.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module FhirEvaluator - module Rules - class AllIGExtensionsHaveExamples < HasExamples - def check(context) - @used_resources = context.data.map { |e| extension_urls(e) }.flatten.uniq - get_unused_resource_urls(context.ig.extensions) do |extension| - next true if extension.context.any? do |ctx| - # Skip extensions that are defined for definitional artifacts. - # For example, US Core's uscdi-requirement extension is applied to - # the profiles and extensions of the IG, not data that conforms to the IG. - # There may eventually be cases where SD/ED are data, so this may become configurable. - ctx.expression == 'StructureDefinition' || ctx.expression == 'ElementDefinition' - end - - versioned_url = "#{extension.url}|#{extension.version}" - used_resources.include?(extension.url) || used_resources.include?(versioned_url) - end - - if unused_resource_urls.any? - message = "Found unused extensions defined in the IG: #{unused_resource_urls.join(', ')}" - result = EvaluationResult.new(message, rule: self) - else - message = 'All defined extensions are represented in examples' - result = EvaluationResult.new(message, severity: 'success', rule: self) - end - - context.add_result result - end - - def extension_urls(resource) - urls = [] - resource.each_element do |value, _metadata, path| - path_elements = path.split('.') - next unless path_elements.length > 1 - - urls << value if path_elements[-2].include?('extension') && path_elements[-1] == 'url' - end - urls.uniq - end - end - end -end diff --git a/lib/inferno/dsl/fhir_evaluator/rules/all_extensions_used.rb b/lib/inferno/dsl/fhir_evaluator/rules/all_extensions_used.rb deleted file mode 100644 index b285ebf89..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/all_extensions_used.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require_relative '../evaluator_util' - -module FhirEvaluator - module Rules - class AllExtensionsUsed < Rule - def check(context) - # all_extensions and unused_extensions are hashes { profile_url: [extension_url,...]} - all_extensions = collect_profile_extensions(context.ig.profiles) - unused_extensions = remove_found_resource_extensions(all_extensions, context.data) - - if unused_extensions.any? { |_profile, extensions| !extensions.empty? } - message = gen_extension_fail_message(unused_extensions) - result = EvaluationResult.new(message, rule: self) - else - message = 'All extensions specified in profiles are represented in instances' - result = EvaluationResult.new(message, severity: 'success', rule: self) - end - - context.add_result result - end - - def collect_profile_extensions(profiles) - extensions = Hash.new { |h, k| h[k] = Set.new } - profiles.each do |profile| - profile.each_element do |value, metadata, _path| - next unless metadata['type'] == 'ElementDefinition' - - path_end = value.id.split('.')[-1] - next unless path_end.include?('extension') - - value.type.each do |t| - t.profile.each do |p| - extensions[profile.url].add(p) - end - end - end - end - extensions - end - - def remove_found_resource_extensions(extensions, examples) - unused_extensions = extensions.dup - examples.each do |resource| - resource.each_element do |value, _metadata, path| - path_elements = path.split('.') - next unless path_elements.length > 1 - - next unless path_elements[-2].include?('extension') && path_elements[-1] == 'url' - - profiles = Util.get_meta_profile(resource) - profiles.each do |p| - unused_extensions[p].delete(value) if extensions.key?(p) - end - end - end - unused_extensions - end - - def gen_extension_fail_message(extensions) - "Found extensions specified in profiles, but not used in instances: #{ - extensions.map do |k, v| - next if v.empty? - - "\n Profile: #{k}, \n\tExtensions: #{v.join(', ')}" - end.compact.join(',')}" - end - end - end -end diff --git a/lib/inferno/dsl/fhir_evaluator/rules/all_mustsupports_represent.rb b/lib/inferno/dsl/fhir_evaluator/rules/all_mustsupports_represent.rb deleted file mode 100644 index 99e241071..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/all_mustsupports_represent.rb +++ /dev/null @@ -1,209 +0,0 @@ -# frozen_string_literal: true - -module FhirEvaluator - module Rules - class AllMustSupportPresent < HasExamples - attr_accessor :config - - def check(context) - @config = context.config - unrepresented_elements = evaluate_elements(context) - unrepresented_extensions = evaluate_extensions(context) - - if (unrepresented_elements.count + unrepresented_extensions.count).zero? - result = EvaluationResult.new('All MustSupports represented', severity: 'success', rule: self) - else - message = 'Found Profiles with not all MustSupports represented: ' - result = EvaluationResult.new(message, rule: self) - if unrepresented_elements.count.positive? - result.message += "\n\tMustSupport elements not presented: " - unrepresented_elements.each { |r| result.message += "\n\t\t#{r}" } - end - if unrepresented_extensions.count.positive? - result.message += "\n\tMustSupport extensions not presented: " - unrepresented_extensions.each { |r| result.message += "\n\t\t#{r}" } - end - end - - context.add_result result - end - - def evaluate_elements(context) - unrepresented_profiles = [] - context.ig.profiles.each do |structure_definition| - ms_element_paths = get_mustsupport_elements(structure_definition) - ms_element_paths.uniq! - - resource_elements = [] - context.data.each do |resource| - ######################################################## - # Evaluate if a MS element is represented in examples with same profile URL with structure definition - ######################################################## - if @config.Rule.AllMustSupportPresent.ExampleSelection.byMetaProfile - versioned_url = "#{structure_definition.url}|#{structure_definition.version}" - if Util.get_meta_profile(resource).include?(structure_definition.url) || Util.get_meta_profile(resource).include?(versioned_url) - resource_elements += get_resource_paths(resource.to_hash) - end - end - - next unless @config.Rule.AllMustSupportPresent.ExampleSelection.byConformance - - if structure_definition.type == resource.resourceType && structure_definition.validates_resource?(resource) - resource_elements += get_resource_paths(resource.to_hash) - end - end - resource_elements.uniq! - - unrepresented_cnt = 0 - ms_element_paths.each do |path| - unrepresented_cnt += 1 unless resource_elements.include?(path) - end - - if unrepresented_cnt.positive? - unrepresented_profiles << "#{structure_definition.name} (# represented MS elements: #{ms_element_paths.count - unrepresented_cnt} out of #{ms_element_paths.count})" - end - end - unrepresented_profiles - end - - def evaluate_extensions(context) - unpresented_profiles = [] - context.ig.profiles.each do |structure_definition| - ms_represent_cnt = 0 - resource_paths = [] - versioned_url = "#{structure_definition.url}|#{structure_definition.version}" - - context.data.each do |resource| - unless Util.get_meta_profile(resource).include?(structure_definition.url) || Util.get_meta_profile(resource).include?(versioned_url) - next - end - - resource_paths += get_resource_paths_values(resource.to_hash['resourceType'], - resource.to_hash).select do |path| - path[:path].include?('extension') - end - end - - resource_paths.uniq! - - extensions = get_mustsupport_extensions(structure_definition) - extensions.each do |extension| - value_exist_flg = false - - resource_paths.each do |path| - resource_path = path[:path] - ## Count if a path a Resource is extension, contains url, and urls match. - if resource_path[extension[:path]] && resource_path['url'] && extension[:url] == path[:value] - value_exist_flg = true - end - - ## Count if a path of a Resource is extension, contains sliceName, and has (any) value. - if resource_path[extension[:path]] && extension[:sliceName] && resource_path['value'] - value_exist_flg = true - end - - ## Count if a path of a Resource is extension, contains slicing, and has (any) value. - value_exist_flg = true if resource_path[extension[:path]] && extension[:slicing] && resource_path['value'] - end - - ms_represent_cnt += 1 if value_exist_flg - end - - if extensions.count != ms_represent_cnt - unpresented_profiles << "#{structure_definition.name} (# represented MS extensions: #{ms_represent_cnt} out of #{extensions.count})" - end - end - unpresented_profiles - end - - def get_resource_paths_values(resource_type, resource, current_path = '', result = []) - if resource.is_a?(Array) - resource.each_with_index do |element, index| - get_resource_paths_values(resource_type, element, "#{current_path}[#{index}]", result) - end - elsif resource.is_a?(Hash) - resource.each do |key, value| - new_path = current_path.empty? ? key : "#{current_path}.#{key}" - if value.is_a?(Array) || value.is_a?(Hash) - get_resource_paths_values(resource_type, value, new_path, result) - else - result << { path: remove_fhir_datatype("#{resource_type}.#{new_path.gsub(/\[\d+\]/, '')}"), value: } - end - end - else - result << { path: remove_fhir_datatype("#{resource_type}.#{current_path.gsub(/\[\d+\]/, '')}"), - value: resource } - end - result - end - - def remove_fhir_datatype(key) - datatypes = ['CodeableConcept', 'String', 'Quantity', 'Boolean', 'Integer', 'Range', 'Ratio', 'SampleData', - 'DateTime', 'Period', 'Time'] - datatypes.each { |dt| key = key.gsub(dt, '') } - key - end - - def get_resource_paths(hash, parent_key = '') - result = [] - hash.each do |key, value| - current_key = parent_key.empty? ? key : "#{parent_key}.#{key}" - result << remove_fhir_datatype(current_key) - result << get_resource_paths(value, current_key) if value.is_a?(Hash) - result << get_resource_paths_list(value, current_key) if value.is_a?(Array) - end - result.flatten.uniq - end - - def get_resource_paths_list(list, parent_key) - result = [] - list.each do |value| - # NOTE: that we re-use the same parent key here, we don't include the index - result << remove_fhir_datatype(parent_key) - result << get_resource_paths(value, parent_key) if value.is_a?(Hash) - result << get_resource_paths_list(value, parent_key) if value.is_a?(Array) - end - result.flatten.uniq - end - - def get_mustsupport_elements(structure_definition) - mustsupport = [] - structure_definition.snapshot.element.each do |element| - next unless element.mustSupport - next if element.path['extension'] - - #################################### - # For now the evaluator only checks whether an element has value or not, by that an element with slice is evaluated the same way - # Will come back if we decide to validate data for evaluating representation of MS elements. - #################################### - - mustsupport << element.path.sub("#{element.path.split('.')[0]}.", '').gsub('[x]', '') - end - mustsupport - end - - def get_mustsupport_extensions(structure_definition) - mustsupport_extensions = [] - structure_definition.snapshot.element.each do |element| - next unless element.mustSupport - - if element.base.path == 'DomainResource.extension' - element.type.each do |type| - type.profile&.each do |profile| - mustsupport_extensions << { path: element.path, url: profile } - end - end - end - - next unless element.base.path == 'Element.extension' - - mustsupport_extensions << { path: element.path, sliceName: element.sliceName } if element.sliceName - element.slicing&.discriminator&.each do |dm| - mustsupport_extensions << { path: element.path, slicing: dm.path } - end - end - mustsupport_extensions.uniq - end - end - end -end diff --git a/lib/inferno/dsl/fhir_evaluator/rules/all_profiles_have_examples.rb b/lib/inferno/dsl/fhir_evaluator/rules/all_profiles_have_examples.rb deleted file mode 100644 index b3ba75d9e..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/all_profiles_have_examples.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require_relative '../evaluator_util' - -module FhirEvaluator - module Rules - class AllProfilesHaveExamples < HasExamples - attr_accessor :config - - def check(context) - # TODO: rewrite this to use data.summary - @used_resources = [] - @config = context.config - - if config.Rule.AllProfilesHaveExamples.TargetExample.byMetaProfile - @used_resources = context.data.map { |e| profiles(e) }.flatten.uniq - profile_is_used = proc do |profile| - versioned_url = "#{profile.url}|#{profile.version}" - used_resources.include?(profile.url) || used_resources.include?(versioned_url) - end - get_unused_resource_urls(context.ig.profiles, &profile_is_used) - elsif config.Rule.AllProfilesHaveExamples.TargetExample.byConformance - context.ig.profiles.each do |structure_definition| - next if structure_definition.abstract - - pass_flg = false - context.data.each do |resource| - if structure_definition.type == resource.resourceType - if config.Environment.ExternalValidator.Enabled - pass_flg != Util.validate_resource(resource) - elsif structure_definition.validates_resource?(resource) - pass_flg = true - end - end - end - unused_resource_urls << structure_definition.url unless pass_flg - end - end - - if unused_resource_urls.any? - message = "Found unused profiles: #{unused_resource_urls.join(', ')}" - result = EvaluationResult.new(message, rule: self) - else - message = 'All profiles have instances' - result = EvaluationResult.new(message, severity: 'success', rule: self) - end - - context.add_result result - end - - def profiles(resource) - if resource.resourceType == 'Bundle' - all_profiles = resource.entry.map do |e| - single_resource_profiles(e.resource) - end - all_profiles << single_resource_profiles(resource) - all_profiles.flatten.uniq - else - single_resource_profiles(resource) - end - end - - def single_resource_profiles(resource) - resource&.meta&.profile || [] - end - end - end -end diff --git a/lib/inferno/dsl/fhir_evaluator/rules/all_references_resolve.rb b/lib/inferno/dsl/fhir_evaluator/rules/all_references_resolve.rb deleted file mode 100644 index 734fa521b..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/all_references_resolve.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require_relative '../evaluator_util' -module FhirEvaluator - module Rules - class AllReferencesResolve < Rule - def check(context) - # resource_type_ids is for quick look up when there is a reference type - # resource_ids is for quick look up when there is no type (i.e. uuid used) - resource_type_ids, resource_ids, references = Util.extract_ids_references(context.data) - unresolved_references = Hash.new { |h, k| h[k] = [] } - references.each do |k, v| - v.each do |reference| - # no type for the reference - if reference[1] == '' - unresolved_references[k] << reference unless resource_ids.include?(reference[2]) - elsif !resource_type_ids[reference[1]].include?(reference[2]) - unresolved_references[k] << reference - end - end - end - - if unresolved_references.any? - message = gen_reference_fail_message(unresolved_references) - result = EvaluationResult.new(message, rule: self) - else - message = 'All references resolve' - result = EvaluationResult.new(message, severity: 'success', rule: self) - end - - context.add_result result - end - - def gen_reference_fail_message(unresolved_references) - "Found unresolved references: #{ - unresolved_references.map do |k, v| - "\n Resource (id): #{k} #{v.each_with_index.map do |val, _idx| - val.each_with_index.map do |value, index| - case index - when 0 - " \n\tpath: #{value}" - when 1 - " type: #{value}" - when 2 - " id: #{value}" - end - end - end.join(',')}" - end.join(',')}" - end - end - end -end diff --git a/lib/inferno/dsl/fhir_evaluator/rules/all_resources_reachable.rb b/lib/inferno/dsl/fhir_evaluator/rules/all_resources_reachable.rb deleted file mode 100644 index ab8864d82..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/all_resources_reachable.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -module FhirEvaluator - module Rules - class AllResourcesReachable < Rule - attr_accessor :config - - def check(context) - @config = context.config - # TODO: add some customizable configurations for this rule: - # - whether it should be checked at all - # - determine the base/root resource - # - which types/resources to ignore - # TODO: for the customizable configurations, use the config settings below: - # config.Rule.AllResourcesReachable.CheckedAtAll: true/false - # config.Rule.AllResourcesReachable.RunOnlyBaseResource: true/false - # config.Rule.AllResourcesReachable.IgnoreType: - value - # TODO: can come up with a "connectedness metric" to see how well-connected the data set is - - # every resource is either making a resolvable reference or is referenced - @referenced_resources = Set.new - @referencing_resources = Set.new - @resource_type_ids, @resource_ids, references = Util.extract_ids_references(context.data) - references.each do |id, refs| - assess_reachability(id, refs) - end - - island_resources = @resource_ids - @referenced_resources - @referencing_resources - - if island_resources.any? - message = "Found resources that have no resolved references and are not referenced: #{ - island_resources.to_a.join(', ')}" - result = EvaluationResult.new(message, rule: self) - else - message = 'All resources are reachable' - result = EvaluationResult.new(message, severity: 'success', rule: self) - end - - context.add_result result - end - - def assess_reachability(id, references) - makes_resolvable_reference = false - references.each do |reference_data| - # field = reference_data[0] - type = reference_data[1] - referenced_id = reference_data[2] - - # no type for the reference - if type == '' - if @resource_ids.include?(referenced_id) - makes_resolvable_reference = true - @referenced_resources.add(referenced_id) - end - elsif @resource_type_ids[type].include?(referenced_id) - makes_resolvable_reference = true - @referenced_resources.add(referenced_id) - end - end - @referencing_resources.add(id) if makes_resolvable_reference - end - end - end -end diff --git a/lib/inferno/dsl/fhir_evaluator/rules/all_search_parameters_have_examples.rb b/lib/inferno/dsl/fhir_evaluator/rules/all_search_parameters_have_examples.rb deleted file mode 100644 index 13601d084..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/all_search_parameters_have_examples.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -module FhirEvaluator - module Rules - class AllSearchParametersHaveExamples < HasExamples - def check(context) - unless ENV['FHIRPATH_URL'] - puts 'Warning: FHIRPATH_URL env var not set. Skipping rule AllSearchParametersHaveExamples' - return - end - - get_unused_resource_urls(context.ig.search_params) do |param| - param_is_used?(param, context) - end - - if unused_resource_urls.any? - message = "Found SearchParameters with no searchable data: #{unused_resource_urls.join(', ')}" - result = EvaluationResult.new(message, rule: self) - elsif !context.ig.search_params.empty? - message = 'All SearchParameters have examples' - result = EvaluationResult.new(message, severity: 'success', rule: self) - else - message = 'IG contains no SearchParameters' - result = EvaluationResult.new(message, severity: 'information', rule: self) - end - - context.add_result result - end - - def param_is_used?(param, context) - # Assume that all params have an expression (fhirpath) - # This is not guaranteed since the field is 0..1 - # but without it there's no other way to select a value - - unless param.expression - # message = "SearchParameter #{param.url} has no expression and cannot be checked" - # result = EvaluationResult.new(message) - # context.add_result result - - return true # for now - end - - used = false - - context.data.each do |resource| - next unless param.base.include? resource.resourceType - - begin - result = evaluate(param.expression, resource) - rescue StandardError => e - message = "SearchParameter #{param.url} failed to evaluate due to an error. " \ - "Expression: #{param.expression}. #{e}" - result = EvaluationResult.new(message) - context.add_result result - - # don't bother evaluating it again - used = true - break - end - - if result && !result.empty? - used = true - break - end - end - used - end - - def evaluate(expression, resource) - fhirpath_url = ENV.fetch('FHIRPATH_URL') - path = "#{fhirpath_url}/evaluate?path=#{expression}" - - response = Faraday.post(path, resource.to_json, 'Content-Type' => 'application/json') - raise "External FHIRPath service failed: #{response.status}" unless response.status.to_s.start_with? '2' - - # return value of the service is something like: - # [ { "type": "date", "element": "2022-02-22" } ] - # or an empty array if the selector didn't return anything - - JSON.parse(response.body) - rescue Faraday::Error => e - raise "FHIRPath service not available - HTTP request failed: #{e.message}" - end - end - end -end diff --git a/lib/inferno/dsl/fhir_evaluator/rules/differential_content_has_examples.rb b/lib/inferno/dsl/fhir_evaluator/rules/differential_content_has_examples.rb deleted file mode 100644 index 16b354c92..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/differential_content_has_examples.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -module FhirEvaluator - module Rules - class DifferentialContentHasExamples < Rule - def check(context) - unused_differential = Hash.new { |h, k| h[k] = Set.new } - collect_profile_differential_content(unused_differential, context.ig.profiles) - collect_profile_differential_content(unused_differential, context.ig.extensions) - remove_found_differential_content(unused_differential, context.data) - - if unused_differential.any? { |_url, diff| !diff.empty? } - message = gen_differential_fail_message(unused_differential) - result = EvaluationResult.new(message, rule: self) - else - message = 'All differential fields are represented in instances' - result = EvaluationResult.new(message, severity: 'success', rule: self) - end - - context.add_result result - end - - def collect_profile_differential_content(unused_differential, profiles) - profiles.each do |profile| - profile.each_element do |value, _metadata, path| - next unless path.start_with? 'differential' - - next unless value.is_a? FHIR::ElementDefinition - next unless value.id.include? '.' - - # Skip fields that are disallowed by the profile (cardinality 0..0) - # Note that max is a string to allow for "*", not an int - next if value.max == '0' - - # TODO: discriminate between extensions - # if you have field.extension:A and field.extension:B - # only field.extension is recorded and checked for - # if A and B are not defined in the IG,they may be missed - clean_val = clean_value(value) - - unused_differential[profile.url].add(clean_val) - end - end - end - - def remove_found_differential_content(unused_differential, examples) - examples.each do |resource| - extension_base_path = '' - extension_url = '' - resource.each_element do |value, _metadata, path| - profiles = Util.get_meta_profile(resource) - profiles.each do |profile| - processed_path = plain_value(path) - processed_path = rm_brackets(processed_path) - - if path.match?('extension\[\d+\]\.url$') - extension_base_path = path.rpartition('.').first - extension_url = value - unused_differential[extension_url].delete('url') if unused_differential.key?(extension_url) - unused_differential[extension_url].delete('extension') if unused_differential.key?(extension_url) - unused_differential.delete(extension_url) if unused_differential[extension_url].empty? - elsif path.start_with?(extension_base_path) && !extension_base_path.empty? - if unused_differential.key?(extension_url) - unused_differential[extension_url].delete(processed_path.rpartition('.').last) - end - unused_differential.delete(extension_url) if unused_differential[extension_url].empty? - else - unused_differential[profile].delete(processed_path) if unused_differential.key?(profile) - unused_differential.delete(profile) if unused_differential[profile].empty? - end - end - end - end - end - - def clean_value(value) - stripped_val = value.id.partition('.').last - stripped_val = stripped_val.partition('[').first if stripped_val.end_with? ']' - stripped_val.split('.').map do |field| - field = field.partition(':').first if field.include?(':') - field = field.partition('[').first if field.include?('[') - field - end.join('.') - end - - def plain_value(path) - if path.include? '.' - path_array = path.split('.').map! do |field| - field.start_with?('value') ? 'value' : field - end - path_array.join('.') - elsif path.start_with?('value') - 'value' - elsif path.end_with?(']') - path.rpartition('[').first - else - path - end - end - - def rm_brackets(path) - path_array = path.split('.').map! do |field| - field.include?('[') ? field.partition('[').first : field - end - path_array.join('.') - end - - def gen_differential_fail_message(unused_differential) - "Found fields highlighted in the differential view, but not used in instances: #{ - unused_differential.map do |k, v| - next if v.empty? - - "\n Profile/Extension: #{k} \n\tFields: #{v.join(', ')}" - end.compact.join}" - end - end - end -end diff --git a/lib/inferno/dsl/fhir_evaluator/rules/valuesets_demonstrate.rb b/lib/inferno/dsl/fhir_evaluator/rules/valuesets_demonstrate.rb deleted file mode 100644 index 4f6a2ee18..000000000 --- a/lib/inferno/dsl/fhir_evaluator/rules/valuesets_demonstrate.rb +++ /dev/null @@ -1,191 +0,0 @@ -# frozen_string_literal: true - -require 'net/http' -require 'json' -require 'uri' -require 'base64' - -module FhirEvaluator - module Rules - class ValueSetsDemonstrate < Rule - attr_accessor :config - - def check(context) - @config = context.config - - value_set_unevaluated = [] - value_set_used = [] - value_set_unused = [] - - context.ig.value_sets.each do |valueset| - cnt = 0 - system_codes = [] - - if valueset.to_hash['compose'] - valueset.to_hash['compose']['include'].each do |include| - if include['valueSet'] - include['valueSet'].each do |url| - retrieve_remote_valuesets(url)&.each { |vs| system_codes << vs } - end - next - end - - system_url = include['system'] - - if system_url && include['concept'] - include['concept'].each do |code| - system_codes << { system: system_url, code: code.to_hash['code'] } - end - next - end - - if system_url - if system_url['http://hl7.org/fhir'] - retrieve_remote_valuesets(system_url)&.each { |vs| system_codes << vs } - end - next - end - - value_set_unevaluated << valueset.url unless system_url - - # Exclude if system is provided as Uniform Resource Name "urn:" - if @config.Rule.ValueSetsDemonstrate.Exclude.URL && (system_url['urn']) - value_set_unevaluated << valueset.url - end - - # Exclude filter - if @config.Rule.ValueSetsDemonstrate.Exclude.Filter && (system_url && include['filter']) - value_set_unevaluated << valueset.url - end - - # Exclude only system is provided (e.g. http://loing.org) - if @config.Rule.ValueSetsDemonstrate.Exclude.SystemOnly && (system_url && !include['concept'] && !include['filter']) - value_set_unevaluated << valueset.url - end - end - - else - # In case of value set does not have compose element - value_set_unevaluated << valueset.url - end - system_codes.flatten.uniq! - - value_set_unevaluated << valueset.url if system_codes.none? - - next if value_set_unevaluated.include?(valueset.url) - - resource_used = [] - - context.data.each do |resource| - system_codes.each do |sys_code| - if !sys_code.nil? && find_valueset_used(resource.to_hash, sys_code[:system], sys_code[:code]) - cnt += 1 - resource_used << resource.id unless resource_used.include?(resource.id) - end - end - end - - if cnt.positive? - value_set_used << "#{valueset.url} is used #{cnt} times in #{resource_used.count} resources" - else - value_set_unused << valueset.url - end - end - - value_set_unevaluated.uniq! - - if value_set_unused.none? - message = 'All Value sets are used in Examples:' - value_set_used.map { |vs| message += "\n\t#{vs}" } - - if value_set_unevaluated.any? - message += "\nThe following Value Sets were not able to be evaluated: " - value_set_unevaluated.map { |vs| message += "\n\t#{vs}" } - end - - result = EvaluationResult.new(message, severity: 'success', rule: self) - else - message = 'All codes in these value sets are used at least once in Examples:' - value_set_used.map { |vs| message += "\n\t#{vs}" } - - message += "\nFound unused Value Sets: " - value_set_unused.map { |vs| message += "\n\t#{vs}" } - - if value_set_unevaluated.any? - message += "\nFound unevaluated Value Sets: " - value_set_unevaluated.map { |vs| message += "\n\t#{vs}" } - end - - result = EvaluationResult.new(message, rule: self) - end - - context.add_result result - end - - def find_valueset_used(resource, system, code) - resource.each do |key, value| - next unless key == 'code' || ['value', 'valueCodeableConcept', 'valueString', 'valueQuantity', 'valueBoolean', - 'valueInteger', 'valueRange', 'valueRatio', 'valueSampleData', 'valueDateTime', 'valuePeriod', 'valueTime'].include?(key) - next unless value.is_a?(Hash) - - value['coding']&.each do |codeset| - return true if codeset.to_hash['system'] == system && codeset.to_hash['code'] == code - end - end - - false - end - - def extract_valueset(response) - value_set = JSON.parse(response.body) - - if value_set['compose'] && value_set['compose']['include'] - value_set['compose']['include'].map do |include| - include['concept']&.map { |concept| { system: include['system'], code: concept['code'] } } - end.flatten - else - puts 'No Value Set found in the response.' - end - end - - def retrieve_remote_valuesets(url) - url['http:'] = 'https:' if url['http:'] - uri = URI.parse(url) - - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = (uri.scheme == 'https') - - request = Net::HTTP::Get.new(uri.request_uri) - if url[@config.Rule.ValueSetsDemonstrate.VSAC.url] - username = 'apikey' - password = @config.Rule.ValueSetsDemonstrate.VSAC.apikey - encoded_credentials = Base64.strict_encode64("#{username}:#{password}") - request['Authorization'] = "Basic #{encoded_credentials}" - end - - response = http.request(request) - - content_type = response['content-type'] - return unless content_type && !content_type.include?('text/html') - - while response.is_a?(Net::HTTPRedirection) - redirect_url = response['location'] - - redirect_url['xml'] = 'json' - uri = URI.parse(redirect_url) - - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = (uri.scheme == 'https') - - response = http.request(Net::HTTP::Get.new(uri.request_uri)) - end - - if response.code.to_i == 200 - extract_valueset(response) - else - puts "Failed to retrieve the Value Set: #{url}. HTTP Status Code: #{response.code}" - end - end - end - end -end