inferno_core 0.6.1 → 0.6.2
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.
- checksums.yaml +4 -4
 - data/lib/inferno/apps/cli/evaluate.rb +22 -12
 - data/lib/inferno/config/boot/presets.rb +1 -1
 - data/lib/inferno/dsl/fhir_client.rb +66 -0
 - data/lib/inferno/dsl/fhir_evaluation/evaluation_context.rb +4 -2
 - data/lib/inferno/dsl/fhir_evaluation/evaluator.rb +8 -3
 - data/lib/inferno/dsl/fhir_evaluation/profile_conformance_helper.rb +66 -0
 - data/lib/inferno/dsl/fhir_evaluation/reference_extractor.rb +61 -0
 - data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +379 -0
 - data/lib/inferno/dsl/fhir_evaluation/rules/all_references_resolve.rb +53 -0
 - data/lib/inferno/dsl/fhir_evaluation/rules/all_resources_reachable.rb +63 -0
 - data/lib/inferno/dsl/fhir_resource_navigation.rb +226 -0
 - data/lib/inferno/dsl/must_support_metadata_extractor.rb +366 -0
 - data/lib/inferno/dsl/primitive_type.rb +9 -0
 - data/lib/inferno/dsl/value_extractor.rb +136 -0
 - data/lib/inferno/entities/ig.rb +46 -24
 - data/lib/inferno/public/bundle.js +16 -16
 - data/lib/inferno/version.rb +1 -1
 - data/spec/shared/test_kit_examples.rb +23 -1
 - metadata +11 -2
 
| 
         @@ -0,0 +1,379 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require_relative '../../fhir_resource_navigation'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require_relative '../../must_support_metadata_extractor'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative '../profile_conformance_helper'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Inferno
         
     | 
| 
      
 6 
     | 
    
         
            +
              module DSL
         
     | 
| 
      
 7 
     | 
    
         
            +
                module FHIREvaluation
         
     | 
| 
      
 8 
     | 
    
         
            +
                  module Rules
         
     | 
| 
      
 9 
     | 
    
         
            +
                    # AllMustSupportsPresent checks that at least one instance of every MustSupport element
         
     | 
| 
      
 10 
     | 
    
         
            +
                    #  defined in the given profiles is populated in the given data.
         
     | 
| 
      
 11 
     | 
    
         
            +
                    # MustSupport elements include plain elements, extensions, and slices.
         
     | 
| 
      
 12 
     | 
    
         
            +
                    # The basis of the test is metadata generated in a first pass that processes the profile into a list of fields,
         
     | 
| 
      
 13 
     | 
    
         
            +
                    #  then the second pass check that all elements in the list are present.
         
     | 
| 
      
 14 
     | 
    
         
            +
                    # This metadata approach allows for customizing what is checked, for example elements may be added or removed,
         
     | 
| 
      
 15 
     | 
    
         
            +
                    #  or choices may be defined where only one choice of multiple must be populated to demonstrate support.
         
     | 
| 
      
 16 
     | 
    
         
            +
                    class AllMustSupportsPresent < Rule
         
     | 
| 
      
 17 
     | 
    
         
            +
                      include FHIRResourceNavigation
         
     | 
| 
      
 18 
     | 
    
         
            +
                      include ProfileConformanceHelper
         
     | 
| 
      
 19 
     | 
    
         
            +
                      attr_accessor :metadata
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                      # check is invoked from the evaluator CLI and applies the logic for this Rule to the provided data.
         
     | 
| 
      
 22 
     | 
    
         
            +
                      # At least one instance of every MustSupport element defined in the profiles must be populated in the data.
         
     | 
| 
      
 23 
     | 
    
         
            +
                      # Findings from the rule will be added to context.results.
         
     | 
| 
      
 24 
     | 
    
         
            +
                      # The logic is configurable with a few options, but this method does not support customizing the metadata.
         
     | 
| 
      
 25 
     | 
    
         
            +
                      # @param context [Inferno::DSL::FHIREvaluation::EvaluationContext]
         
     | 
| 
      
 26 
     | 
    
         
            +
                      # @return [void]
         
     | 
| 
      
 27 
     | 
    
         
            +
                      def check(context)
         
     | 
| 
      
 28 
     | 
    
         
            +
                        missing_items_by_profile = {}
         
     | 
| 
      
 29 
     | 
    
         
            +
                        context.ig.profiles.each do |profile|
         
     | 
| 
      
 30 
     | 
    
         
            +
                          resources = pick_resources_for_profile(profile, context)
         
     | 
| 
      
 31 
     | 
    
         
            +
                          if resources.blank?
         
     | 
| 
      
 32 
     | 
    
         
            +
                            missing_items_by_profile[profile.url] = ['No matching resources were found to check']
         
     | 
| 
      
 33 
     | 
    
         
            +
                            next
         
     | 
| 
      
 34 
     | 
    
         
            +
                          end
         
     | 
| 
      
 35 
     | 
    
         
            +
                          requirement_extension = context.config.data['Rule']['AllMustSupportsPresent']['RequirementExtensionUrl']
         
     | 
| 
      
 36 
     | 
    
         
            +
                          debug_metadata = context.config.data['Rule']['AllMustSupportsPresent']['WriteMetadataForDebugging']
         
     | 
| 
      
 37 
     | 
    
         
            +
                          profile_metadata = extract_metadata(profile, context.ig, requirement_extension:)
         
     | 
| 
      
 38 
     | 
    
         
            +
                          missing_items = perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata:)
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                          missing_items_by_profile[profile.url] = missing_items if missing_items.any?
         
     | 
| 
      
 41 
     | 
    
         
            +
                        end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                        if missing_items_by_profile.count.zero?
         
     | 
| 
      
 44 
     | 
    
         
            +
                          result = EvaluationResult.new('All MustSupports are present', severity: 'success', rule: self)
         
     | 
| 
      
 45 
     | 
    
         
            +
                        else
         
     | 
| 
      
 46 
     | 
    
         
            +
                          message = 'Found Profiles with not all MustSupports represented:'
         
     | 
| 
      
 47 
     | 
    
         
            +
                          missing_items_by_profile.each do |profile_url, missing_items|
         
     | 
| 
      
 48 
     | 
    
         
            +
                            message += "\n\t\t#{profile_url}: #{missing_items.join(', ')}"
         
     | 
| 
      
 49 
     | 
    
         
            +
                          end
         
     | 
| 
      
 50 
     | 
    
         
            +
                          result = EvaluationResult.new(message, rule: self)
         
     | 
| 
      
 51 
     | 
    
         
            +
                        end
         
     | 
| 
      
 52 
     | 
    
         
            +
                        context.add_result result
         
     | 
| 
      
 53 
     | 
    
         
            +
                      end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                      def pick_resources_for_profile(profile, context)
         
     | 
| 
      
 56 
     | 
    
         
            +
                        conformance_options = context.config.data['Rule']['AllMustSupportsPresent']['ConformanceOptions'].to_options
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                        # Unless specifically looking for Bundles, break them out into the resources they include
         
     | 
| 
      
 59 
     | 
    
         
            +
                        all_resources =
         
     | 
| 
      
 60 
     | 
    
         
            +
                          if profile.type == 'Bundle'
         
     | 
| 
      
 61 
     | 
    
         
            +
                            context.data
         
     | 
| 
      
 62 
     | 
    
         
            +
                          else
         
     | 
| 
      
 63 
     | 
    
         
            +
                            flatten_bundles(context.data)
         
     | 
| 
      
 64 
     | 
    
         
            +
                          end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                        all_resources.filter do |r|
         
     | 
| 
      
 67 
     | 
    
         
            +
                          conforms_to_profile?(r, profile, conformance_options, context.validator)
         
     | 
| 
      
 68 
     | 
    
         
            +
                        end
         
     | 
| 
      
 69 
     | 
    
         
            +
                      end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                      # perform_must_support_test is invoked from DSL assertions, allows customizing the metadata with a block.
         
     | 
| 
      
 72 
     | 
    
         
            +
                      # Customizing the metadata may add, modify, or remove items.
         
     | 
| 
      
 73 
     | 
    
         
            +
                      # For instance, US Core 3.1.1 Patient "Previous Name" is defined as MS only in narrative.
         
     | 
| 
      
 74 
     | 
    
         
            +
                      # Choices are also defined only in narrative.
         
     | 
| 
      
 75 
     | 
    
         
            +
                      # @param profile [FHIR::StructureDefinition]
         
     | 
| 
      
 76 
     | 
    
         
            +
                      # @param resources [Array<FHIR::Model>]
         
     | 
| 
      
 77 
     | 
    
         
            +
                      # @param ig [Inferno::Entities::IG]
         
     | 
| 
      
 78 
     | 
    
         
            +
                      # @param debug_metadata [Boolean] if true, write out the final metadata used to a temporary file
         
     | 
| 
      
 79 
     | 
    
         
            +
                      # @param requirement_extension [String] Extension URL that implies "required" as an alternative to the MS flag
         
     | 
| 
      
 80 
     | 
    
         
            +
                      # @yield [Metadata] Customize the metadata before running the test
         
     | 
| 
      
 81 
     | 
    
         
            +
                      # @return [Array<String>] list of elements that were not found in the provided resources
         
     | 
| 
      
 82 
     | 
    
         
            +
                      def perform_must_support_test(profile, resources, ig, debug_metadata: false, requirement_extension: nil)
         
     | 
| 
      
 83 
     | 
    
         
            +
                        profile_metadata = extract_metadata(profile, ig, requirement_extension:)
         
     | 
| 
      
 84 
     | 
    
         
            +
                        yield profile_metadata if block_given?
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                        perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata:)
         
     | 
| 
      
 87 
     | 
    
         
            +
                      end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                      # perform_must_support_test_with_metadata is invoked from check and perform_must_support_test,
         
     | 
| 
      
 90 
     | 
    
         
            +
                      # with the metadata to be used as the basis for the test.
         
     | 
| 
      
 91 
     | 
    
         
            +
                      # It may also be invoked directly from a test if you want to completely overwrite the metadata.
         
     | 
| 
      
 92 
     | 
    
         
            +
                      # @param resources [Array<FHIR::Model>]
         
     | 
| 
      
 93 
     | 
    
         
            +
                      # @param profile_metadata [Metadata] Metadata object with must_supports field
         
     | 
| 
      
 94 
     | 
    
         
            +
                      # @param debug_metadata [Boolean] if true, write out the final metadata used to a temporary file
         
     | 
| 
      
 95 
     | 
    
         
            +
                      # @return [Array<String>] list of elements that were not found in the provided resources
         
     | 
| 
      
 96 
     | 
    
         
            +
                      def perform_must_support_test_with_metadata(resources, profile_metadata, debug_metadata: false)
         
     | 
| 
      
 97 
     | 
    
         
            +
                        return if resources.blank?
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                        @metadata = profile_metadata
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                        write_metadata_for_debugging if debug_metadata
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                        missing_elements(resources)
         
     | 
| 
      
 104 
     | 
    
         
            +
                        missing_slices(resources)
         
     | 
| 
      
 105 
     | 
    
         
            +
                        missing_extensions(resources)
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                        handle_must_support_choices if metadata.must_supports[:choices].present?
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                        missing_must_support_strings
         
     | 
| 
      
 110 
     | 
    
         
            +
                      end
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                      def extract_metadata(profile, ig, requirement_extension: nil)
         
     | 
| 
      
 113 
     | 
    
         
            +
                        MustSupportMetadataExtractor.new(profile.snapshot.element, profile, profile.type, ig, requirement_extension)
         
     | 
| 
      
 114 
     | 
    
         
            +
                      end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                      def write_metadata_for_debugging
         
     | 
| 
      
 117 
     | 
    
         
            +
                        outfile = "#{metadata.profile&.id}-#{SecureRandom.uuid}.yml"
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                        File.open(File.join(Dir.tmpdir, outfile), 'w') do |f|
         
     | 
| 
      
 120 
     | 
    
         
            +
                          writable_metadata = { must_supports: @metadata.must_supports.to_hash }
         
     | 
| 
      
 121 
     | 
    
         
            +
                          f.write(YAML.dump(writable_metadata))
         
     | 
| 
      
 122 
     | 
    
         
            +
                          puts "Wrote MustSupport metadata to #{f.path}"
         
     | 
| 
      
 123 
     | 
    
         
            +
                        end
         
     | 
| 
      
 124 
     | 
    
         
            +
                      end
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                      def handle_must_support_choices
         
     | 
| 
      
 127 
     | 
    
         
            +
                        handle_must_support_element_choices
         
     | 
| 
      
 128 
     | 
    
         
            +
                        handle_must_support_extension_choices
         
     | 
| 
      
 129 
     | 
    
         
            +
                        handle_must_support_slice_choices
         
     | 
| 
      
 130 
     | 
    
         
            +
                      end
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
                      def handle_must_support_element_choices
         
     | 
| 
      
 133 
     | 
    
         
            +
                        missing_elements.delete_if do |element|
         
     | 
| 
      
 134 
     | 
    
         
            +
                          choices = metadata.must_supports[:choices].find do |choice|
         
     | 
| 
      
 135 
     | 
    
         
            +
                            choice[:paths]&.include?(element[:path]) ||
         
     | 
| 
      
 136 
     | 
    
         
            +
                              choice[:elements]&.any? { |ms_element| ms_element[:path] == element[:path] }
         
     | 
| 
      
 137 
     | 
    
         
            +
                          end
         
     | 
| 
      
 138 
     | 
    
         
            +
                          any_choice_supported?(choices)
         
     | 
| 
      
 139 
     | 
    
         
            +
                        end
         
     | 
| 
      
 140 
     | 
    
         
            +
                      end
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                      def handle_must_support_extension_choices
         
     | 
| 
      
 143 
     | 
    
         
            +
                        missing_extensions.delete_if do |extension|
         
     | 
| 
      
 144 
     | 
    
         
            +
                          choices = metadata.must_supports[:choices].find do |choice|
         
     | 
| 
      
 145 
     | 
    
         
            +
                            choice[:extension_ids]&.include?(extension[:id])
         
     | 
| 
      
 146 
     | 
    
         
            +
                          end
         
     | 
| 
      
 147 
     | 
    
         
            +
                          any_choice_supported?(choices)
         
     | 
| 
      
 148 
     | 
    
         
            +
                        end
         
     | 
| 
      
 149 
     | 
    
         
            +
                      end
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
                      def handle_must_support_slice_choices
         
     | 
| 
      
 152 
     | 
    
         
            +
                        missing_slices.delete_if do |slice|
         
     | 
| 
      
 153 
     | 
    
         
            +
                          choices = metadata.must_supports[:choices].find { |choice| choice[:slice_names]&.include?(slice[:name]) }
         
     | 
| 
      
 154 
     | 
    
         
            +
                          any_choice_supported?(choices)
         
     | 
| 
      
 155 
     | 
    
         
            +
                        end
         
     | 
| 
      
 156 
     | 
    
         
            +
                      end
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                      def any_choice_supported?(choices)
         
     | 
| 
      
 159 
     | 
    
         
            +
                        return false unless choices.present?
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
                        any_path_choice_supported?(choices) ||
         
     | 
| 
      
 162 
     | 
    
         
            +
                          any_extension_ids_choice_supported?(choices) ||
         
     | 
| 
      
 163 
     | 
    
         
            +
                          any_slice_names_choice_supported?(choices) ||
         
     | 
| 
      
 164 
     | 
    
         
            +
                          any_elements_choice_supported?(choices)
         
     | 
| 
      
 165 
     | 
    
         
            +
                      end
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                      def any_path_choice_supported?(choices)
         
     | 
| 
      
 168 
     | 
    
         
            +
                        return false unless choices[:paths].present?
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
                        choices[:paths].any? { |path| missing_elements.none? { |element| element[:path] == path } }
         
     | 
| 
      
 171 
     | 
    
         
            +
                      end
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
      
 173 
     | 
    
         
            +
                      def any_extension_ids_choice_supported?(choices)
         
     | 
| 
      
 174 
     | 
    
         
            +
                        return false unless choices[:extension_ids].present?
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
                        choices[:extension_ids].any? do |extension_id|
         
     | 
| 
      
 177 
     | 
    
         
            +
                          missing_extensions.none? { |extension| extension[:id] == extension_id }
         
     | 
| 
      
 178 
     | 
    
         
            +
                        end
         
     | 
| 
      
 179 
     | 
    
         
            +
                      end
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                      def any_slice_names_choice_supported?(choices)
         
     | 
| 
      
 182 
     | 
    
         
            +
                        return false unless choices[:slice_names].present?
         
     | 
| 
      
 183 
     | 
    
         
            +
             
     | 
| 
      
 184 
     | 
    
         
            +
                        choices[:slice_names].any? { |slice_name| missing_slices.none? { |slice| slice[:name] == slice_name } }
         
     | 
| 
      
 185 
     | 
    
         
            +
                      end
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
                      def any_elements_choice_supported?(choices)
         
     | 
| 
      
 188 
     | 
    
         
            +
                        return false unless choices[:elements].present?
         
     | 
| 
      
 189 
     | 
    
         
            +
             
     | 
| 
      
 190 
     | 
    
         
            +
                        choices[:elements].any? do |choice|
         
     | 
| 
      
 191 
     | 
    
         
            +
                          missing_elements.none? do |element|
         
     | 
| 
      
 192 
     | 
    
         
            +
                            element[:path] == choice[:path] && element[:fixed_value] == choice[:fixed_value]
         
     | 
| 
      
 193 
     | 
    
         
            +
                          end
         
     | 
| 
      
 194 
     | 
    
         
            +
                        end
         
     | 
| 
      
 195 
     | 
    
         
            +
                      end
         
     | 
| 
      
 196 
     | 
    
         
            +
             
     | 
| 
      
 197 
     | 
    
         
            +
                      def missing_must_support_strings
         
     | 
| 
      
 198 
     | 
    
         
            +
                        missing_elements.map { |element_definition| missing_element_string(element_definition) } +
         
     | 
| 
      
 199 
     | 
    
         
            +
                          missing_slices.map { |slice_definition| slice_definition[:slice_id] } +
         
     | 
| 
      
 200 
     | 
    
         
            +
                          missing_extensions.map { |extension_definition| extension_definition[:id] }
         
     | 
| 
      
 201 
     | 
    
         
            +
                      end
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
      
 203 
     | 
    
         
            +
                      def missing_element_string(element_definition)
         
     | 
| 
      
 204 
     | 
    
         
            +
                        if element_definition[:fixed_value].present?
         
     | 
| 
      
 205 
     | 
    
         
            +
                          "#{element_definition[:path]}:#{element_definition[:fixed_value]}"
         
     | 
| 
      
 206 
     | 
    
         
            +
                        else
         
     | 
| 
      
 207 
     | 
    
         
            +
                          element_definition[:path]
         
     | 
| 
      
 208 
     | 
    
         
            +
                        end
         
     | 
| 
      
 209 
     | 
    
         
            +
                      end
         
     | 
| 
      
 210 
     | 
    
         
            +
             
     | 
| 
      
 211 
     | 
    
         
            +
                      def must_support_extensions
         
     | 
| 
      
 212 
     | 
    
         
            +
                        metadata.must_supports[:extensions]
         
     | 
| 
      
 213 
     | 
    
         
            +
                      end
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
                      def missing_extensions(resources = [])
         
     | 
| 
      
 216 
     | 
    
         
            +
                        @missing_extensions ||=
         
     | 
| 
      
 217 
     | 
    
         
            +
                          must_support_extensions.select do |extension_definition|
         
     | 
| 
      
 218 
     | 
    
         
            +
                            resources.none? do |resource|
         
     | 
| 
      
 219 
     | 
    
         
            +
                              path = extension_definition[:path]
         
     | 
| 
      
 220 
     | 
    
         
            +
             
     | 
| 
      
 221 
     | 
    
         
            +
                              if path == 'extension'
         
     | 
| 
      
 222 
     | 
    
         
            +
                                resource.extension.any? { |extension| extension.url == extension_definition[:url] }
         
     | 
| 
      
 223 
     | 
    
         
            +
                              else
         
     | 
| 
      
 224 
     | 
    
         
            +
                                extension = find_a_value_at(resource, path) do |el|
         
     | 
| 
      
 225 
     | 
    
         
            +
                                  el.url == extension_definition[:url]
         
     | 
| 
      
 226 
     | 
    
         
            +
                                end
         
     | 
| 
      
 227 
     | 
    
         
            +
             
     | 
| 
      
 228 
     | 
    
         
            +
                                extension.present?
         
     | 
| 
      
 229 
     | 
    
         
            +
                              end
         
     | 
| 
      
 230 
     | 
    
         
            +
                            end
         
     | 
| 
      
 231 
     | 
    
         
            +
                          end
         
     | 
| 
      
 232 
     | 
    
         
            +
                      end
         
     | 
| 
      
 233 
     | 
    
         
            +
             
     | 
| 
      
 234 
     | 
    
         
            +
                      def must_support_elements
         
     | 
| 
      
 235 
     | 
    
         
            +
                        metadata.must_supports[:elements]
         
     | 
| 
      
 236 
     | 
    
         
            +
                      end
         
     | 
| 
      
 237 
     | 
    
         
            +
             
     | 
| 
      
 238 
     | 
    
         
            +
                      def missing_elements(resources = [])
         
     | 
| 
      
 239 
     | 
    
         
            +
                        @missing_elements ||= find_missing_elements(resources, must_support_elements)
         
     | 
| 
      
 240 
     | 
    
         
            +
                      end
         
     | 
| 
      
 241 
     | 
    
         
            +
             
     | 
| 
      
 242 
     | 
    
         
            +
                      def find_missing_elements(resources, must_support_elements)
         
     | 
| 
      
 243 
     | 
    
         
            +
                        must_support_elements.select do |element_definition|
         
     | 
| 
      
 244 
     | 
    
         
            +
                          resources.none? { |resource| resource_populates_element?(resource, element_definition) }
         
     | 
| 
      
 245 
     | 
    
         
            +
                        end
         
     | 
| 
      
 246 
     | 
    
         
            +
                      end
         
     | 
| 
      
 247 
     | 
    
         
            +
             
     | 
| 
      
 248 
     | 
    
         
            +
                      def resource_populates_element?(resource, element_definition)
         
     | 
| 
      
 249 
     | 
    
         
            +
                        path = element_definition[:path]
         
     | 
| 
      
 250 
     | 
    
         
            +
                        ms_extension_urls = must_support_extensions.select { |ex| ex[:path] == "#{path}.extension" }
         
     | 
| 
      
 251 
     | 
    
         
            +
                          .map { |ex| ex[:url] }
         
     | 
| 
      
 252 
     | 
    
         
            +
             
     | 
| 
      
 253 
     | 
    
         
            +
                        value_found = find_a_value_at(resource, path) do |potential_value|
         
     | 
| 
      
 254 
     | 
    
         
            +
                          matching_without_extensions?(potential_value, ms_extension_urls, element_definition[:fixed_value])
         
     | 
| 
      
 255 
     | 
    
         
            +
                        end
         
     | 
| 
      
 256 
     | 
    
         
            +
             
     | 
| 
      
 257 
     | 
    
         
            +
                        # Note that false.present? => false, which is why we need to add this extra check
         
     | 
| 
      
 258 
     | 
    
         
            +
                        value_found.present? || value_found == false
         
     | 
| 
      
 259 
     | 
    
         
            +
                      end
         
     | 
| 
      
 260 
     | 
    
         
            +
             
     | 
| 
      
 261 
     | 
    
         
            +
                      def matching_without_extensions?(value, ms_extension_urls, fixed_value)
         
     | 
| 
      
 262 
     | 
    
         
            +
                        if value.instance_of?(Inferno::DSL::PrimitiveType)
         
     | 
| 
      
 263 
     | 
    
         
            +
                          urls = value.extension&.map(&:url)
         
     | 
| 
      
 264 
     | 
    
         
            +
                          has_ms_extension = (urls & ms_extension_urls).present?
         
     | 
| 
      
 265 
     | 
    
         
            +
                          value = value.value
         
     | 
| 
      
 266 
     | 
    
         
            +
                        end
         
     | 
| 
      
 267 
     | 
    
         
            +
             
     | 
| 
      
 268 
     | 
    
         
            +
                        return false unless has_ms_extension || value_without_extensions?(value)
         
     | 
| 
      
 269 
     | 
    
         
            +
             
     | 
| 
      
 270 
     | 
    
         
            +
                        matches_fixed_value?(value, fixed_value)
         
     | 
| 
      
 271 
     | 
    
         
            +
                      end
         
     | 
| 
      
 272 
     | 
    
         
            +
             
     | 
| 
      
 273 
     | 
    
         
            +
                      def matches_fixed_value?(value, fixed_value)
         
     | 
| 
      
 274 
     | 
    
         
            +
                        fixed_value.blank? || value == fixed_value
         
     | 
| 
      
 275 
     | 
    
         
            +
                      end
         
     | 
| 
      
 276 
     | 
    
         
            +
             
     | 
| 
      
 277 
     | 
    
         
            +
                      def value_without_extensions?(value)
         
     | 
| 
      
 278 
     | 
    
         
            +
                        value_without_extensions = value.respond_to?(:to_hash) ? value.to_hash.except('extension') : value
         
     | 
| 
      
 279 
     | 
    
         
            +
                        value_without_extensions.present? || value_without_extensions == false
         
     | 
| 
      
 280 
     | 
    
         
            +
                      end
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
                      def must_support_slices
         
     | 
| 
      
 283 
     | 
    
         
            +
                        metadata.must_supports[:slices]
         
     | 
| 
      
 284 
     | 
    
         
            +
                      end
         
     | 
| 
      
 285 
     | 
    
         
            +
             
     | 
| 
      
 286 
     | 
    
         
            +
                      def missing_slices(resources = [])
         
     | 
| 
      
 287 
     | 
    
         
            +
                        @missing_slices ||=
         
     | 
| 
      
 288 
     | 
    
         
            +
                          must_support_slices.select do |slice|
         
     | 
| 
      
 289 
     | 
    
         
            +
                            resources.none? do |resource|
         
     | 
| 
      
 290 
     | 
    
         
            +
                              path = slice[:path]
         
     | 
| 
      
 291 
     | 
    
         
            +
                              find_slice(resource, path, slice[:discriminator]).present?
         
     | 
| 
      
 292 
     | 
    
         
            +
                            end
         
     | 
| 
      
 293 
     | 
    
         
            +
                          end
         
     | 
| 
      
 294 
     | 
    
         
            +
                      end
         
     | 
| 
      
 295 
     | 
    
         
            +
             
     | 
| 
      
 296 
     | 
    
         
            +
                      def find_slice(resource, path, discriminator)
         
     | 
| 
      
 297 
     | 
    
         
            +
                        # TODO: there is a lot of similarity
         
     | 
| 
      
 298 
     | 
    
         
            +
                        # between this and FHIRResourceNavigation.matching_slice?
         
     | 
| 
      
 299 
     | 
    
         
            +
                        # Can these be combined?
         
     | 
| 
      
 300 
     | 
    
         
            +
                        find_a_value_at(resource, path) do |element|
         
     | 
| 
      
 301 
     | 
    
         
            +
                          case discriminator[:type]
         
     | 
| 
      
 302 
     | 
    
         
            +
                          when 'patternCodeableConcept'
         
     | 
| 
      
 303 
     | 
    
         
            +
                            find_pattern_codeable_concept_slice(element, discriminator)
         
     | 
| 
      
 304 
     | 
    
         
            +
                          when 'patternCoding'
         
     | 
| 
      
 305 
     | 
    
         
            +
                            find_pattern_coding_slice(element, discriminator)
         
     | 
| 
      
 306 
     | 
    
         
            +
                          when 'patternIdentifier'
         
     | 
| 
      
 307 
     | 
    
         
            +
                            find_pattern_identifier_slice(element, discriminator)
         
     | 
| 
      
 308 
     | 
    
         
            +
                          when 'value'
         
     | 
| 
      
 309 
     | 
    
         
            +
                            find_value_slice(element, discriminator)
         
     | 
| 
      
 310 
     | 
    
         
            +
                          when 'type'
         
     | 
| 
      
 311 
     | 
    
         
            +
                            find_type_slice(element, discriminator)
         
     | 
| 
      
 312 
     | 
    
         
            +
                          when 'requiredBinding'
         
     | 
| 
      
 313 
     | 
    
         
            +
                            find_required_binding_slice(element, discriminator)
         
     | 
| 
      
 314 
     | 
    
         
            +
                          end
         
     | 
| 
      
 315 
     | 
    
         
            +
                        end
         
     | 
| 
      
 316 
     | 
    
         
            +
                      end
         
     | 
| 
      
 317 
     | 
    
         
            +
             
     | 
| 
      
 318 
     | 
    
         
            +
                      def find_pattern_codeable_concept_slice(element, discriminator)
         
     | 
| 
      
 319 
     | 
    
         
            +
                        coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
         
     | 
| 
      
 320 
     | 
    
         
            +
                        find_a_value_at(element, coding_path) do |coding|
         
     | 
| 
      
 321 
     | 
    
         
            +
                          coding.code == discriminator[:code] && coding.system == discriminator[:system]
         
     | 
| 
      
 322 
     | 
    
         
            +
                        end
         
     | 
| 
      
 323 
     | 
    
         
            +
                      end
         
     | 
| 
      
 324 
     | 
    
         
            +
             
     | 
| 
      
 325 
     | 
    
         
            +
                      def find_pattern_coding_slice(element, discriminator)
         
     | 
| 
      
 326 
     | 
    
         
            +
                        coding_path = discriminator[:path].present? ? discriminator[:path] : ''
         
     | 
| 
      
 327 
     | 
    
         
            +
                        find_a_value_at(element, coding_path) do |coding|
         
     | 
| 
      
 328 
     | 
    
         
            +
                          coding.code == discriminator[:code] && coding.system == discriminator[:system]
         
     | 
| 
      
 329 
     | 
    
         
            +
                        end
         
     | 
| 
      
 330 
     | 
    
         
            +
                      end
         
     | 
| 
      
 331 
     | 
    
         
            +
             
     | 
| 
      
 332 
     | 
    
         
            +
                      def find_pattern_identifier_slice(element, discriminator)
         
     | 
| 
      
 333 
     | 
    
         
            +
                        find_a_value_at(element, discriminator[:path]) do |identifier|
         
     | 
| 
      
 334 
     | 
    
         
            +
                          identifier.system == discriminator[:system]
         
     | 
| 
      
 335 
     | 
    
         
            +
                        end
         
     | 
| 
      
 336 
     | 
    
         
            +
                      end
         
     | 
| 
      
 337 
     | 
    
         
            +
             
     | 
| 
      
 338 
     | 
    
         
            +
                      def find_value_slice(element, discriminator)
         
     | 
| 
      
 339 
     | 
    
         
            +
                        values = discriminator[:values].map { |value| value.merge(path: value[:path].split('.')) }
         
     | 
| 
      
 340 
     | 
    
         
            +
                        find_slice_by_values(element, values)
         
     | 
| 
      
 341 
     | 
    
         
            +
                      end
         
     | 
| 
      
 342 
     | 
    
         
            +
             
     | 
| 
      
 343 
     | 
    
         
            +
                      def find_type_slice(element, discriminator)
         
     | 
| 
      
 344 
     | 
    
         
            +
                        case discriminator[:code]
         
     | 
| 
      
 345 
     | 
    
         
            +
                        when 'Date'
         
     | 
| 
      
 346 
     | 
    
         
            +
                          begin
         
     | 
| 
      
 347 
     | 
    
         
            +
                            Date.parse(element)
         
     | 
| 
      
 348 
     | 
    
         
            +
                          rescue ArgumentError
         
     | 
| 
      
 349 
     | 
    
         
            +
                            false
         
     | 
| 
      
 350 
     | 
    
         
            +
                          end
         
     | 
| 
      
 351 
     | 
    
         
            +
                        when 'DateTime'
         
     | 
| 
      
 352 
     | 
    
         
            +
                          begin
         
     | 
| 
      
 353 
     | 
    
         
            +
                            DateTime.parse(element)
         
     | 
| 
      
 354 
     | 
    
         
            +
                          rescue ArgumentError
         
     | 
| 
      
 355 
     | 
    
         
            +
                            false
         
     | 
| 
      
 356 
     | 
    
         
            +
                          end
         
     | 
| 
      
 357 
     | 
    
         
            +
                        when 'String'
         
     | 
| 
      
 358 
     | 
    
         
            +
                          element.is_a? String
         
     | 
| 
      
 359 
     | 
    
         
            +
                        else
         
     | 
| 
      
 360 
     | 
    
         
            +
                          element.is_a? FHIR.const_get(discriminator[:code])
         
     | 
| 
      
 361 
     | 
    
         
            +
                        end
         
     | 
| 
      
 362 
     | 
    
         
            +
                      end
         
     | 
| 
      
 363 
     | 
    
         
            +
             
     | 
| 
      
 364 
     | 
    
         
            +
                      def find_required_binding_slice(element, discriminator)
         
     | 
| 
      
 365 
     | 
    
         
            +
                        coding_path = discriminator[:path].present? ? "#{discriminator[:path]}.coding" : 'coding'
         
     | 
| 
      
 366 
     | 
    
         
            +
             
     | 
| 
      
 367 
     | 
    
         
            +
                        find_a_value_at(element, coding_path) do |coding|
         
     | 
| 
      
 368 
     | 
    
         
            +
                          discriminator[:values].any? { |value| value[:system] == coding.system && value[:code] == coding.code }
         
     | 
| 
      
 369 
     | 
    
         
            +
                        end
         
     | 
| 
      
 370 
     | 
    
         
            +
                      end
         
     | 
| 
      
 371 
     | 
    
         
            +
             
     | 
| 
      
 372 
     | 
    
         
            +
                      def find_slice_by_values(element, value_definitions)
         
     | 
| 
      
 373 
     | 
    
         
            +
                        Array.wrap(element).find { |el| verify_slice_by_values(el, value_definitions) }
         
     | 
| 
      
 374 
     | 
    
         
            +
                      end
         
     | 
| 
      
 375 
     | 
    
         
            +
                    end
         
     | 
| 
      
 376 
     | 
    
         
            +
                  end
         
     | 
| 
      
 377 
     | 
    
         
            +
                end
         
     | 
| 
      
 378 
     | 
    
         
            +
              end
         
     | 
| 
      
 379 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,53 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative '../reference_extractor'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Inferno
         
     | 
| 
      
 6 
     | 
    
         
            +
              module DSL
         
     | 
| 
      
 7 
     | 
    
         
            +
                module FHIREvaluation
         
     | 
| 
      
 8 
     | 
    
         
            +
                  module Rules
         
     | 
| 
      
 9 
     | 
    
         
            +
                    class AllReferencesResolve < Rule
         
     | 
| 
      
 10 
     | 
    
         
            +
                      def check(context)
         
     | 
| 
      
 11 
     | 
    
         
            +
                        extractor = Inferno::DSL::FHIREvaluation::ReferenceExtractor.new
         
     | 
| 
      
 12 
     | 
    
         
            +
                        resource_type_ids = extractor.extract_resource_type_ids(context.data)
         
     | 
| 
      
 13 
     | 
    
         
            +
                        resource_ids = Set.new(resource_type_ids.values.flatten.uniq)
         
     | 
| 
      
 14 
     | 
    
         
            +
                        reference_map = extractor.extract_references(context.data)
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                        unresolved_references = Hash.new { |reference, id| reference[id] = [] }
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                        reference_map.each do |resource_id, references|
         
     | 
| 
      
 19 
     | 
    
         
            +
                          references.each do |reference|
         
     | 
| 
      
 20 
     | 
    
         
            +
                            if reference[:type] == ''
         
     | 
| 
      
 21 
     | 
    
         
            +
                              unresolved_references[resource_id] << reference unless resource_ids.include?(reference[:id])
         
     | 
| 
      
 22 
     | 
    
         
            +
                            elsif !resource_type_ids[reference[:type]].include?(reference[:id])
         
     | 
| 
      
 23 
     | 
    
         
            +
                              unresolved_references[resource_id] << reference
         
     | 
| 
      
 24 
     | 
    
         
            +
                            end
         
     | 
| 
      
 25 
     | 
    
         
            +
                          end
         
     | 
| 
      
 26 
     | 
    
         
            +
                        end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                        if unresolved_references.any?
         
     | 
| 
      
 29 
     | 
    
         
            +
                          message = gen_reference_fail_message(unresolved_references)
         
     | 
| 
      
 30 
     | 
    
         
            +
                          result = EvaluationResult.new(message, rule: self)
         
     | 
| 
      
 31 
     | 
    
         
            +
                        else
         
     | 
| 
      
 32 
     | 
    
         
            +
                          message = 'All references resolve'
         
     | 
| 
      
 33 
     | 
    
         
            +
                          result = EvaluationResult.new(message, severity: 'success', rule: self)
         
     | 
| 
      
 34 
     | 
    
         
            +
                        end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                        context.add_result result
         
     | 
| 
      
 37 
     | 
    
         
            +
                      end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                      def gen_reference_fail_message(unresolved_references)
         
     | 
| 
      
 40 
     | 
    
         
            +
                        result_message = unresolved_references.map do |resource_id, references|
         
     | 
| 
      
 41 
     | 
    
         
            +
                          reference_detail = references.map do |reference|
         
     | 
| 
      
 42 
     | 
    
         
            +
                            " \n\tpath: #{reference[:path]}, type: #{reference[:type]}, id: #{reference[:id]}"
         
     | 
| 
      
 43 
     | 
    
         
            +
                          end.join(',')
         
     | 
| 
      
 44 
     | 
    
         
            +
                          "\n Resource (id): #{resource_id} #{reference_detail}"
         
     | 
| 
      
 45 
     | 
    
         
            +
                        end.join(',')
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                        "Found unresolved references: #{result_message}"
         
     | 
| 
      
 48 
     | 
    
         
            +
                      end
         
     | 
| 
      
 49 
     | 
    
         
            +
                    end
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
              end
         
     | 
| 
      
 53 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,63 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative '../reference_extractor'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Inferno
         
     | 
| 
      
 6 
     | 
    
         
            +
              module DSL
         
     | 
| 
      
 7 
     | 
    
         
            +
                module FHIREvaluation
         
     | 
| 
      
 8 
     | 
    
         
            +
                  module Rules
         
     | 
| 
      
 9 
     | 
    
         
            +
                    class AllResourcesReachable < Rule
         
     | 
| 
      
 10 
     | 
    
         
            +
                      attr_accessor :config, :referenced_resources, :referencing_resources, :resource_ids, :resource_type_ids
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                      def check(context)
         
     | 
| 
      
 13 
     | 
    
         
            +
                        @config = context.config
         
     | 
| 
      
 14 
     | 
    
         
            +
                        @referenced_resources = Set.new
         
     | 
| 
      
 15 
     | 
    
         
            +
                        @referencing_resources = Set.new
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                        extractor = Inferno::DSL::FHIREvaluation::ReferenceExtractor.new
         
     | 
| 
      
 18 
     | 
    
         
            +
                        @resource_type_ids = extractor.extract_resource_type_ids(context.data)
         
     | 
| 
      
 19 
     | 
    
         
            +
                        @resource_ids = Set.new(resource_type_ids.values.flatten.uniq)
         
     | 
| 
      
 20 
     | 
    
         
            +
                        reference_map = extractor.extract_references(context.data)
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                        reference_map.each do |resource_id, references|
         
     | 
| 
      
 23 
     | 
    
         
            +
                          assess_reachability(resource_id, references)
         
     | 
| 
      
 24 
     | 
    
         
            +
                        end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                        island_resources = resource_ids - referenced_resources - referencing_resources
         
     | 
| 
      
 27 
     | 
    
         
            +
                        island_resources.to_a.sort!
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                        if island_resources.any?
         
     | 
| 
      
 30 
     | 
    
         
            +
                          message = "Found resources that have no resolved references and are not referenced: #{
         
     | 
| 
      
 31 
     | 
    
         
            +
                            island_resources.join(', ')}"
         
     | 
| 
      
 32 
     | 
    
         
            +
                          result = EvaluationResult.new(message, rule: self)
         
     | 
| 
      
 33 
     | 
    
         
            +
                        else
         
     | 
| 
      
 34 
     | 
    
         
            +
                          message = 'All resources are reachable'
         
     | 
| 
      
 35 
     | 
    
         
            +
                          result = EvaluationResult.new(message, severity: 'success', rule: self)
         
     | 
| 
      
 36 
     | 
    
         
            +
                        end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                        context.add_result result
         
     | 
| 
      
 39 
     | 
    
         
            +
                      end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                      def assess_reachability(resource_id, references)
         
     | 
| 
      
 42 
     | 
    
         
            +
                        makes_resolvable_reference = false
         
     | 
| 
      
 43 
     | 
    
         
            +
                        references.each do |reference|
         
     | 
| 
      
 44 
     | 
    
         
            +
                          type = reference[:type]
         
     | 
| 
      
 45 
     | 
    
         
            +
                          referenced_id = reference[:id]
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                          if type == ''
         
     | 
| 
      
 48 
     | 
    
         
            +
                            if resource_ids.include?(referenced_id)
         
     | 
| 
      
 49 
     | 
    
         
            +
                              makes_resolvable_reference = true
         
     | 
| 
      
 50 
     | 
    
         
            +
                              referenced_resources.add(referenced_id)
         
     | 
| 
      
 51 
     | 
    
         
            +
                            end
         
     | 
| 
      
 52 
     | 
    
         
            +
                          elsif resource_type_ids[type].include?(referenced_id)
         
     | 
| 
      
 53 
     | 
    
         
            +
                            makes_resolvable_reference = true
         
     | 
| 
      
 54 
     | 
    
         
            +
                            referenced_resources.add(referenced_id)
         
     | 
| 
      
 55 
     | 
    
         
            +
                          end
         
     | 
| 
      
 56 
     | 
    
         
            +
                        end
         
     | 
| 
      
 57 
     | 
    
         
            +
                        referencing_resources.add(resource_id) if makes_resolvable_reference
         
     | 
| 
      
 58 
     | 
    
         
            +
                      end
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end
         
     | 
| 
      
 60 
     | 
    
         
            +
                  end
         
     | 
| 
      
 61 
     | 
    
         
            +
                end
         
     | 
| 
      
 62 
     | 
    
         
            +
              end
         
     | 
| 
      
 63 
     | 
    
         
            +
            end
         
     |