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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/pacio_inferno_core/custom_groups/capability_statement/conformance_support_test.rb +41 -0
  4. data/lib/pacio_inferno_core/custom_groups/capability_statement/fhir_version_test.rb +15 -0
  5. data/lib/pacio_inferno_core/custom_groups/capability_statement/instantiate_test.rb +23 -0
  6. data/lib/pacio_inferno_core/custom_groups/capability_statement/json_support_test.rb +40 -0
  7. data/lib/pacio_inferno_core/date_search_validation.rb +112 -0
  8. data/lib/pacio_inferno_core/fhir_resource_navigation.rb +7 -0
  9. data/lib/pacio_inferno_core/generator/group_generator.rb +161 -0
  10. data/lib/pacio_inferno_core/generator/group_metadata.rb +114 -0
  11. data/lib/pacio_inferno_core/generator/group_metadata_extractor.rb +301 -0
  12. data/lib/pacio_inferno_core/generator/ig_loader.rb +118 -0
  13. data/lib/pacio_inferno_core/generator/ig_metadata.rb +60 -0
  14. data/lib/pacio_inferno_core/generator/ig_metadata_extractor.rb +88 -0
  15. data/lib/pacio_inferno_core/generator/ig_resources.rb +88 -0
  16. data/lib/pacio_inferno_core/generator/must_support_metadata_extractor.rb +8 -0
  17. data/lib/pacio_inferno_core/generator/must_support_test_generator.rb +138 -0
  18. data/lib/pacio_inferno_core/generator/naming.rb +50 -0
  19. data/lib/pacio_inferno_core/generator/read_test_generator.rb +102 -0
  20. data/lib/pacio_inferno_core/generator/reference_resolution_test_generator.rb +96 -0
  21. data/lib/pacio_inferno_core/generator/search_definition_metadata_extractor.rb +228 -0
  22. data/lib/pacio_inferno_core/generator/search_metadata_extractor.rb +78 -0
  23. data/lib/pacio_inferno_core/generator/search_test_generator.rb +298 -0
  24. data/lib/pacio_inferno_core/generator/special_cases.rb +61 -0
  25. data/lib/pacio_inferno_core/generator/suite_generator.rb +105 -0
  26. data/lib/pacio_inferno_core/generator/templates/group.rb.erb +27 -0
  27. data/lib/pacio_inferno_core/generator/templates/must_support.rb.erb +36 -0
  28. data/lib/pacio_inferno_core/generator/templates/read.rb.erb +34 -0
  29. data/lib/pacio_inferno_core/generator/templates/reference_resolution.rb.erb +40 -0
  30. data/lib/pacio_inferno_core/generator/templates/search.rb.erb +40 -0
  31. data/lib/pacio_inferno_core/generator/templates/validation.rb.erb +36 -0
  32. data/lib/pacio_inferno_core/generator/terminology_binding_metadata_extractor.rb +116 -0
  33. data/lib/pacio_inferno_core/generator/validation_test_generator.rb +146 -0
  34. data/lib/pacio_inferno_core/generator/value_extractor.rb +152 -0
  35. data/lib/pacio_inferno_core/generator.rb +130 -0
  36. data/lib/pacio_inferno_core/must_support_test.rb +20 -0
  37. data/lib/pacio_inferno_core/primitive_type.rb +5 -0
  38. data/lib/pacio_inferno_core/read_test.rb +103 -0
  39. data/lib/pacio_inferno_core/reference_resolution_test.rb +181 -0
  40. data/lib/pacio_inferno_core/request_logger.rb +46 -0
  41. data/lib/pacio_inferno_core/resource_search_param_checker.rb +136 -0
  42. data/lib/pacio_inferno_core/search_test.rb +859 -0
  43. data/lib/pacio_inferno_core/search_test_properties.rb +56 -0
  44. data/lib/pacio_inferno_core/validation_test.rb +55 -0
  45. data/lib/pacio_inferno_core/version.rb +4 -0
  46. data/lib/pacio_inferno_core/well_known_code_systems.rb +21 -0
  47. metadata +165 -0
@@ -0,0 +1,88 @@
1
+ require_relative './naming'
2
+ module PacioInfernoCore
3
+ class Generator
4
+ class IGResources
5
+ def add(resource)
6
+ resources_by_type[resource.resourceType] << resource
7
+ end
8
+
9
+ def naming
10
+ self.class.module_parent::Naming
11
+ end
12
+
13
+ def capability_statement(mode = 'server')
14
+ resources_by_type['CapabilityStatement'].find do |capability_statement_resource|
15
+ capability_statement_resource.rest.any? { |r| r.mode == mode } &&
16
+ capability_statement_resource.implementationGuide.any? { |p| p.include?(naming.implementation_guide_id) }
17
+ end
18
+ end
19
+
20
+ def ig
21
+ resources_by_type['ImplementationGuide'].first
22
+ end
23
+
24
+ def inspect
25
+ 'IGResources'
26
+ end
27
+
28
+ def profile_by_url(url)
29
+ return if url.nil? || url.empty?
30
+
31
+ normalized_url = url.split('|').first
32
+
33
+ resources_by_type['StructureDefinition'].find do |profile|
34
+ profile.url == normalized_url || profile.id == normalized_url
35
+ end
36
+ end
37
+
38
+ def resource_for_profile(url)
39
+ profile_by_url(url).type
40
+ end
41
+
42
+ def value_set_by_url(url)
43
+ resources_by_type['ValueSet'].find { |profile| profile.url == url }
44
+ end
45
+
46
+ def code_system_by_url(url)
47
+ resources_by_type['CodeSystem'].find { |system| system.url == url }
48
+ end
49
+
50
+ def search_param_by_resource_and_name(resource, name)
51
+ # remove '_' from search parameter name, such as _id or _tag
52
+ normalized_name = name.to_s.delete_prefix('_')
53
+
54
+ exact_id_matches = []
55
+ resource_scoped_code_matches = []
56
+
57
+ resources_by_type['SearchParameter'].each do |param|
58
+ exact_id_match = (param.id == "us-core-#{resource.downcase}-#{normalized_name}")
59
+ resource_scoped_code_match = Array(param.base).include?(resource) &&
60
+ (param.name == name || param.code == name)
61
+
62
+ if exact_id_match
63
+ exact_id_matches << param
64
+ elsif resource_scoped_code_match
65
+ resource_scoped_code_matches << param
66
+ end
67
+ end
68
+
69
+ candidates = exact_id_matches.empty? ? resource_scoped_code_matches : exact_id_matches
70
+ warn_about_multiple_search_param_candidates(resource, name, candidates) if candidates.length > 1
71
+
72
+ candidates.first
73
+ end
74
+
75
+ private
76
+
77
+ def warn_about_multiple_search_param_candidates(resource, name, candidates)
78
+ candidate_ids = candidates.map(&:id).join(', ')
79
+
80
+ puts "WARNING: Multiple SearchParameters matched #{resource}.#{name}: #{candidate_ids}"
81
+ end
82
+
83
+ def resources_by_type
84
+ @resources_by_type ||= Hash.new { |hash, key| hash[key] = [] }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,8 @@
1
+ require 'inferno'
2
+
3
+ module PacioInfernoCore
4
+ class Generator
5
+ class MustSupportMetadataExtractor < Inferno::DSL::MustSupportMetadataExtractor
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,138 @@
1
+ require_relative 'naming'
2
+ require_relative 'special_cases'
3
+
4
+ module PacioInfernoCore
5
+ class Generator
6
+ class MustSupportTestGenerator
7
+ class << self
8
+ def generate(ig_metadata, base_output_dir)
9
+ ig_metadata.groups
10
+ .reject { |group| SpecialCases.exclude_group? group }
11
+ .each { |group| new(group, base_output_dir).generate }
12
+ end
13
+ end
14
+
15
+ attr_accessor :group_metadata, :base_output_dir
16
+
17
+ def initialize(group_metadata, base_output_dir)
18
+ self.group_metadata = group_metadata
19
+ self.base_output_dir = base_output_dir
20
+ end
21
+
22
+ def template
23
+ @template ||= File.read(File.join(__dir__, 'templates', 'must_support.rb.erb'))
24
+ end
25
+
26
+ def output
27
+ @output ||= ERB.new(template, trim_mode: '-').result(binding)
28
+ end
29
+
30
+ def base_output_file_name
31
+ "#{class_name.underscore}.rb"
32
+ end
33
+
34
+ def output_file_directory
35
+ File.join(base_output_dir, profile_identifier)
36
+ end
37
+
38
+ def output_file_name
39
+ File.join(output_file_directory, base_output_file_name)
40
+ end
41
+
42
+ def read_interaction
43
+ self.class.read_interaction(group_metadata)
44
+ end
45
+
46
+ def naming
47
+ self.class.module_parent::Naming
48
+ end
49
+
50
+ def profile_identifier
51
+ naming.snake_case_for_profile(group_metadata)
52
+ end
53
+
54
+ def test_id
55
+ "#{naming.prefix}_#{group_metadata.reformatted_version}_#{profile_identifier}_must_support_test"
56
+ end
57
+
58
+ def class_name
59
+ "#{naming.upper_camel_case_for_profile(group_metadata)}MustSupportTest"
60
+ end
61
+
62
+ def module_name_with_version
63
+ "#{naming.module_name}#{group_metadata.reformatted_version.upcase}"
64
+ end
65
+
66
+ def resource_type
67
+ group_metadata.resource
68
+ end
69
+
70
+ def resource_collection_string
71
+ 'all_scratch_resources'
72
+ end
73
+
74
+ def must_support_list_string
75
+ build_must_support_list_string(false)
76
+ end
77
+
78
+ def uscdi_list_string
79
+ build_must_support_list_string(true)
80
+ end
81
+
82
+ def build_must_support_list_string(uscdi_only)
83
+ slice_names = group_metadata.must_supports[:slices]
84
+ .select { |slice| slice[:uscdi_only].presence == uscdi_only.presence }
85
+ .map { |slice| slice[:slice_id] }
86
+
87
+ element_names = group_metadata.must_supports[:elements]
88
+ .select { |element| element[:uscdi_only].presence == uscdi_only.presence }
89
+ .map { |element| "#{resource_type}.#{element[:path]}" }
90
+
91
+ extension_names = group_metadata.must_supports[:extensions]
92
+ .select { |extension| extension[:uscdi_only].presence == uscdi_only.presence }
93
+ .map { |extension| extension[:id] }
94
+
95
+ choice_names = []
96
+ group_metadata.must_supports[:choices]&.map do |choice|
97
+ next unless choice[:uscdi_only].presence == uscdi_only.presence
98
+
99
+ combined = []
100
+ if choice.key?(:paths)
101
+ choice[:paths].each { |path| element_names.delete("#{resource_type}.#{path}") }
102
+ combined << choice[:paths].map { |path| "#{resource_type}.#{path}" }.join(' or ')
103
+ end
104
+
105
+ if choice.key?(:extension_ids)
106
+ choice[:extension_ids].each { |extesnion_id| extension_names.delete(extesnion_id) }
107
+ combined << choice[:extension_ids].join(' or ')
108
+ end
109
+
110
+ if choice.key?(:elements)
111
+ choice[:elements].each { |element| element_names.delete("#{resource_type}.#{element[:path]}") }
112
+ combined << choice[:elements].map { |element|
113
+ "#{resource_type}.#{element[:path]}:#{element[:fixed_value]}"
114
+ }.join(' or ')
115
+ end
116
+
117
+ choice_names << combined.join(' or ') if combined.any?
118
+ end || []
119
+
120
+ (slice_names + element_names + extension_names + choice_names)
121
+ .uniq
122
+ .sort
123
+ .map { |name| "#{' ' * 8}* #{name}" }
124
+ .join("\n")
125
+ end
126
+
127
+ def generate
128
+ FileUtils.mkdir_p(output_file_directory)
129
+ File.write(output_file_name, output)
130
+
131
+ group_metadata.add_test(
132
+ id: test_id,
133
+ file_name: base_output_file_name
134
+ )
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,50 @@
1
+ module PacioInfernoCore
2
+ class Generator
3
+ module Naming
4
+ IG_LINKS = {}.freeze
5
+
6
+ class << self
7
+ def resources_with_multiple_profiles
8
+ []
9
+ end
10
+
11
+ def prefix
12
+ # Example: 'us_core', 'smp'
13
+ # raise NotImplementedError, "#{self.class} must implement #naming_prefix"
14
+ end
15
+
16
+ def implementation_guide_id
17
+ # Example: 'hl7.fhir.us.pacio-pfe'
18
+ end
19
+
20
+ def module_name
21
+ # Example: 'USCore', 'PacioSMP'
22
+ end
23
+
24
+ def long_name
25
+ # Example: 'US Core Server'
26
+ end
27
+
28
+ def resource_has_multiple_profiles?(resource)
29
+ resources_with_multiple_profiles.include? resource
30
+ end
31
+
32
+ def snake_case_for_profile(group_metadata)
33
+ resource = group_metadata.resource
34
+ return resource.underscore unless resource_has_multiple_profiles?(resource)
35
+
36
+ group_metadata.name
37
+ .delete_prefix("#{prefix.downcase}_").underscore
38
+ end
39
+
40
+ def upper_camel_case_for_profile(group_metadata)
41
+ snake_case_for_profile(group_metadata).camelize
42
+ end
43
+
44
+ def ig_link(version)
45
+ IG_LINKS[version]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,102 @@
1
+ require_relative 'naming'
2
+ require_relative 'special_cases'
3
+
4
+ module PacioInfernoCore
5
+ class Generator
6
+ class ReadTestGenerator
7
+ class << self
8
+ def generate(ig_metadata, base_output_dir)
9
+ ig_metadata.groups
10
+ .reject { |group| SpecialCases.exclude_group? group }
11
+ .select { |group| read_interaction(group).present? }
12
+ .each do |group|
13
+ new(
14
+ group, base_output_dir
15
+ ).generate
16
+ end
17
+ end
18
+
19
+ def read_interaction(group_metadata)
20
+ group_metadata.interactions.find { |interaction| interaction[:code] == 'read' }
21
+ end
22
+ end
23
+
24
+ attr_accessor :group_metadata, :base_output_dir
25
+
26
+ def initialize(group_metadata, base_output_dir)
27
+ self.group_metadata = group_metadata
28
+ self.base_output_dir = base_output_dir
29
+ end
30
+
31
+ def template
32
+ @template ||= File.read(File.join(__dir__, 'templates', 'read.rb.erb'))
33
+ end
34
+
35
+ def output
36
+ @output ||= ERB.new(template).result(binding)
37
+ end
38
+
39
+ def base_output_file_name
40
+ "#{class_name.underscore}.rb"
41
+ end
42
+
43
+ def output_file_directory
44
+ File.join(base_output_dir, profile_identifier)
45
+ end
46
+
47
+ def output_file_name
48
+ File.join(output_file_directory, base_output_file_name)
49
+ end
50
+
51
+ def read_interaction
52
+ self.class.read_interaction(group_metadata)
53
+ end
54
+
55
+ def naming
56
+ self.class.module_parent::Naming
57
+ end
58
+
59
+ def profile_identifier
60
+ naming.snake_case_for_profile(group_metadata)
61
+ end
62
+
63
+ def test_id
64
+ "#{naming.prefix}_#{group_metadata.reformatted_version}_#{profile_identifier}_read_test"
65
+ end
66
+
67
+ def class_name
68
+ "#{naming.upper_camel_case_for_profile(group_metadata)}ReadTest"
69
+ end
70
+
71
+ def module_name_with_version
72
+ "#{naming.module_name}#{group_metadata.reformatted_version.upcase}"
73
+ end
74
+
75
+ def resource_type
76
+ group_metadata.resource
77
+ end
78
+
79
+ def resource_collection_string
80
+ if group_metadata.delayed? && resource_type != 'Provenance'
81
+ "scratch.dig(:references, '#{resource_type}'), delayed_reference: true"
82
+ else
83
+ 'all_scratch_resources'
84
+ end
85
+ end
86
+
87
+ def conformance_expectation
88
+ read_interaction[:expectation]
89
+ end
90
+
91
+ def generate
92
+ FileUtils.mkdir_p(output_file_directory)
93
+ File.write(output_file_name, output)
94
+
95
+ group_metadata.add_test(
96
+ id: test_id,
97
+ file_name: base_output_file_name
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,96 @@
1
+ require_relative 'naming'
2
+ require_relative 'special_cases'
3
+
4
+ module PacioInfernoCore
5
+ class Generator
6
+ class ReferenceResolutionTestGenerator
7
+ class << self
8
+ def generate(ig_metadata, base_output_dir)
9
+ ig_metadata.groups
10
+ .reject { |group| SpecialCases.exclude_group? group }
11
+ .each { |group| new(group, base_output_dir).generate }
12
+ end
13
+ end
14
+
15
+ attr_accessor :group_metadata, :base_output_dir
16
+
17
+ def initialize(group_metadata, base_output_dir)
18
+ self.group_metadata = group_metadata
19
+ self.base_output_dir = base_output_dir
20
+ end
21
+
22
+ def template
23
+ @template ||= File.read(File.join(__dir__, 'templates', 'reference_resolution.rb.erb'))
24
+ end
25
+
26
+ def output
27
+ @output ||= ERB.new(template, trim_mode: '-').result(binding)
28
+ end
29
+
30
+ def base_output_file_name
31
+ "#{class_name.underscore}.rb"
32
+ end
33
+
34
+ def output_file_directory
35
+ File.join(base_output_dir, profile_identifier)
36
+ end
37
+
38
+ def output_file_name
39
+ File.join(output_file_directory, base_output_file_name)
40
+ end
41
+
42
+ def naming
43
+ self.class.module_parent::Naming
44
+ end
45
+
46
+ def profile_identifier
47
+ naming.snake_case_for_profile(group_metadata)
48
+ end
49
+
50
+ def test_id
51
+ "#{naming.prefix}_#{group_metadata.reformatted_version}_#{profile_identifier}_reference_resolution_test"
52
+ end
53
+
54
+ def class_name
55
+ "#{naming.upper_camel_case_for_profile(group_metadata)}ReferenceResolutionTest"
56
+ end
57
+
58
+ def module_name_with_version
59
+ "#{naming.module_name}#{group_metadata.reformatted_version.upcase}"
60
+ end
61
+
62
+ def resource_type
63
+ group_metadata.resource
64
+ end
65
+
66
+ def resource_collection_string
67
+ 'scratch_resources[:all]'
68
+ end
69
+
70
+ def must_support_references
71
+ group_metadata.must_supports[:elements]
72
+ .select { |element| element[:types]&.include?('Reference') }
73
+ end
74
+
75
+ def must_support_reference_list_string
76
+ must_support_references
77
+ .map { |element| "#{' ' * 8}* #{resource_type}.#{element[:path]}" }
78
+ .uniq
79
+ .sort
80
+ .join("\n")
81
+ end
82
+
83
+ def generate
84
+ return if must_support_references.empty?
85
+
86
+ FileUtils.mkdir_p(output_file_directory)
87
+ File.write(output_file_name, output)
88
+
89
+ group_metadata.add_test(
90
+ id: test_id,
91
+ file_name: base_output_file_name
92
+ )
93
+ end
94
+ end
95
+ end
96
+ end