diff --git a/generator/generator_base.rb b/generator/generator_base.rb index c895e0d58..e8e808246 100644 --- a/generator/generator_base.rb +++ b/generator/generator_base.rb @@ -27,7 +27,14 @@ def load_resources # We should consider using the native Ruby models instead of JSON # There were problems with round-tripping certain SearchParameters though - new_resource_json = JSON.parse(File.read(resource)) + + begin + new_resource_json = JSON.parse(File.read(resource)) + rescue JSON::ParserError + Inferno.logger.debug("Failed to parse JSON: #{resource}") + next + end + new_resource = FHIR.from_contents(File.read(resource)) next unless new_resource.present? diff --git a/generator/generic/generic_generator.rb b/generator/generic/generic_generator.rb index e609b61f0..86b472d96 100644 --- a/generator/generic/generic_generator.rb +++ b/generator/generic/generic_generator.rb @@ -2,32 +2,46 @@ require_relative '../generator_base' require_relative '../sequence_metadata' +require_relative '../search_parameter_metadata' require_relative './read_test' require_relative './profile_validation_test' +require_relative './search_test' +require_relative './interaction_test' +require_relative '../generic_generator_utilities' module Inferno module Generator class GenericGenerator < Generator::Base include ReadTest include ProfileValidationTest + include SearchTest + include InteractionTest + include Inferno::Generator::GenericGeneratorUtilties def resource_profiles resources_by_type['StructureDefinition'].reject { |definition| definition['type'] == 'Extension' } end def sequence_metadata - @sequence_metadata ||= resource_profiles.map { |profile| SequenceMetadata.new(profile) } + @sequence_metadata ||= resource_profiles.map { |profile| SequenceMetadata.new(profile, module_name, search_parameter_metadata, capability_statement) } + end + + def search_parameter_metadata + @search_parameter_metadata ||= resources_by_type['SearchParameter'].map { |parameter_json| SearchParameterMetadata.new(parameter_json) } end def generate generate_sequences + copy_static_files generate_module end def generate_sequences sequence_metadata.each do |metadata| create_read_test(metadata) + create_search_tests(metadata) create_profile_validation_test(metadata) + create_interaction_tests(metadata) generate_sequence(metadata) end end @@ -39,6 +53,46 @@ def generate_sequence(metadata) output = template.result_with_hash(metadata: metadata) FileUtils.mkdir_p(sequence_out_path + '/') unless File.directory?(sequence_out_path + '/') File.write(file_name, output) + + generate_sequence_definitions(metadata) + end + + def generate_sequence_definitions(metadata) + output_directory = File.join(sequence_out_path, 'profile_definitions') + file_name = File.join(output_directory, metadata.file_name + '_definitions.rb') + template = ERB.new(File.read(File.join(__dir__, 'templates/sequence_definition.rb.erb'))) + output = template.result_with_hash(sequence_definition_hash(metadata)) + FileUtils.mkdir_p(sequence_out_path + '/profile_definitions/') unless File.directory?(sequence_out_path + '/profile_definitions/') + File.write(file_name, output) + end + + def sequence_definition_hash(metadata) + search_parameters = metadata.search_parameter_metadata&.map do |param_metadata| + { + url: param_metadata.url, + code: param_metadata.code, + expression: param_metadata.expression, + multipleOr: param_metadata.multiple_or, + multipleOrExpectation: param_metadata.multiple_or_expectation, + multipleAnd: param_metadata.multiple_and, + multipleAndExpectation: param_metadata.multiple_and_expectation, + modifiers: param_metadata.modifiers, + comparators: param_metadata.comparators + } + end + search_parameters ||= [] + { + module_name: module_name + 'ProfileDefinitions', + class_name: metadata.class_name + 'Definition', + search_parameters: structure_to_string(search_parameters) + } + end + + def module_name + delimiters = ['-', '_', '.'] + @path.split(Regexp.union(delimiters)) + .map(&:capitalize) + .join end def module_file_path @@ -59,6 +113,12 @@ def generate_module File.write(file_name, output) end + + def copy_static_files + Dir.glob(File.join(__dir__, 'static', '*')).each do |static_file| + FileUtils.cp(static_file, sequence_out_path) + end + end end end end diff --git a/generator/generic/interaction_test.rb b/generator/generic/interaction_test.rb new file mode 100644 index 000000000..5981d73e1 --- /dev/null +++ b/generator/generic/interaction_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require_relative '../test_metadata' + +module Inferno + module Generator + module InteractionTest + def create_interaction_tests(metadata) + metadata.interactions.each do |interaction| + next if ['read', 'search-type'].include? interaction[:code] # already have tests for + next if ['create', 'update', 'patch', 'delete', 'history-type'].include? interaction[:code] # not currently supported + + interaction[:code] = 'history' if interaction[:code] == 'history-instance' # how the history interaction is called already + + interaction_test = TestMetadata.new( + title: "Server supports the #{metadata.resource_type} #{interaction[:code]} interaction", + key: "resource_#{interaction[:code].gsub('-', '_').downcase}".to_sym, + description: "This test will verify that #{metadata.resource_type} #{interaction[:code]} interactions are supported by the server.", + optional: interaction[:expectation] != 'SHALL' + ) + + validate_reply_args = [ + '@resource_found', + "versioned_resource_class('#{metadata.resource_type}')" + ] + validate_reply_args_string = validate_reply_args.join(', ') + + interaction_test.code = %( + skip 'No resource found from Read test' unless @resource_found.present? + validate_#{interaction[:code].gsub('-', '_')}_reply(#{validate_reply_args_string}) + ) + metadata.add_test(interaction_test) + end + end + end + end +end diff --git a/generator/generic/profile_validation_test.rb b/generator/generic/profile_validation_test.rb index 7ee8e5fa7..bad9b4b81 100644 --- a/generator/generic/profile_validation_test.rb +++ b/generator/generic/profile_validation_test.rb @@ -13,7 +13,7 @@ def create_profile_validation_test(metadata) ) profile_validation_test.code = %( skip 'No resource found from Read test' unless @resource_found.present? - test_resource_against_profile('#{metadata.resource_type}', @resource_found, '#{metadata.url}') + test_resources_against_profile('#{metadata.resource_type}','#{metadata.url}') ) metadata.add_test(profile_validation_test) end diff --git a/generator/generic/read_test.rb b/generator/generic/read_test.rb index 766fbd96d..fed59b71e 100644 --- a/generator/generic/read_test.rb +++ b/generator/generic/read_test.rb @@ -14,6 +14,7 @@ def create_read_test(metadata) read_test.code = %( resource_id = @instance.#{metadata.resource_type.underscore}_id @resource_found = validate_read_reply(FHIR::#{metadata.resource_type}.new(id: resource_id), FHIR::#{metadata.resource_type}) + save_resource_references(versioned_resource_class('#{metadata.resource_type}'), [@resource_found], '#{metadata.url}') ) metadata.add_test(read_test) end diff --git a/generator/generic/search_test.rb b/generator/generic/search_test.rb new file mode 100644 index 000000000..29e1f3752 --- /dev/null +++ b/generator/generic/search_test.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative '../test_metadata' + +module Inferno + module Generator + module SearchTest + def create_search_tests(metadata) + metadata.searches.each do |search| + search_test = TestMetadata.new( + title: "Server returns expected results from #{metadata.resource_type} search by #{search[:parameters].join('+')}", + key: :"search_by_#{search[:parameters].map(&:underscore).join('_')}", + description: "This test will verify that #{metadata.resource_type} resources can be searched from the server.", + optional: search[:expectation] != 'SHALL' + ) + + search_parameter_assignments = search[:parameters].map do |parameter| + param_metadata = metadata.search_parameter_metadata.find { |parameter_metadata| parameter_metadata.code == parameter } + path = param_metadata + .expression + .gsub(/(? - <%= sequence.class_name %><% end %> +sequence_requirements:<% sequences.flat_map { |seq| seq.requirements }.uniq.map { |req| req.gsub(':', '') }.each do |requirement| %> + <%= requirement %>: + label: <%= requirement %><% end %> diff --git a/generator/generic/templates/sequence.rb.erb b/generator/generic/templates/sequence.rb.erb index d84943d0f..08388fa94 100644 --- a/generator/generic/templates/sequence.rb.erb +++ b/generator/generic/templates/sequence.rb.erb @@ -3,6 +3,8 @@ module Inferno module Sequence class <%= metadata.class_name %> < SequenceBase + include Inferno::SequenceUtilities + title '<%= metadata.title %> Tests' description 'Verify support for the server capabilities required by the <%= metadata.title %> profile.' details %( @@ -12,6 +14,8 @@ module Inferno @resource_found = nil + <%=metadata.create_search_validation(metadata)%> + <% metadata.tests.each_with_index do |test, idx|%> <% if test.key.present? %> test :<%= test.key %> do diff --git a/generator/generic/templates/sequence_definition.rb.erb b/generator/generic/templates/sequence_definition.rb.erb new file mode 100644 index 000000000..0de34d704 --- /dev/null +++ b/generator/generic/templates/sequence_definition.rb.erb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Inferno + module <%= module_name %> + class <%= class_name %> + SEARCH_PARAMETERS = <%= search_parameters %>.freeze + end + end +end diff --git a/generator/generic_generator_utilities.rb b/generator/generic_generator_utilities.rb new file mode 100644 index 000000000..21170a862 --- /dev/null +++ b/generator/generic_generator_utilities.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +module Inferno + module Generator + module GenericGeneratorUtilties + EXPECTATION_URL = 'http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation' + + def structure_to_string(struct) + if struct.is_a? Hash + %({ + #{struct.map { |k, v| "#{k}: #{structure_to_string(v)}" }.join(",\n")} + }) + elsif struct.is_a? Array + if struct.empty? + '[]' + else + %([ + #{struct.map { |el| structure_to_string(el) }.join(",\n")} + ]) + end + elsif struct.is_a? String + "'#{struct}'" + elsif [true, false].include? struct + struct.to_s + else + "''" + end + end + + def create_search_validation(sequence_metadata) + search_validators = '' + sequence_metadata.search_parameter_metadata&.each do |parameter_metadata| + type = sequence_metadata.element_type_by_path(parameter_metadata.expression) || parameter_metadata.type + path = parameter_metadata.expression + .gsub(/(?