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,114 @@
|
|
|
1
|
+
require_relative 'special_cases'
|
|
2
|
+
|
|
3
|
+
module PacioInfernoCore
|
|
4
|
+
class Generator
|
|
5
|
+
class GroupMetadata
|
|
6
|
+
ATTRIBUTES = %i[
|
|
7
|
+
name
|
|
8
|
+
class_name
|
|
9
|
+
version
|
|
10
|
+
reformatted_version
|
|
11
|
+
resource
|
|
12
|
+
profile_url
|
|
13
|
+
profile_name
|
|
14
|
+
profile_version
|
|
15
|
+
title
|
|
16
|
+
short_description
|
|
17
|
+
is_delayed
|
|
18
|
+
interactions
|
|
19
|
+
operations
|
|
20
|
+
searches
|
|
21
|
+
search_definitions
|
|
22
|
+
include_params
|
|
23
|
+
revincludes
|
|
24
|
+
required_concepts
|
|
25
|
+
must_supports
|
|
26
|
+
mandatory_elements
|
|
27
|
+
bindings
|
|
28
|
+
references
|
|
29
|
+
tests
|
|
30
|
+
granular_scope_tests
|
|
31
|
+
id
|
|
32
|
+
file_name
|
|
33
|
+
delayed_references
|
|
34
|
+
resource_conformance_expectation
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
ATTRIBUTES.each { |name| attr_accessor name }
|
|
38
|
+
|
|
39
|
+
def initialize(metadata)
|
|
40
|
+
metadata.each do |key, value|
|
|
41
|
+
raise "Unknown attribute #{key}" unless ATTRIBUTES.include? key
|
|
42
|
+
|
|
43
|
+
instance_variable_set(:"@#{key}", value)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delayed?
|
|
48
|
+
@is_delayed ||= if resource == 'Patient'
|
|
49
|
+
false
|
|
50
|
+
else
|
|
51
|
+
no_patient_searches?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def exclude_search_tests?
|
|
56
|
+
delayed? && !searchable_delayed_resource?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def no_patient_searches?
|
|
60
|
+
searches.none? { |search| search[:names].include?('patient') && search[:expectation] == 'SHALL' }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def optional_profile?
|
|
64
|
+
SpecialCases::OPTIONAL_PROFILES.key?(profile_url) && SpecialCases::OPTIONAL_PROFILES[profile_url].include?(reformatted_version)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def searchable_delayed_resource?
|
|
68
|
+
SpecialCases::SEARCHABLE_DELAYED_RESOURCES.key?(resource) && SpecialCases::SEARCHABLE_DELAYED_RESOURCES[resource].include?(reformatted_version)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_test(id:, file_name:)
|
|
72
|
+
self.tests ||= []
|
|
73
|
+
|
|
74
|
+
test_metadata = {
|
|
75
|
+
id: id,
|
|
76
|
+
file_name: file_name
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if delayed? && id.include?('read')
|
|
80
|
+
self.tests.unshift(test_metadata)
|
|
81
|
+
else
|
|
82
|
+
self.tests << test_metadata
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def add_granular_scope_test(id:, file_name:)
|
|
87
|
+
self.granular_scope_tests ||= []
|
|
88
|
+
|
|
89
|
+
self.granular_scope_tests << {
|
|
90
|
+
id:,
|
|
91
|
+
file_name:
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_hash
|
|
96
|
+
ATTRIBUTES.each_with_object({}) { |key, hash| hash[key] = send(key) unless send(key).nil? }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def add_delayed_references(delayed_profiles, ig_resources)
|
|
100
|
+
self.delayed_references =
|
|
101
|
+
references
|
|
102
|
+
.select { |reference| (reference[:profiles] & delayed_profiles).present? }
|
|
103
|
+
.map do |reference|
|
|
104
|
+
profile_urls = (reference[:profiles] & delayed_profiles)
|
|
105
|
+
delayed_resources = profile_urls.map { |url| ig_resources.resource_for_profile(url) }
|
|
106
|
+
{
|
|
107
|
+
path: reference[:path].gsub("#{resource}.", ''),
|
|
108
|
+
resources: delayed_resources
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
require_relative 'group_metadata'
|
|
2
|
+
require_relative 'ig_metadata'
|
|
3
|
+
require_relative 'must_support_metadata_extractor'
|
|
4
|
+
require_relative 'search_metadata_extractor'
|
|
5
|
+
require_relative 'terminology_binding_metadata_extractor'
|
|
6
|
+
require_relative 'naming'
|
|
7
|
+
|
|
8
|
+
module PacioInfernoCore
|
|
9
|
+
class Generator
|
|
10
|
+
class GroupMetadataExtractor
|
|
11
|
+
attr_accessor :resource_capabilities, :profile_url, :ig_metadata, :ig_resources
|
|
12
|
+
|
|
13
|
+
def initialize(resource_capabilities, profile_url, ig_metadata, ig_resources)
|
|
14
|
+
self.resource_capabilities = resource_capabilities
|
|
15
|
+
self.profile_url = profile_url
|
|
16
|
+
self.ig_metadata = ig_metadata
|
|
17
|
+
self.ig_resources = ig_resources
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def group_metadata
|
|
21
|
+
@group_metadata ||=
|
|
22
|
+
GroupMetadata.new(group_metadata_hash)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def group_metadata_hash
|
|
26
|
+
@group_metadata_hash ||=
|
|
27
|
+
{
|
|
28
|
+
name: name,
|
|
29
|
+
class_name: class_name,
|
|
30
|
+
version: version,
|
|
31
|
+
reformatted_version: reformatted_version,
|
|
32
|
+
# test_id_prefix: test_id_prefix,
|
|
33
|
+
resource: resource,
|
|
34
|
+
profile_url: profile_url,
|
|
35
|
+
profile_name: profile_name,
|
|
36
|
+
profile_version: profile_version,
|
|
37
|
+
title: title,
|
|
38
|
+
short_description: short_description,
|
|
39
|
+
interactions: interactions,
|
|
40
|
+
operations: operations,
|
|
41
|
+
searches: searches,
|
|
42
|
+
search_definitions: search_definitions,
|
|
43
|
+
include_params: include_params,
|
|
44
|
+
revincludes: revincludes,
|
|
45
|
+
required_concepts: required_concepts,
|
|
46
|
+
must_supports: must_supports,
|
|
47
|
+
mandatory_elements: mandatory_elements,
|
|
48
|
+
bindings: bindings,
|
|
49
|
+
references: references,
|
|
50
|
+
resource_conformance_expectation: resource_conformance_expectation
|
|
51
|
+
# tests: []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
mark_mandatory_and_must_support_searches
|
|
55
|
+
handle_special_cases
|
|
56
|
+
|
|
57
|
+
@group_metadata_hash
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def mark_mandatory_and_must_support_searches
|
|
61
|
+
searches.each do |search|
|
|
62
|
+
search[:names_not_must_support_or_mandatory] = search[:names].reject do |name|
|
|
63
|
+
full_paths = search_definitions[name.to_sym][:full_paths]
|
|
64
|
+
any_must_support_elements = must_supports[:elements].any? do |element|
|
|
65
|
+
full_must_support_paths = ["#{resource}.#{element[:original_path]}", "#{resource}.#{element[:path]}"]
|
|
66
|
+
|
|
67
|
+
full_paths.any? do |path|
|
|
68
|
+
# allow for non-choice, choice types, and _id
|
|
69
|
+
name == '_id' || full_must_support_paths.include?(path) || full_must_support_paths.include?("#{path}[x]")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
any_must_support_slices = must_supports[:slices].any? do |slice|
|
|
74
|
+
# only handle type slices because that is all we need for now
|
|
75
|
+
# for a slice like Observation.effective[x]:effectiveDateTime, the search parameter's expression could be
|
|
76
|
+
# either Observation.effective or Observation.effectiveDateTime.
|
|
77
|
+
if slice[:discriminator] && slice[:discriminator][:type] == 'type'
|
|
78
|
+
full_must_support_path = "#{resource}.#{slice[:path].sub('[x]', slice[:discriminator][:code])}"
|
|
79
|
+
base_must_support_path = "#{resource}.#{slice[:path].sub('[x]', '')}"
|
|
80
|
+
|
|
81
|
+
full_paths.intersection([full_must_support_path, base_must_support_path]).present?
|
|
82
|
+
else
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
any_mandatory_elements = mandatory_elements.any? do |element|
|
|
88
|
+
full_paths.include?(element)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
any_must_support_elements || any_must_support_slices || any_mandatory_elements
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
search[:must_support_or_mandatory] = search[:names_not_must_support_or_mandatory].empty?
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
### BEGIN SPECIAL CASES ###
|
|
99
|
+
def category_first_profile?
|
|
100
|
+
SpecialCases::ALL_VERSION_CATEGORY_FIRST_PROFILES.include?(profile_url) ||
|
|
101
|
+
SpecialCases::VERSION_SPECIFIC_CATEGORY_FIRST_PROFILES[profile_url]&.include?(reformatted_version)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def first_search_params
|
|
105
|
+
@first_search_params ||=
|
|
106
|
+
if category_first_profile?
|
|
107
|
+
%w[patient category]
|
|
108
|
+
elsif resource == 'Observation'
|
|
109
|
+
%w[patient code]
|
|
110
|
+
elsif resource == 'MedicationRequest'
|
|
111
|
+
%w[patient intent]
|
|
112
|
+
elsif resource == 'CareTeam'
|
|
113
|
+
%w[patient status]
|
|
114
|
+
else
|
|
115
|
+
['patient']
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def handle_special_cases
|
|
120
|
+
set_first_search
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def set_first_search
|
|
124
|
+
search = searches.find { |param| param[:names] == first_search_params }
|
|
125
|
+
return if search.nil?
|
|
126
|
+
|
|
127
|
+
searches.delete(search)
|
|
128
|
+
searches.unshift(search)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
### END SPECIAL CASES ###
|
|
132
|
+
|
|
133
|
+
def profile
|
|
134
|
+
@profile ||= ig_resources.profile_by_url(profile_url)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def profile_elements
|
|
138
|
+
@profile_elements ||= profile.snapshot.element
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def base_name
|
|
142
|
+
profile_url.split('StructureDefinition/').last
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def name
|
|
146
|
+
base_name.tr('-', '_')
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def class_name
|
|
150
|
+
base_name
|
|
151
|
+
.split('-')
|
|
152
|
+
.map(&:capitalize)
|
|
153
|
+
.join
|
|
154
|
+
.gsub('UsCore', "USCore#{ig_metadata.reformatted_version}")
|
|
155
|
+
.concat('Sequence')
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def version
|
|
159
|
+
ig_metadata.ig_version
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def reformatted_version
|
|
163
|
+
ig_metadata.reformatted_version
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def resource
|
|
167
|
+
resource_capabilities.type
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def profile_name
|
|
171
|
+
profile.title.gsub(' ', ' ')
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def profile_version
|
|
175
|
+
profile.version
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def title
|
|
179
|
+
title = profile.title.gsub(/US\s*Core\s*/, '').gsub(/\s*Profile/, '').strip
|
|
180
|
+
|
|
181
|
+
if Naming.resources_with_multiple_profiles.include?(resource) && !title.start_with?(resource) && version != 'v3.1.1'
|
|
182
|
+
title = resource + ' ' + title.split(resource).map(&:strip).join(' ')
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
title
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def short_description
|
|
189
|
+
"Verify support for the server capabilities required by the #{profile_name}."
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def interactions
|
|
193
|
+
@interactions ||=
|
|
194
|
+
resource_capabilities.interaction.map do |interaction|
|
|
195
|
+
{
|
|
196
|
+
code: interaction.code,
|
|
197
|
+
expectation: interaction.extension.first.valueCode # TODO: fix expectation extension finding
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def operations
|
|
203
|
+
@operations ||=
|
|
204
|
+
resource_capabilities.operation.map do |operation|
|
|
205
|
+
{
|
|
206
|
+
code: operation.name,
|
|
207
|
+
expectation: operation.extension.first.valueCode # TODO: fix expectation extension finding
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def search_metadata_extractor
|
|
213
|
+
@search_metadata_extractor ||= SearchMetadataExtractor.new(
|
|
214
|
+
resource_capabilities,
|
|
215
|
+
ig_resources,
|
|
216
|
+
profile_elements,
|
|
217
|
+
{
|
|
218
|
+
resource: resource,
|
|
219
|
+
profile_url: profile_url,
|
|
220
|
+
must_supports: must_supports
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def searches
|
|
226
|
+
@searches ||= search_metadata_extractor.searches
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def search_definitions
|
|
230
|
+
@search_definitions ||= search_metadata_extractor.search_definitions
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def include_params
|
|
234
|
+
resource_capabilities.searchInclude || []
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def revincludes
|
|
238
|
+
resource_capabilities.searchRevInclude || []
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def required_concepts
|
|
242
|
+
# The base FHIR vital signs profile has a required binding that isn't
|
|
243
|
+
# relevant for any of its child profiles
|
|
244
|
+
return [] if resource == 'Observation'
|
|
245
|
+
|
|
246
|
+
profile_elements
|
|
247
|
+
.select { |element| element.type&.any? { |type| type.code == 'CodeableConcept' } }
|
|
248
|
+
.select { |element| element.binding&.strength == 'required' }
|
|
249
|
+
.map { |element| element.path.gsub("#{resource}.", '').gsub('[x]', 'CodeableConcept') }
|
|
250
|
+
.uniq
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def terminology_binding_metadata_extractor
|
|
254
|
+
@terminology_binding_metadata_extractor ||=
|
|
255
|
+
TerminologyBindingMetadataExtractor.new(profile_elements, ig_resources, resource)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def bindings
|
|
259
|
+
@bindings ||=
|
|
260
|
+
terminology_binding_metadata_extractor.terminology_bindings
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def must_support_metadata_extractor
|
|
264
|
+
@must_support_metadata_extractor ||=
|
|
265
|
+
MustSupportMetadataExtractor.new(profile_elements, profile, resource, ig_resources)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def must_supports
|
|
269
|
+
@must_supports ||=
|
|
270
|
+
must_support_metadata_extractor.must_supports
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def mandatory_elements
|
|
274
|
+
@mandatory_elements ||=
|
|
275
|
+
profile_elements
|
|
276
|
+
.select { |element| element.min.positive? }
|
|
277
|
+
.map { |element| element.path }
|
|
278
|
+
.uniq
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def references
|
|
282
|
+
@references ||=
|
|
283
|
+
profile_elements
|
|
284
|
+
.select { |element| element.type&.first&.code == 'Reference' }
|
|
285
|
+
.map do |reference_definition|
|
|
286
|
+
{
|
|
287
|
+
path: reference_definition.path,
|
|
288
|
+
profiles: reference_definition.type.first.targetProfile
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def resource_conformance_expectation
|
|
294
|
+
@resource_conformance_expectation ||=
|
|
295
|
+
resource_capabilities.extension.find do |extension|
|
|
296
|
+
extension.url == 'http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation'
|
|
297
|
+
end&.valueCode
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require 'active_support/all'
|
|
2
|
+
require 'fhir_models'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'rubygems/package'
|
|
5
|
+
require 'zlib'
|
|
6
|
+
require_relative 'ig_resources'
|
|
7
|
+
|
|
8
|
+
module PacioInfernoCore
|
|
9
|
+
class Generator
|
|
10
|
+
class IGLoader
|
|
11
|
+
attr_accessor :ig_file_name
|
|
12
|
+
|
|
13
|
+
def initialize(ig_file_name)
|
|
14
|
+
self.ig_file_name = ig_file_name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ig_resources
|
|
18
|
+
@ig_resources ||= IGResources.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load
|
|
22
|
+
load_ig
|
|
23
|
+
load_standalone_resources
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def load_ig
|
|
27
|
+
tar = Gem::Package::TarReader.new(
|
|
28
|
+
Zlib::GzipReader.open(ig_file_name)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
tar.each do |entry|
|
|
32
|
+
next if entry.directory?
|
|
33
|
+
|
|
34
|
+
file_name = entry.full_name.split('/').last
|
|
35
|
+
|
|
36
|
+
next if file_name.end_with? 'openapi.json'
|
|
37
|
+
|
|
38
|
+
next unless file_name.end_with? '.json'
|
|
39
|
+
|
|
40
|
+
next unless entry.full_name.start_with? 'package/'
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
resource = FHIR.from_contents(entry.read)
|
|
44
|
+
next if resource.nil?
|
|
45
|
+
rescue StandardError
|
|
46
|
+
puts "#{file_name} does not appear to be a FHIR resource."
|
|
47
|
+
next
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
ig_resources.add(resource)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
ig_resources
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_standalone_resources
|
|
57
|
+
ig_directory = ig_file_name.chomp('.tgz')
|
|
58
|
+
|
|
59
|
+
return ig_resources unless File.exist? ig_directory
|
|
60
|
+
|
|
61
|
+
Dir.glob(File.join(ig_directory, '*.{json,tgz}')).each do |file_path|
|
|
62
|
+
if file_path.end_with? '.tgz'
|
|
63
|
+
load_tgz_resources(file_path)
|
|
64
|
+
else
|
|
65
|
+
load_json_resource(file_path)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
ig_resources
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def load_tgz_resources(file_path)
|
|
75
|
+
tar = Gem::Package::TarReader.new(
|
|
76
|
+
Zlib::GzipReader.open(file_path)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
tar.each do |entry|
|
|
80
|
+
next if entry.directory?
|
|
81
|
+
|
|
82
|
+
file_name = entry.full_name.split('/').last
|
|
83
|
+
|
|
84
|
+
next if file_name.end_with? 'openapi.json'
|
|
85
|
+
|
|
86
|
+
next if file_name.start_with? 'CapabilityStatement'
|
|
87
|
+
|
|
88
|
+
next unless file_name.end_with? '.json'
|
|
89
|
+
|
|
90
|
+
next unless entry.full_name.start_with? 'package/'
|
|
91
|
+
|
|
92
|
+
# exclude examples from FHIR R4
|
|
93
|
+
next if file_name.end_with? 'example.json'
|
|
94
|
+
next if file_name.end_with? 'example-reference.json'
|
|
95
|
+
next if file_name.end_with? 'example-extension.json'
|
|
96
|
+
|
|
97
|
+
load_resource(entry.read, file_name)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def load_json_resource(file_path)
|
|
102
|
+
load_resource(File.read(file_path), file_path.split('/').last)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def load_resource(contents, file_name)
|
|
106
|
+
begin
|
|
107
|
+
resource = FHIR.from_contents(contents)
|
|
108
|
+
return if resource.nil?
|
|
109
|
+
rescue StandardError
|
|
110
|
+
puts "#{file_name} does not appear to be a FHIR resource."
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
ig_resources.add(resource)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module PacioInfernoCore
|
|
2
|
+
class Generator
|
|
3
|
+
class IGMetadata
|
|
4
|
+
attr_accessor :ig_version, :groups, :ig_package_id
|
|
5
|
+
|
|
6
|
+
def reformatted_version
|
|
7
|
+
@reformatted_version ||= ig_version.delete('.').gsub('-', '_')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def ordered_groups
|
|
11
|
+
@ordered_groups ||=
|
|
12
|
+
[patient_group] + non_delayed_groups + delayed_groups
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def patient_group
|
|
16
|
+
@patient_group ||=
|
|
17
|
+
groups.find { |group| group.resource == 'Patient' }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def delayed_groups
|
|
21
|
+
@delayed_groups ||=
|
|
22
|
+
groups.select { |group| group.delayed? }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def non_delayed_groups
|
|
26
|
+
@non_delayed_groups ||=
|
|
27
|
+
groups.reject { |group| group.delayed? } - [patient_group]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def delayed_profiles
|
|
31
|
+
@delayed_profiles ||=
|
|
32
|
+
delayed_groups.map(&:profile_url)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def postprocess_groups(ig_resources)
|
|
36
|
+
groups.each do |group|
|
|
37
|
+
group.add_delayed_references(delayed_profiles, ig_resources)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def granular_scope_resource_type_groups
|
|
42
|
+
@granular_scope_resource_type_groups ||=
|
|
43
|
+
Hash.new { |hash, key| hash[key] = [] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def granular_scope_groups
|
|
47
|
+
@granular_scope_groups ||= []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_hash
|
|
51
|
+
{
|
|
52
|
+
ig_version:,
|
|
53
|
+
groups: groups.map(&:to_hash),
|
|
54
|
+
granular_scope_resource_type_groups:,
|
|
55
|
+
granular_scope_groups:
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require_relative 'ig_metadata'
|
|
2
|
+
require_relative 'group_metadata_extractor'
|
|
3
|
+
|
|
4
|
+
module PacioInfernoCore
|
|
5
|
+
class Generator
|
|
6
|
+
class IGMetadataExtractor
|
|
7
|
+
attr_accessor :ig_resources, :metadata
|
|
8
|
+
|
|
9
|
+
def initialize(ig_resources)
|
|
10
|
+
self.ig_resources = ig_resources
|
|
11
|
+
add_missing_supported_profiles
|
|
12
|
+
remove_version_from_supported_profiles
|
|
13
|
+
remove_extra_supported_profiles
|
|
14
|
+
self.metadata = IGMetadata.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def extract
|
|
18
|
+
add_metadata_from_ig
|
|
19
|
+
add_metadata_from_resources
|
|
20
|
+
metadata
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_metadata_from_ig
|
|
24
|
+
metadata.ig_version = "v#{ig_resources.ig.version}".delete('-ballot')
|
|
25
|
+
metadata.ig_package_id = ig_resources.ig.packageId
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resources_in_capability_statement
|
|
29
|
+
ig_resources.capability_statement.rest.first.resource
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_missing_supported_profiles
|
|
33
|
+
case ig_resources.ig.version.delete('-ballot')
|
|
34
|
+
when '3.1.1'
|
|
35
|
+
# TODO: Remove these after v8.0.0 IG is fixed
|
|
36
|
+
|
|
37
|
+
# The US Core v3.1.1 Server Capability Statement does not list support for the
|
|
38
|
+
# required vital signs profiles, so they need to be added
|
|
39
|
+
ig_resources.capability_statement.rest.first.resource
|
|
40
|
+
.find { |resource| resource.type == 'Observation' }
|
|
41
|
+
.supportedProfile.concat [
|
|
42
|
+
'http://hl7.org/fhir/StructureDefinition/bodyheight',
|
|
43
|
+
'http://hl7.org/fhir/StructureDefinition/bodytemp',
|
|
44
|
+
'http://hl7.org/fhir/StructureDefinition/bp',
|
|
45
|
+
'http://hl7.org/fhir/StructureDefinition/bodyweight',
|
|
46
|
+
'http://hl7.org/fhir/StructureDefinition/heartrate',
|
|
47
|
+
'http://hl7.org/fhir/StructureDefinition/resprate'
|
|
48
|
+
]
|
|
49
|
+
when '5.0.1'
|
|
50
|
+
# The US Core v5.0.1 Server Capability Statement does not have supported-profile for Encounter
|
|
51
|
+
ig_resources.capability_statement.rest.first.resource
|
|
52
|
+
.find { |resource| resource.type == 'Encounter' }
|
|
53
|
+
.supportedProfile.concat [
|
|
54
|
+
'http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter'
|
|
55
|
+
]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def remove_version_from_supported_profiles
|
|
60
|
+
resources_in_capability_statement.each do |resource|
|
|
61
|
+
resource.supportedProfile.map! { |profile_url| profile_url.split('|').first }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def remove_extra_supported_profiles
|
|
66
|
+
ig_resources.capability_statement.rest.first.resource
|
|
67
|
+
.find { |resource| resource.type == 'Observation' }
|
|
68
|
+
.supportedProfile.delete_if do |profile_url|
|
|
69
|
+
SpecialCases::PROFILES_TO_EXCLUDE.include?(profile_url)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def add_metadata_from_resources
|
|
74
|
+
metadata.groups =
|
|
75
|
+
resources_in_capability_statement.flat_map do |resource|
|
|
76
|
+
resource.supportedProfile&.map do |supported_profile|
|
|
77
|
+
# supported_profile = supported_profile.split('|').first
|
|
78
|
+
next if supported_profile == 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire'
|
|
79
|
+
|
|
80
|
+
GroupMetadataExtractor.new(resource, supported_profile, metadata, ig_resources).group_metadata
|
|
81
|
+
end
|
|
82
|
+
end.compact
|
|
83
|
+
|
|
84
|
+
metadata.postprocess_groups(ig_resources)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|