Skip to content
This repository has been archived by the owner on Jan 23, 2024. It is now read-only.

Commit

Permalink
Merge pull request #301 from onc-healthit/development
Browse files Browse the repository at this point in the history
Version 2.6.0
  • Loading branch information
Jammjammjamm authored Sep 24, 2019
2 parents 260c7e9 + b7839b0 commit dbb146a
Show file tree
Hide file tree
Showing 70 changed files with 1,758 additions and 289 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ coverage
tmp/*
.vscode/*
resources/terminology/*
.ruby-version
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.5.6
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruby 2.5.6
3 changes: 0 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
language: ruby
services:
- docker
rvm:
- 2.5
- 2.6
before_install:
- gem update --system
- gem install bundler
Expand Down
30 changes: 25 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
FROM ruby:2.5
FROM ruby:2.5.6

# Install gems into a temporary directory
COPY Gemfile* ./
RUN gem install bundler && bundle install
WORKDIR /var/www/inferno

# Expose the port
### Install dependencies

COPY Gemfile* /var/www/inferno/
RUN gem install bundler
# Throw an error if Gemfile & Gemfile.lock are out of sync
RUN bundle config --global frozen 1
RUN bundle install

### Install Inferno

RUN mkdir data
COPY public /var/www/inferno/public
COPY resources /var/www/inferno/resources
COPY config* /var/www/inferno/
COPY Rakefile /var/www/inferno/
COPY test /var/www/inferno/test
COPY lib /var/www/inferno/lib

### Set up environment

ENV APP_ENV=production
EXPOSE 4567

CMD ["bundle", "exec", "rackup", "-o", "0.0.0.0"]
10 changes: 6 additions & 4 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ gem 'json-jwt'
gem 'kramdown'
gem 'pry'
gem 'pry-byebug'
gem 'rack-test'
gem 'rake'
gem 'rb-readline'
gem 'rest-client'
gem 'rubocop', require: false
gem 'selenium-webdriver'
gem 'sinatra'
gem 'sinatra-contrib'
gem 'sqlite3'
gem 'thin'
gem 'time_difference'
gem 'webmock'

gem 'simplecov', require: false, group: :test
group :test do
gem 'rack-test'
gem 'rubocop', require: false
gem 'simplecov', require: false
gem 'webmock'
end
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<img src="https://github.com/onc-healthit/inferno/blob/master/public/images/inferno_logo.png" width="300px" />
<img src="https://raw.githubusercontent.com/onc-healthit/inferno/master/public/images/inferno_logo.png" width="300px" />

[![Build Status](https://travis-ci.org/onc-healthit/inferno.svg?branch=master)](https://travis-ci.org/onc-healthit/inferno)

Expand Down Expand Up @@ -82,7 +82,7 @@ bundle exec rake test
## Inspecting and Exporting Tests

Tests are written to be easily understood, even by those who aren't familiar with Ruby. They can be
viewed directly [in this repository](https://github.com/onc-healthit/inferno/tree/master/lib/app/sequences).
viewed directly [in this repository](https://github.com/onc-healthit/inferno/tree/master/lib/app/modules).

Tests contain metadata that provide additional details and traceability to standards. The active tests and related metadata can be exported into CSV format and saved to a file named `testlist.csv` with the following command:

Expand Down
3 changes: 0 additions & 3 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ app_name: Inferno
# There are a few exceptions, such as "/" and "/landing".
base_path: "inferno"

# Show or hide tutorials
show_tutorial: true

# Useful during development to purge the database on each reload
purge_database_on_reload: false

Expand Down
15 changes: 4 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
version: '3'
services:
# the main ruby server
ruby_server:
# build the image from ./Dockerfile (this installs the dependencies into the image)
build:
context: ./
# map the project directory into the running container
volumes:
- .:/var/www/inferno
# expose the internal application to direct HTTP requests
#ports:
# - "4567:8080"
# run the application
working_dir: /var/www/inferno
command: rackup -o 0.0.0.0
- ./config.yml:/var/www/inferno/config.yml
ports:
- "4567:8080"
nginx_server:
image: nginx
volumes:
Expand All @@ -24,4 +17,4 @@ services:
links:
- ruby_server:ruby_server
depends_on:
- ruby_server
- ruby_server
86 changes: 69 additions & 17 deletions generators/uscore-r4/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
require 'net/http'
require 'fhir_models'
require_relative './metadata_extractor'
require_relative '../../lib/app/utils/validation'

OUT_PATH = '../../lib/app/modules'
RESOURCE_PATH = '../../resources/us_core_r4/'
OUT_PATH = File.expand_path('../../lib/app/modules', __dir__)
RESOURCE_PATH = File.expand_path('../../resources/us_core_r4', __dir__)

PROFILE_URIS = Inferno::ValidationUtil::US_CORE_R4_URIS

def validation_profile_uri(sequence)
profile_uri = PROFILE_URIS.key(sequence[:profile])
"Inferno::ValidationUtil::US_CORE_R4_URIS[:#{profile_uri}]" if profile_uri
end

def run
redownload_files = (ARGV&.first == '-d')
Expand All @@ -31,8 +39,15 @@ def generate_search_validators(metadata)
end

def generate_tests(metadata)
# first isolate the profiles that don't have patient searches
mark_delayed_sequences(metadata)

metadata[:sequences].each do |sequence|
puts "Generating test #{sequence[:name]}"

# read reference if sequence contains no search sequences
create_read_test(sequence) if sequence[:delayed_sequence]

# authorization test
create_authorization_test(sequence)

Expand All @@ -52,6 +67,7 @@ def generate_tests(metadata)
.each do |interaction|
# specific edge cases
interaction[:code] = 'history' if interaction[:code] == 'history-instance'
next if interaction[:code] == 'read' && sequence[:delayed_sequence]

create_interaction_test(sequence, interaction)
end
Expand All @@ -62,25 +78,52 @@ def generate_tests(metadata)
end
end

def mark_delayed_sequences(metadata)
metadata[:sequences].each do |sequence|
sequence[:delayed_sequence] = sequence[:resource] != 'Patient' && sequence[:searches].none? { |search| search[:names].include? 'patient' }
end
metadata[:delayed_sequences] = metadata[:sequences].select { |seq| seq[:delayed_sequence] }
metadata[:non_delayed_sequences] = metadata[:sequences].reject { |seq| seq[:delayed_sequence] }
end

def find_first_search(sequence)
sequence[:searches].find { |search_param| search_param[:expectation] == 'SHALL' } ||
sequence[:searches].find { |search_param| search_param[:expectation] == 'SHOULD' }
end

def generate_sequence(sequence)
puts "Generating #{sequence[:name]}\n"
file_name = OUT_PATH + '/us_core_r4/' + sequence[:name].downcase + '_sequence.rb'

template = ERB.new(File.read('./templates/sequence.rb.erb'))
template = ERB.new(File.read(File.join(__dir__, 'templates/sequence.rb.erb')))
output = template.result_with_hash(sequence)
FileUtils.mkdir_p(OUT_PATH + '/us_core_r4') unless File.directory?(OUT_PATH + '/us_core_r4')
File.write(file_name, output)
end

def create_read_test(sequence)
read_test = {
tests_that: "Can read #{sequence[:resource]} from the server",
index: sequence[:tests].length + 1,
link: 'https://build.fhir.org/ig/HL7/US-Core-R4/CapabilityStatement-us-core-server.html'
}

read_test[:test_code] = %(
#{sequence[:resource].downcase}_id = @instance.resource_references.find { |reference| reference.resource_type == '#{sequence[:resource]}' }&.resource_id
skip 'No #{sequence[:resource]} references found from the prior searches' if #{sequence[:resource].downcase}_id.nil?
@#{sequence[:resource].downcase} = fetch_resource('#{sequence[:resource]}', #{sequence[:resource].downcase}_id)
@resources_found = !@#{sequence[:resource].downcase}.nil?)
sequence[:tests] << read_test
end

def create_authorization_test(sequence)
authorization_test = {
tests_that: "Server rejects #{sequence[:resource]} search without authorization",
index: sequence[:tests].length + 1,
link: 'http://www.fhir.org/guides/argonaut/r2/Conformance-server.html'
}

first_search = sequence[:searches].find { |search_param| search_param[:expectation] == 'SHALL' } ||
sequence[:searches].find { |search_param| search_param[:expectation] == 'SHOULD' }
first_search = find_first_search(sequence)
return if first_search.nil?

authorization_test[:test_code] = %(
Expand All @@ -98,10 +141,17 @@ def create_search_test(sequence, search_param)
search_test = {
tests_that: "Server returns expected results from #{sequence[:resource]} search by #{search_param[:names].join('+')}",
index: sequence[:tests].length + 1,
link: 'https://build.fhir.org/ig/HL7/US-Core-R4/CapabilityStatement-us-core-server.html'
link: 'https://build.fhir.org/ig/HL7/US-Core-R4/CapabilityStatement-us-core-server.html',
optional: search_param[:expectation] != 'SHALL'
}

is_first_search = search_test[:index] == 2 # if first search - fix this check later
is_first_search = search_param == find_first_search(sequence)
save_resource_ids_in_bundle_arguments = [
"versioned_resource_class('#{sequence[:resource]}')",
'reply',
validation_profile_uri(sequence)
].compact.join(', ')

search_test[:test_code] =
if is_first_search
%(#{get_search_params(search_param[:names], sequence)}
Expand All @@ -116,7 +166,8 @@ def create_search_test(sequence, search_param)
@#{sequence[:resource].downcase} = reply.try(:resource).try(:entry).try(:first).try(:resource)
@#{sequence[:resource].downcase}_ary = reply&.resource&.entry&.map { |entry| entry&.resource }
save_resource_ids_in_bundle(versioned_resource_class('#{sequence[:resource]}'), reply)
save_resource_ids_in_bundle(#{save_resource_ids_in_bundle_arguments})
save_delayed_sequence_references(@#{sequence[:resource].downcase})
validate_search_reply(versioned_resource_class('#{sequence[:resource]}'), reply, search_params))
else
%(
Expand Down Expand Up @@ -216,7 +267,7 @@ def create_resource_profile_test(sequence)
}
test[:test_code] = %(
skip 'No resources appear to be available for this patient. Please use patients with more information.' unless @resources_found
test_resources_against_profile('#{sequence[:resource]}'))
test_resources_against_profile('#{sequence[:resource]}'#{', ' + validation_profile_uri(sequence) if validation_profile_uri(sequence)}))

sequence[:tests] << test
end
Expand Down Expand Up @@ -259,6 +310,8 @@ def get_value_path_by_type(type)
'.code'
when 'HumanName'
'.family'
when 'Address'
'.city'
else
''
end
Expand Down Expand Up @@ -334,14 +387,13 @@ def get_comparator_searches(search_params, sequence)
def search_param_constants(search_parameters, sequence)
return "patient: @instance.patient_id, category: 'assess-plan'" if search_parameters == ['patient', 'category'] && sequence[:resource] == 'CarePlan'
return "patient: @instance.patient_id, status: 'active'" if search_parameters == ['patient', 'status'] && sequence[:resource] == 'CareTeam'
return "patient: @instance.patient_id, name: 'Boston'" if search_parameters == ['name'] && (['Location', 'Organization'].include? sequence[:resource])
return "'_id': @instance.patient_id" if search_parameters == ['_id'] && sequence[:resource] == 'Patient'
return "patient: @instance.patient_id, code: '72166-2'" if search_parameters == ['patient', 'code'] && sequence[:profile] == 'https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-smokingstatus.json'
return "patient: @instance.patient_id, category: 'laboratory'" if search_parameters == ['patient', 'category'] && sequence[:profile] == 'https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-observation-lab.json'
return "patient: @instance.patient_id, code: '77606-2'" if search_parameters == ['patient', 'code'] && sequence[:profile] == 'https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-pediatric-weight-for-height.json'
return "patient: @instance.patient_id, code: '59576-9'" if search_parameters == ['patient', 'code'] && sequence[:profile] == 'https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-pediatric-bmi-for-age.json'
return "patient: @instance.patient_id, category: 'LAB'" if search_parameters == ['patient', 'category'] && sequence[:profile] == 'https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-diagnosticreport-lab.json'
return "patient: @instance.patient_id, code: 'LP29684-5'" if search_parameters == ['patient', 'category'] && sequence[:profile] == 'https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-diagnosticreport-note.json'
return "patient: @instance.patient_id, code: '72166-2'" if search_parameters == ['patient', 'code'] && sequence[:profile] == PROFILE_URIS[:smoking_status]
return "patient: @instance.patient_id, category: 'laboratory'" if search_parameters == ['patient', 'category'] && sequence[:profile] == PROFILE_URIS[:lab_results]
return "patient: @instance.patient_id, code: '77606-2'" if search_parameters == ['patient', 'code'] && sequence[:profile] == PROFILE_URIS[:pediatric_weight_height]
return "patient: @instance.patient_id, code: '59576-9'" if search_parameters == ['patient', 'code'] && sequence[:profile] == PROFILE_URIS[:pediatric_bmi_age]
return "patient: @instance.patient_id, category: 'LAB'" if search_parameters == ['patient', 'category'] && sequence[:profile] == PROFILE_URIS[:diagnostic_report_lab]
return "patient: @instance.patient_id, code: 'LP29684-5'" if search_parameters == ['patient', 'category'] && sequence[:profile] == PROFILE_URIS[:diagnostic_report_note]
end

def create_search_validation(sequence)
Expand Down Expand Up @@ -418,7 +470,7 @@ def validate_resource_item(resource, property, value)
def generate_module(module_info)
file_name = OUT_PATH + '/us_core_module.yml'

template = ERB.new(File.read('./templates/module.yml.erb'))
template = ERB.new(File.read(File.join(__dir__, 'templates/module.yml.erb')))
output = template.result_with_hash(module_info)

File.write(file_name, output)
Expand Down
23 changes: 14 additions & 9 deletions generators/uscore-r4/metadata_extractor.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# frozen_string_literal: true

class MetadataExtractor
CAPABILITY_STATEMENT_URI = 'https://build.fhir.org/ig/HL7/US-Core-R4/CapabilityStatement-us-core-server.json'
CAPABILITY_STATEMENT_URI = 'https://www.hl7.org/fhir/us/core/CapabilityStatement-us-core-server.json'

def profile_uri(profile)
"https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-#{profile}.json"
"http://hl7.org/fhir/us/core/StructureDefinition/#{profile}"
end

def profile_json_uri(profile)
"https://www.hl7.org/fhir/us/core/StructureDefinition-#{profile}.json"
end

def search_param_uri(resource, param)
param = 'id' if param == '_id'
"https://build.fhir.org/ig/HL7/US-Core-R4/SearchParameter-us-core-#{resource.downcase}-#{param}.json"
"https://www.hl7.org/fhir/us/core/SearchParameter-us-core-#{resource.downcase}-#{param}.json"
end

def get_json_from_uri(uri)
filename = RESOURCE_PATH + uri.split('/').last
filename = File.join(RESOURCE_PATH, uri.split('/').last)
unless File.exist?(filename)
puts "Downloading #{uri}\n"
json_result = Net::HTTP.get(URI(uri))
Expand All @@ -33,7 +37,7 @@ def extract_metadata

def build_new_sequence(resource, profile)
base_name = profile.split('StructureDefinition/')[1]
profile_json = get_json_from_uri(profile_uri(base_name))
profile_json = get_json_from_uri(profile_json_uri(base_name))
profile_title = profile_json['title'].gsub(/US\s*Core\s*/, '').gsub(/\s*Profile/, '').strip
{
name: base_name.tr('-', '_'),
Expand All @@ -44,6 +48,7 @@ def build_new_sequence(resource, profile)
.gsub('UsCore', 'USCoreR4') + 'Sequence',
resource: resource['type'],
profile: profile_uri(base_name), # link in capability statement is incorrect,
profile_json: profile_json_uri(base_name),
title: profile_title,
interactions: [],
searches: [],
Expand All @@ -67,7 +72,7 @@ def extract_metadata_from_resources(resources)
add_combo_searches(resource, new_sequence)
add_interactions(resource, new_sequence)

profile_definition = get_json_from_uri(new_sequence[:profile])
profile_definition = get_json_from_uri(new_sequence[:profile_json])
add_must_support_elements(profile_definition, new_sequence)
add_search_param_descriptions(profile_definition, new_sequence)
add_element_definitions(profile_definition, new_sequence)
Expand Down Expand Up @@ -203,9 +208,9 @@ def add_element_definitions(profile_definition, sequence)

def add_special_cases
category_first_profiles = [
'https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-diagnosticreport-lab.json',
'https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-observation-lab.json',
'https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-diagnosticreport-note.json'
PROFILE_URIS[:diagnostic_report_lab],
PROFILE_URIS[:lab_results],
PROFILE_URIS[:diagnostic_report_note]
]

# search by patient first
Expand Down
7 changes: 4 additions & 3 deletions generators/uscore-r4/templates/module.yml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ test_sets:
- ManualRegistrationSequence
- StandaloneLaunchSequence
- EHRLaunchSequence
- name: US Core R4
- name: US Core R4 Patient Based Profiles
run_all: true
sequences:<% sequences.each do |sequence| %>
sequences:<% non_delayed_sequences.each do |sequence| %>
- <%=sequence[:classname]%><% end %>
- R4ProvenanceSequence
- USCoreR4ClinicalNotesSequence
- USCoreR4ClinicalNotesSequence<% delayed_sequences.each do |sequence| %>
- <%=sequence[:classname]%><% end %>
Loading

0 comments on commit dbb146a

Please sign in to comment.