pacio_inferno_core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/pacio_inferno_core/custom_groups/capability_statement/conformance_support_test.rb +41 -0
- data/lib/pacio_inferno_core/custom_groups/capability_statement/fhir_version_test.rb +15 -0
- data/lib/pacio_inferno_core/custom_groups/capability_statement/instantiate_test.rb +23 -0
- data/lib/pacio_inferno_core/custom_groups/capability_statement/json_support_test.rb +40 -0
- data/lib/pacio_inferno_core/date_search_validation.rb +112 -0
- data/lib/pacio_inferno_core/fhir_resource_navigation.rb +7 -0
- data/lib/pacio_inferno_core/generator/group_generator.rb +161 -0
- data/lib/pacio_inferno_core/generator/group_metadata.rb +114 -0
- data/lib/pacio_inferno_core/generator/group_metadata_extractor.rb +301 -0
- data/lib/pacio_inferno_core/generator/ig_loader.rb +118 -0
- data/lib/pacio_inferno_core/generator/ig_metadata.rb +60 -0
- data/lib/pacio_inferno_core/generator/ig_metadata_extractor.rb +88 -0
- data/lib/pacio_inferno_core/generator/ig_resources.rb +88 -0
- data/lib/pacio_inferno_core/generator/must_support_metadata_extractor.rb +8 -0
- data/lib/pacio_inferno_core/generator/must_support_test_generator.rb +138 -0
- data/lib/pacio_inferno_core/generator/naming.rb +50 -0
- data/lib/pacio_inferno_core/generator/read_test_generator.rb +102 -0
- data/lib/pacio_inferno_core/generator/reference_resolution_test_generator.rb +96 -0
- data/lib/pacio_inferno_core/generator/search_definition_metadata_extractor.rb +228 -0
- data/lib/pacio_inferno_core/generator/search_metadata_extractor.rb +78 -0
- data/lib/pacio_inferno_core/generator/search_test_generator.rb +298 -0
- data/lib/pacio_inferno_core/generator/special_cases.rb +61 -0
- data/lib/pacio_inferno_core/generator/suite_generator.rb +105 -0
- data/lib/pacio_inferno_core/generator/templates/group.rb.erb +27 -0
- data/lib/pacio_inferno_core/generator/templates/must_support.rb.erb +36 -0
- data/lib/pacio_inferno_core/generator/templates/read.rb.erb +34 -0
- data/lib/pacio_inferno_core/generator/templates/reference_resolution.rb.erb +40 -0
- data/lib/pacio_inferno_core/generator/templates/search.rb.erb +40 -0
- data/lib/pacio_inferno_core/generator/templates/validation.rb.erb +36 -0
- data/lib/pacio_inferno_core/generator/terminology_binding_metadata_extractor.rb +116 -0
- data/lib/pacio_inferno_core/generator/validation_test_generator.rb +146 -0
- data/lib/pacio_inferno_core/generator/value_extractor.rb +152 -0
- data/lib/pacio_inferno_core/generator.rb +130 -0
- data/lib/pacio_inferno_core/must_support_test.rb +20 -0
- data/lib/pacio_inferno_core/primitive_type.rb +5 -0
- data/lib/pacio_inferno_core/read_test.rb +103 -0
- data/lib/pacio_inferno_core/reference_resolution_test.rb +181 -0
- data/lib/pacio_inferno_core/request_logger.rb +46 -0
- data/lib/pacio_inferno_core/resource_search_param_checker.rb +136 -0
- data/lib/pacio_inferno_core/search_test.rb +859 -0
- data/lib/pacio_inferno_core/search_test_properties.rb +56 -0
- data/lib/pacio_inferno_core/validation_test.rb +55 -0
- data/lib/pacio_inferno_core/version.rb +4 -0
- data/lib/pacio_inferno_core/well_known_code_systems.rb +21 -0
- metadata +165 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
module PacioInfernoCore
|
|
2
|
+
class Generator
|
|
3
|
+
class ValueExactor
|
|
4
|
+
attr_accessor :ig_resources, :resource, :profile_elements
|
|
5
|
+
|
|
6
|
+
def initialize(ig_resources, resource, profile_elements)
|
|
7
|
+
self.ig_resources = ig_resources
|
|
8
|
+
self.resource = resource
|
|
9
|
+
self.profile_elements = profile_elements
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def values_from_fixed_codes(profile_element, type)
|
|
13
|
+
return [] unless type == 'CodeableConcept'
|
|
14
|
+
|
|
15
|
+
elements = profile_elements.select do |element|
|
|
16
|
+
element.path == "#{profile_element.path}.coding.code" && element.fixedCode.present?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
elements.map(&:fixedCode)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def values_from_pattern_coding(profile_element, type)
|
|
23
|
+
return [] unless type == 'CodeableConcept'
|
|
24
|
+
|
|
25
|
+
elements = profile_elements.select do |element|
|
|
26
|
+
element.path == "#{profile_element.path}.coding" && element.patternCoding.present?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
elements.map { |element| element.patternCoding.code }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def values_from_pattern_codeable_concept(profile_element, type)
|
|
33
|
+
return [] unless type == 'CodeableConcept'
|
|
34
|
+
|
|
35
|
+
elements = profile_elements.select do |element|
|
|
36
|
+
element.path == profile_element.path && element.patternCodeableConcept.present? && element.min.positive?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
elements.map { |element| element.patternCodeableConcept.coding.first.code }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def value_set_binding(the_element)
|
|
43
|
+
the_element&.binding
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def value_set(the_element)
|
|
47
|
+
binding = value_set_binding(the_element)
|
|
48
|
+
target_valueset = binding&.valueSet
|
|
49
|
+
|
|
50
|
+
additional_binding_ext = binding&.extension&.find do |ext|
|
|
51
|
+
ext.url == 'http://hl7.org/fhir/tools/StructureDefinition/additional-binding' &&
|
|
52
|
+
ext.extension.any? { |sub_ext| sub_ext.url == 'purpose' && sub_ext.valueCode == 'minimum' }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
min_valueset_ext = binding&.extension&.find do |ext|
|
|
56
|
+
ext.url == 'http://hl7.org/fhir/StructureDefinition/elementdefinition-minValueSet'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if additional_binding_ext.present?
|
|
60
|
+
min_valueset = additional_binding_ext.extension.find { |ext| ext.url == 'valueSet' }
|
|
61
|
+
|
|
62
|
+
target_valueset = min_valueset.valueCanonical if min_valueset.present?
|
|
63
|
+
elsif min_valueset_ext.present?
|
|
64
|
+
target_valueset = min_valueset_ext.valueCanonical
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ig_resources.value_set_by_url(target_valueset)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def bound_systems(the_element)
|
|
71
|
+
bound_systems_from_valueset(value_set(the_element))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def bound_systems_from_valueset(value_set)
|
|
75
|
+
value_set&.compose&.include&.map do |include_element|
|
|
76
|
+
if include_element.concept.present?
|
|
77
|
+
include_element
|
|
78
|
+
elsif include_element.system.present? && include_element.filter&.empty?
|
|
79
|
+
# Cannot process intensional value set with filters
|
|
80
|
+
ig_resources.code_system_by_url(include_element.system)
|
|
81
|
+
elsif include_element.valueSet.present?
|
|
82
|
+
include_element.valueSet.map do |vs|
|
|
83
|
+
a_value_set = ig_resources.value_set_by_url(vs)
|
|
84
|
+
bound_systems_from_valueset(a_value_set)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end&.flatten&.compact
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def codes_from_value_set_binding(the_element)
|
|
91
|
+
codes_from_system_code_pair(codings_from_value_set_binding(the_element))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def codes_from_system_code_pair(codings)
|
|
95
|
+
codings.present? ? codings.map { |coding| coding[:code] }.compact.uniq : []
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def codings_from_value_set_binding(the_element)
|
|
99
|
+
return [] if the_element.nil?
|
|
100
|
+
|
|
101
|
+
bound_systems = bound_systems(the_element)
|
|
102
|
+
|
|
103
|
+
return codings_from_bound_systems(bound_systems) if bound_systems.present?
|
|
104
|
+
|
|
105
|
+
expansion_contains = value_set_expansion_contains(the_element)
|
|
106
|
+
|
|
107
|
+
return [] if expansion_contains.blank?
|
|
108
|
+
|
|
109
|
+
expansion_contains.map { |contains| { system: contains.system, code: contains.code } }.compact.uniq
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def codings_from_bound_systems(bound_systems)
|
|
113
|
+
return [] unless bound_systems.present?
|
|
114
|
+
|
|
115
|
+
bound_systems.flat_map do |bound_system|
|
|
116
|
+
case bound_system
|
|
117
|
+
when FHIR::ValueSet::Compose::Include
|
|
118
|
+
bound_system.concept.map { |concept| { system: bound_system.system, code: concept.code } }
|
|
119
|
+
when FHIR::CodeSystem
|
|
120
|
+
bound_system.concept.map { |concept| { system: bound_system.url, code: concept.code } }
|
|
121
|
+
else
|
|
122
|
+
[]
|
|
123
|
+
end
|
|
124
|
+
end.uniq
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def value_set_expansion_contains(element)
|
|
128
|
+
value_set(element)&.expansion&.contains
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def fhir_metadata(current_path)
|
|
132
|
+
FHIR.const_get(resource)::METADATA[current_path]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def values_from_resource_metadata(paths)
|
|
136
|
+
values = []
|
|
137
|
+
|
|
138
|
+
paths.each do |current_path|
|
|
139
|
+
current_metadata = fhir_metadata(current_path)
|
|
140
|
+
|
|
141
|
+
next unless current_metadata&.dig('valid_codes').present?
|
|
142
|
+
|
|
143
|
+
values += current_metadata['valid_codes'].flat_map do |system, codes|
|
|
144
|
+
codes.map { |code| { system:, code: } }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
values
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require 'fhir_models'
|
|
2
|
+
require 'inferno/ext/fhir_models'
|
|
3
|
+
|
|
4
|
+
require_relative 'generator/ig_loader'
|
|
5
|
+
require_relative 'generator/ig_metadata_extractor'
|
|
6
|
+
require_relative 'generator/group_generator'
|
|
7
|
+
require_relative 'generator/must_support_test_generator'
|
|
8
|
+
require_relative 'generator/read_test_generator'
|
|
9
|
+
require_relative 'generator/reference_resolution_test_generator'
|
|
10
|
+
require_relative 'generator/search_test_generator'
|
|
11
|
+
require_relative 'generator/suite_generator'
|
|
12
|
+
require_relative 'generator/validation_test_generator'
|
|
13
|
+
|
|
14
|
+
module PacioInfernoCore
|
|
15
|
+
class Generator
|
|
16
|
+
# def self.generate
|
|
17
|
+
# ig_packages = Dir.glob(File.join(Dir.pwd, 'lib', 'us_core_test_kit', 'igs', '*.tgz'))
|
|
18
|
+
|
|
19
|
+
# ig_packages.each do |ig_package|
|
|
20
|
+
# new(ig_package).generate
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
|
|
24
|
+
attr_accessor :ig_resources, :ig_metadata, :ig_file_name
|
|
25
|
+
|
|
26
|
+
def initialize(ig_file_name)
|
|
27
|
+
self.ig_file_name = ig_file_name
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def generate
|
|
31
|
+
puts "Generating tests for IG #{File.basename(ig_file_name)}"
|
|
32
|
+
load_ig_package
|
|
33
|
+
extract_metadata
|
|
34
|
+
generate_search_tests
|
|
35
|
+
generate_read_tests
|
|
36
|
+
# TODO: generate_vread_tests
|
|
37
|
+
# TODO: generate_history_tests
|
|
38
|
+
generate_provenance_revinclude_search_tests
|
|
39
|
+
generate_validation_tests
|
|
40
|
+
generate_must_support_tests
|
|
41
|
+
generate_reference_resolution_tests
|
|
42
|
+
generate_practitioner_address_tests
|
|
43
|
+
generate_interpreter_required_extension_test_generator
|
|
44
|
+
|
|
45
|
+
generate_granular_scope_tests
|
|
46
|
+
|
|
47
|
+
generate_groups
|
|
48
|
+
|
|
49
|
+
generate_granular_scope_resource_type_groups
|
|
50
|
+
|
|
51
|
+
generate_granular_scope_groups
|
|
52
|
+
|
|
53
|
+
generate_suites
|
|
54
|
+
|
|
55
|
+
write_metadata
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_metadata
|
|
59
|
+
self.ig_metadata = IGMetadataExtractor.new(ig_resources).extract
|
|
60
|
+
|
|
61
|
+
FileUtils.mkdir_p(base_output_dir)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def write_metadata
|
|
65
|
+
File.write(File.join(base_output_dir, 'metadata.yml'), YAML.dump(ig_metadata.to_hash))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def base_output_dir
|
|
69
|
+
File.join(__dir__, 'generated', ig_metadata.ig_version)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def load_ig_package
|
|
73
|
+
FHIR.logger = Logger.new('/dev/null')
|
|
74
|
+
self.ig_resources = IGLoader.new(ig_file_name).load
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def generate_reference_resolution_tests
|
|
78
|
+
ReferenceResolutionTestGenerator.generate(ig_metadata, base_output_dir)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def generate_must_support_tests
|
|
82
|
+
MustSupportTestGenerator.generate(ig_metadata, base_output_dir)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def generate_validation_tests
|
|
86
|
+
ValidationTestGenerator.generate(ig_metadata, base_output_dir)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def generate_read_tests
|
|
90
|
+
ReadTestGenerator.generate(ig_metadata, base_output_dir)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def generate_search_tests
|
|
94
|
+
SearchTestGenerator.generate(ig_metadata, base_output_dir)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def generate_provenance_revinclude_search_tests
|
|
98
|
+
ProvenanceRevincludeSearchTestGenerator.generate(ig_metadata, base_output_dir)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def generate_granular_scope_tests
|
|
102
|
+
GranularScopeTestGenerator.generate(ig_metadata, base_output_dir)
|
|
103
|
+
GranularScopeReadTestGenerator.generate(ig_metadata, base_output_dir)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def generate_groups
|
|
107
|
+
GroupGenerator.generate(ig_metadata, base_output_dir)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def generate_granular_scope_resource_type_groups
|
|
111
|
+
GranularScopeResourceTypeGroupGenerator.generate(ig_metadata, base_output_dir)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def generate_granular_scope_groups
|
|
115
|
+
GranularScopeGroupGenerator.generate(ig_metadata, base_output_dir)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def generate_practitioner_address_tests
|
|
119
|
+
PractitionerAddressTestGenerator.generate(ig_metadata, base_output_dir)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def generate_interpreter_required_extension_test_generator
|
|
123
|
+
InterpreterRequiredExtensionTestGenerator.generate(ig_metadata, base_output_dir)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def generate_suites
|
|
127
|
+
SuiteGenerator.generate(ig_metadata, base_output_dir)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative 'fhir_resource_navigation'
|
|
2
|
+
|
|
3
|
+
module PacioInfernoCore
|
|
4
|
+
module MustSupportTest
|
|
5
|
+
include Inferno::DSL::FHIRResourceNavigation
|
|
6
|
+
extend Forwardable
|
|
7
|
+
|
|
8
|
+
def_delegators 'self.class', :metadata
|
|
9
|
+
|
|
10
|
+
def all_scratch_resources
|
|
11
|
+
scratch_resources[:all]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def perform_must_support_test(resources)
|
|
15
|
+
skip_if resources.blank?, "No #{resource_type} resources were found"
|
|
16
|
+
|
|
17
|
+
skip { assert_must_support_elements_present(resources, nil, metadata:) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module PacioInfernoCore
|
|
2
|
+
module ReadTest
|
|
3
|
+
def all_scratch_resources
|
|
4
|
+
scratch_resources[:all] ||= []
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def perform_read_test(resources, _reply_handler = nil, delayed_reference: false)
|
|
8
|
+
skip_if resources.blank?, no_resources_skip_message
|
|
9
|
+
|
|
10
|
+
resources_to_read = if delayed_reference
|
|
11
|
+
readable_references(resources)
|
|
12
|
+
else
|
|
13
|
+
readable_resources(resources)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
assert resources_to_read.present?, "No #{resource_type} id found."
|
|
17
|
+
|
|
18
|
+
if config.options[:read_all_resources]
|
|
19
|
+
if delayed_reference
|
|
20
|
+
all_referencing_resources = referencing_resources(resources_to_read)
|
|
21
|
+
info %(
|
|
22
|
+
The #{resource_type} references used for this test were pulled from the following resources:
|
|
23
|
+
#{all_referencing_resources}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
resources_to_read.map! { |resource| resource[:reference] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
resources_to_read.each do |resource|
|
|
30
|
+
read_and_validate(resource)
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
first_resource = resources_to_read.first
|
|
34
|
+
if delayed_reference.present?
|
|
35
|
+
info %(
|
|
36
|
+
The #{resource_type} reference used for this test was pulled from resource
|
|
37
|
+
#{first_resource[:referencing_resource]}
|
|
38
|
+
)
|
|
39
|
+
first_resource = first_resource[:reference]
|
|
40
|
+
end
|
|
41
|
+
read_and_validate(first_resource)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def referencing_resources(readable_resources)
|
|
46
|
+
readable_resources
|
|
47
|
+
.map { |resource| resource[:referencing_resource] }
|
|
48
|
+
.join(', ')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def readable_references(resources)
|
|
52
|
+
resources
|
|
53
|
+
.filter_map do |resource|
|
|
54
|
+
next unless resource[:reference].present? && resource[:reference].is_a?(FHIR::Reference)
|
|
55
|
+
|
|
56
|
+
reference_id = resource[:reference].reference&.split('/')&.last
|
|
57
|
+
next unless reference_id&.present?
|
|
58
|
+
|
|
59
|
+
resource
|
|
60
|
+
end
|
|
61
|
+
.uniq { |resource| resource[:reference].reference.split('/').last }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def readable_resources(resources)
|
|
65
|
+
resources
|
|
66
|
+
.select { |resource| resource.is_a?(resource_class) || resource.is_a?(FHIR::Reference) }
|
|
67
|
+
.select { |resource| (resource.is_a?(FHIR::Reference) ? resource.reference.split('/').last : resource.id).present? }
|
|
68
|
+
.compact
|
|
69
|
+
.uniq { |resource| resource.is_a?(FHIR::Reference) ? resource.reference.split('/').last : resource.id }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def read_and_validate(resource_to_read)
|
|
73
|
+
id = resource_id(resource_to_read)
|
|
74
|
+
|
|
75
|
+
fhir_read resource_type, id
|
|
76
|
+
|
|
77
|
+
assert_response_status(200)
|
|
78
|
+
assert_resource_type(resource_type)
|
|
79
|
+
assert resource.id.present? && resource.id == id, bad_resource_id_message(id)
|
|
80
|
+
|
|
81
|
+
return unless resource_to_read.is_a? FHIR::Reference
|
|
82
|
+
|
|
83
|
+
all_scratch_resources << resource
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def resource_id(resource)
|
|
87
|
+
resource.is_a?(FHIR::Reference) ? resource.reference.split('/').last : resource.id
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def no_resources_skip_message
|
|
91
|
+
"No #{resource_type} resources appear to be available. " \
|
|
92
|
+
'Please use patients with more information.'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def bad_resource_id_message(expected_id)
|
|
96
|
+
"Expected resource to have id: `#{expected_id.inspect}`, but found `#{resource.id.inspect}`"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resource_class
|
|
100
|
+
FHIR.const_get(resource_type)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
require_relative 'fhir_resource_navigation'
|
|
2
|
+
|
|
3
|
+
module PacioInfernoCore
|
|
4
|
+
module ReferenceResolutionTest
|
|
5
|
+
include Inferno::DSL::FHIRResourceNavigation
|
|
6
|
+
extend Forwardable
|
|
7
|
+
|
|
8
|
+
def_delegators 'self.class', :metadata
|
|
9
|
+
|
|
10
|
+
def perform_reference_resolution_test(resources)
|
|
11
|
+
skip_if resources.blank?, no_resources_skip_message
|
|
12
|
+
|
|
13
|
+
pass if unresolved_references(resources).length.zero?
|
|
14
|
+
|
|
15
|
+
skip "Could not resolve and validate any Must Support references for #{unresolved_references_strings.join(', ')}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unresolved_references_strings
|
|
19
|
+
unresolved_reference_hash =
|
|
20
|
+
unresolved_references.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |missing, hash|
|
|
21
|
+
hash[missing[:path]] << missing[:target_profile]
|
|
22
|
+
end
|
|
23
|
+
unresolved_reference_hash.map do |path, profiles|
|
|
24
|
+
"#{path} element: Reference#{"(#{profiles.join('|')})" unless profiles.first.empty?}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record_resolved_reference(reference, target_profile)
|
|
29
|
+
saved_reference = resolved_references.find { |item| item[:reference] == reference.reference }
|
|
30
|
+
|
|
31
|
+
if saved_reference.present?
|
|
32
|
+
if target_profile.present? && !saved_reference[:profiles].include?(target_profile)
|
|
33
|
+
saved_reference[:profiles] << target_profile
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
saved_reference = {
|
|
37
|
+
reference: reference.reference,
|
|
38
|
+
profiles: []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
saved_reference[:profiles] << target_profile if target_profile.present?
|
|
42
|
+
resolved_references.add(saved_reference)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def is_reference_resolved?(reference, target_profile)
|
|
47
|
+
resolved_references.any? do |item|
|
|
48
|
+
item[:reference] == reference.reference &&
|
|
49
|
+
(
|
|
50
|
+
target_profile.blank? || item[:profiles].include?(target_profile)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolved_references
|
|
56
|
+
scratch[:resolved_references] ||= Set.new
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def no_resources_skip_message
|
|
60
|
+
"No #{resource_type} resources appear to be available. " \
|
|
61
|
+
'Please use patients with more information.'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def must_support_references
|
|
65
|
+
metadata.must_supports[:elements].select do |element_definition|
|
|
66
|
+
element_definition[:types]&.include?('Reference')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def must_support_references_with_target_profile
|
|
71
|
+
# mapping array of target_profiles to array of {path, target_profile} pair
|
|
72
|
+
must_support_references.map do |element_definition|
|
|
73
|
+
(element_definition[:target_profiles] || ['']).map do |target_profile|
|
|
74
|
+
{
|
|
75
|
+
path: element_definition[:path],
|
|
76
|
+
target_profile: target_profile
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end.flatten
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def unresolved_references(resources = [])
|
|
83
|
+
@unresolved_references ||=
|
|
84
|
+
must_support_references_with_target_profile.select do |reference_path_profile_pair|
|
|
85
|
+
path = reference_path_profile_pair[:path]
|
|
86
|
+
target_profile = reference_path_profile_pair[:target_profile]
|
|
87
|
+
|
|
88
|
+
found_one_reference = false
|
|
89
|
+
|
|
90
|
+
resolve_one_reference = resources.any? do |resource|
|
|
91
|
+
value_found = resolve_path(resource, path)
|
|
92
|
+
next if value_found.empty?
|
|
93
|
+
|
|
94
|
+
found_one_reference = true
|
|
95
|
+
|
|
96
|
+
value_found.any? do |reference|
|
|
97
|
+
validate_reference_resolution(resource, reference, target_profile)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
found_one_reference && !resolve_one_reference
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if metadata.must_supports[:choices].present?
|
|
105
|
+
@unresolved_references.delete_if do |reference|
|
|
106
|
+
choice_profiles = metadata.must_supports[:choices].find do |choice|
|
|
107
|
+
choice[:target_profiles]&.include?(reference[:target_profile])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
choice_profiles.present? &&
|
|
111
|
+
choice_profiles[:target_profiles]&.any? do |profile|
|
|
112
|
+
@unresolved_references.none? do |element|
|
|
113
|
+
element[:target_profile] == profile
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
@unresolved_references
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate_reference_resolution(resource, reference, target_profile)
|
|
123
|
+
return true if is_reference_resolved?(reference, target_profile)
|
|
124
|
+
|
|
125
|
+
if reference.contained?
|
|
126
|
+
# if reference_id is blank it is referring to itself, so we know it exists
|
|
127
|
+
return true if reference.reference_id.blank?
|
|
128
|
+
|
|
129
|
+
return resource.contained.any? do |contained_resource|
|
|
130
|
+
contained_resource&.id == reference.reference_id &&
|
|
131
|
+
resource_is_valid_with_target_profile?(contained_resource, target_profile)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
reference_type = reference.resource_type
|
|
136
|
+
reference_id = reference.reference_id
|
|
137
|
+
|
|
138
|
+
resolved_resource = resolve_reference(reference)
|
|
139
|
+
|
|
140
|
+
return false if resolved_resource.nil?
|
|
141
|
+
|
|
142
|
+
return false unless resolved_resource.resourceType == reference_type && resolved_resource.id == reference_id
|
|
143
|
+
|
|
144
|
+
return false unless resource_is_valid_with_target_profile?(resolved_resource, target_profile)
|
|
145
|
+
|
|
146
|
+
record_resolved_reference(reference, target_profile)
|
|
147
|
+
true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def resolve_reference(reference)
|
|
151
|
+
reference_type = reference.resource_type
|
|
152
|
+
reference_id = reference.reference_id
|
|
153
|
+
|
|
154
|
+
begin
|
|
155
|
+
if reference.relative?
|
|
156
|
+
begin
|
|
157
|
+
reference.resource_class
|
|
158
|
+
rescue NameError
|
|
159
|
+
return nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
fhir_read(reference_type, reference_id)&.resource
|
|
163
|
+
elsif reference.base_uri.chomp('/') == fhir_client.instance_variable_get(:@base_service_url).chomp('/')
|
|
164
|
+
fhir_read(reference_type, reference_id)&.resource
|
|
165
|
+
else
|
|
166
|
+
get(reference.reference)&.resource
|
|
167
|
+
end
|
|
168
|
+
rescue StandardError => e
|
|
169
|
+
Inferno::Application['logger'].error("Unable to resolve reference #{reference.reference}")
|
|
170
|
+
Inferno::Application['logger'].error(e.full_message)
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def resource_is_valid_with_target_profile?(resource, target_profile)
|
|
176
|
+
return true if target_profile.blank?
|
|
177
|
+
|
|
178
|
+
resource_is_valid?(resource:, profile_url: target_profile, add_messages_to_runnable: false)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Inferno
|
|
2
|
+
module Utils
|
|
3
|
+
# @private
|
|
4
|
+
module Middleware
|
|
5
|
+
class RequestLogger
|
|
6
|
+
def log_response(response, start_time, end_time, exception = nil)
|
|
7
|
+
elapsed = end_time - start_time
|
|
8
|
+
status, _response_headers, body = response if response
|
|
9
|
+
status, = response if exception
|
|
10
|
+
|
|
11
|
+
logger.info("#{status} in #{elapsed.in_milliseconds} ms")
|
|
12
|
+
return unless body.present?
|
|
13
|
+
|
|
14
|
+
body = body.join if body.is_a?(Array)
|
|
15
|
+
|
|
16
|
+
if body.length > 100
|
|
17
|
+
logger.info("#{body[0..100]}...")
|
|
18
|
+
else
|
|
19
|
+
logger.info(body)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def log_request(env)
|
|
24
|
+
method = env['REQUEST_METHOD']
|
|
25
|
+
scheme = env['rack.url_scheme']
|
|
26
|
+
host = env['HTTP_HOST']
|
|
27
|
+
path = env['REQUEST_URI']
|
|
28
|
+
query = env['rack.request.query_string']
|
|
29
|
+
body = env['rack.input']
|
|
30
|
+
body = body.instance_of?(Puma::NullIO) ? nil : body.string
|
|
31
|
+
query_string = query.blank? ? '' : "?#{query}"
|
|
32
|
+
|
|
33
|
+
logger.info("#{method} #{scheme}://#{host}#{path}#{query_string}")
|
|
34
|
+
|
|
35
|
+
return unless body.present?
|
|
36
|
+
|
|
37
|
+
if body.length > 100
|
|
38
|
+
logger.info("#{body[0..100]}...")
|
|
39
|
+
else
|
|
40
|
+
logger.info(body)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|