Skip to content

Commit

Permalink
FI-2431 inferno new IG loading (#454)
Browse files Browse the repository at this point in the history
* implement load_igs

* use https://packages.fhir.org registry

* factorize into Inferno::Utils::IgDownloader

* rspec, rubocop, and clean

* update CLI "--help" docs

* replace "test suite app" with test kit"

---------

Co-authored-by: Stephen MacVicar <Jammjammjamm@users.noreply.github.com>
  • Loading branch information
Shaumik-Ashraf and Jammjammjamm authored Feb 27, 2024
1 parent 21b7aef commit 123ab58
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 20 deletions.
60 changes: 42 additions & 18 deletions lib/inferno/apps/cli/new.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
require 'thor'
require 'bundler'
require_relative '../../utils/named_thor_actions'
require_relative '../../utils/ig_downloader'
require_relative '../../version'

module Inferno
module CLI
class New < Thor::Group
include Thor::Actions
include Inferno::Utils::NamedThorActions
include Inferno::Utils::IgDownloader

desc <<~HELP
Generate a new Inferno test kit for FHIR software testing
Examples:
`inferno new test_fhir_app`
=> generates an Inferno app
`inferno new my_test_kit`
=> generates an Inferno test kit
`inferno new test_my_ig -a MyName`
=> generates Inferno app and specifies MyName as gemspec author
`inferno new test-us-core -i hl7.fhir.us.core@6.1.0`
=> generates Inferno test kit with US Core 6.1.0 implementation guide
`inferno new TestMatching -i https://build.fhir.org/ig/HL7/fhir-identity-matching-ig/`
=> generates Inferno test kit with an implementation guide from its continuous web build
`inferno new test-my-ig -a "My Name" -i file:///absolute/path/to/my/ig/package.tgz`
=> generates Inferno test kit with a local IG and specifies My Name as gem author
`inferno new test_my_igs -a "My Name" -a "Another Name" -i file:///my/first/package.tgz -i hl7.fhir.us.core@6.1.0`
=> generates Inferno test kit with multiple IGs and multiple authors
https://inferno-framework.github.io/index.html
HELP
Expand All @@ -45,15 +56,23 @@ def self.source_root
type: :boolean,
aliases: '-b',
default: false,
desc: 'Do not run bundle install'
desc: 'Do not run bundle install or inferno migrate'
class_option :implementation_guide,
type: :string,
aliases: '-i',
repeatable: true,
desc: 'Load an Implementation Guide by FHIR Registry name, URL, or absolute path'

add_runtime_options!

def create_app
def create_test_kit
directory('.', root_name, { mode: :preserve, recursive: true, verbose: !options['quiet'] })

bundle_install
inferno_migrate
inside(root_name) do
bundle_install
inferno_migrate
load_igs
end

say_unless_quiet "Created #{root_name} Inferno test kit!", :green

Expand All @@ -65,10 +84,6 @@ def create_app

private

def ig_path
File.join('lib', library_name, 'igs')
end

def authors
options['author'].presence || [default_author]
end
Expand All @@ -80,18 +95,27 @@ def default_author
def bundle_install
return if options['skip_bundle']

inside(root_name) do
Bundler.with_unbundled_env do
run 'bundle install', verbose: !options['quiet'], capture: options['quiet']
end
Bundler.with_unbundled_env do
run 'bundle install', verbose: !options['quiet'], capture: options['quiet']
end
end

def inferno_migrate
return if options['skip_bundle']

inside(root_name) do
run 'bundle exec inferno migrate', verbose: !options['quiet'], capture: options['quiet']
run 'bundle exec inferno migrate', verbose: !options['quiet'], capture: options['quiet']
end

def load_igs
config = { verbose: !options['quiet'] }
options['implementation_guide']&.each_with_index do |ig, idx|
uri = options['implementation_guide'].length == 1 ? load_ig(ig, nil, config) : load_ig(ig, idx, config)
say_unless_quiet "Downloaded IG from #{uri}"
rescue OpenURI::HTTPError => e
say_unless_quiet "Failed to install implementation guide #{ig}", :red
say_unless_quiet e.message, :red
rescue StandardError => e
say_unless_quiet e.message, :red
end
end

Expand Down
57 changes: 57 additions & 0 deletions lib/inferno/utils/ig_downloader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module Inferno
module Utils
module IgDownloader
FHIR_PACKAGE_NAME_REG_EX = /^[a-z][a-zA-Z0-9-]*\.([a-z][a-zA-Z0-9-]*\.?)*/
HTTP_URI_REG_EX = %r{^https?://[^/?#]+[^?#]*}
FILE_URI_REG_EX = %r{^file://(.+)}
HTTP_URI_END_REG_EX = %r{[^/]*\.x?html?$}

def ig_path
File.join('lib', library_name, 'igs')
end

def ig_file(suffix = nil)
File.join(ig_path, suffix ? "package_#{suffix}.tgz" : 'package.tgz')
end

def load_ig(ig_input, idx = nil, thor_config = { verbose: true })
case ig_input
when FHIR_PACKAGE_NAME_REG_EX
uri = ig_registry_url(ig_input)
when HTTP_URI_REG_EX
uri = ig_http_url(ig_input)
when FILE_URI_REG_EX
uri = ig_input[7..]
else
raise StandardError, <<~FAILED_TO_LOAD
Could not find implementation guide: #{ig_input}
Put its package.tgz file directly in #{ig_path}
FAILED_TO_LOAD
end

# use Thor's get to support CLI options config
get(uri, ig_file(idx), thor_config)
uri
end

def ig_registry_url(ig_npm_style)
unless ig_npm_style.include? '@'
raise StandardError, <<~NO_VERSION
No IG version specified for #{ig_npm_style}; you must specify one with '@'. I.e: hl7.fhir.us.core@6.1.0
NO_VERSION
end

package_name, version = ig_npm_style.split('@')
"https://packages.fhir.org/#{package_name}/-/#{package_name}-#{version}.tgz"
end

def ig_http_url(ig_page_url)
return ig_page_url if ig_page_url.end_with? 'package.tgz'

return "#{ig_page_url}package.tgz" if ig_page_url.end_with? '/'

ig_page_url.gsub(HTTP_URI_END_REG_EX, 'package.tgz')
end
end
end
end
16 changes: 14 additions & 2 deletions spec/inferno/apps/cli/new_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'rspec'
require 'inferno/apps/cli/new'

ABSOLUTE_PATH_TO_IG = File.expand_path('../../../fixtures/small_package.tgz', __dir__)
PACKAGE_FIXTURE = File.expand_path('../../../fixtures/small_package.tgz', __dir__)

RSpec.describe Inferno::CLI::New do # rubocop:disable RSpec/FilePath
around do |test|
Expand All @@ -15,7 +15,10 @@
[
%w[test-fhir-app],
%w[test-fhir-app --author ABC],
%w[test-fhir-app --author ABC --author DEF]
%w[test-fhir-app --author ABC --author DEF],
%W[test-fhir-app --implementation-guide file://#{PACKAGE_FIXTURE}],
%W[test-fhir-app --implementation-guide file://#{PACKAGE_FIXTURE} --implementation-guide file://#{PACKAGE_FIXTURE}],
%W[test-fhir-app --author ABC --implementation-guide file://#{PACKAGE_FIXTURE}]
].each do |cli_args|
cli_args.append('--quiet')
cli_args.append('--skip-bundle')
Expand All @@ -35,6 +38,15 @@
if cli_args.count('--author') == 2
expect(File.read('test-fhir-app/test_fhir_app.gemspec')).to match(/authors\s*=.*ABC.*DEF/)
end

if cli_args.count('--implementation-guide') == 1
expect(File).to exist('test-fhir-app/lib/test_fhir_app/igs/package.tgz')
end

if cli_args.count('--implementation-guide') == 2
expect(File).to exist('test-fhir-app/lib/test_fhir_app/igs/package_0.tgz')
expect(File).to exist('test-fhir-app/lib/test_fhir_app/igs/package_1.tgz')
end
end
end
end
165 changes: 165 additions & 0 deletions spec/inferno/utils/ig_downloader_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
require 'open-uri'
require 'thor'
require_relative '../../../lib/inferno/utils/ig_downloader'

def with_temp_path(name)
path = File.join(Inferno::Application.root, 'tmp', "rspec-#{name.sum}.tmp")
yield(path)
File.delete(path) if File.exist?(path)
end

RSpec.describe Inferno::Utils::IgDownloader do
let(:ig_downloader_class) do
Class.new do
include Thor::Base
include Thor::Actions
include Inferno::Utils::IgDownloader
attr_accessor :library_name

source_root Inferno::Application.root
end
end
let(:ig_downloader) do
ig_downloader_instance = ig_downloader_class.new
ig_downloader_instance.library_name = 'udap'
ig_downloader_instance
end
let(:package_fixture) { File.expand_path('../../fixtures/small_package.tgz', __dir__) }
let(:package_binary) { File.read(package_fixture) }

describe '#ig_path' do
it 'builds correct path to IGs' do
expect(ig_downloader.ig_path).to eq('lib/udap/igs')
end
end

describe '#ig_file' do
it 'builds correct IG file path' do
expect(ig_downloader.ig_file).to eq('lib/udap/igs/package.tgz')
end

it 'suffixes IG file path' do
expect(ig_downloader.ig_file(99)).to eq('lib/udap/igs/package_99.tgz')
end
end

context 'with IG by canonical name' do
let(:canonical) { 'hl7.fhir.us.udap-security@1.0.0' }
let(:resolved_url) do
'https://packages.fhir.org/hl7.fhir.us.udap-security/-/hl7.fhir.us.udap-security-1.0.0.tgz'
end

describe 'FHIR_PACKAGE_NAME_REG_EX' do
it 'matches given canonical name' do
expect(canonical).to match(Inferno::Utils::IgDownloader::FHIR_PACKAGE_NAME_REG_EX)
end
end

describe '#ig_registry_url' do
it 'returns correct registry url' do
expect(ig_downloader.ig_registry_url(canonical)).to eq(resolved_url)
end

it 'raises standard error if missing version' do
expect { ig_downloader.ig_registry_url('hl7.fhir.us.udap-security') }.to raise_error(StandardError)
end
end

describe '#load_ig' do
it 'successfully downloads package' do
stub_request(:get, 'https://packages.fhir.org/hl7.fhir.us.udap-security/-/hl7.fhir.us.udap-security-1.0.0.tgz')
.to_return(body: package_binary)

with_temp_path('ig-downloader-canonical') do |temp_path|
allow(ig_downloader).to receive(:ig_file).and_return(temp_path)
ig_downloader.load_ig(canonical, nil, { verbose: false })
expect(File.read(temp_path)).to eq(package_binary)
end
end
end
end

%w[
https://build.fhir.org/ig/HL7/fhir-udap-security-ig/package.tgz
https://build.fhir.org/ig/HL7/fhir-udap-security-ig/
https://build.fhir.org/ig/HL7/fhir-udap-security-ig/index.html
https://build.fhir.org/ig/HL7/fhir-udap-security-ig/downloads.html
http://build.fhir.org/ig/HL7/fhir-udap-security-ig/
].each do |url|
context "with IG by http URL #{url}" do
describe 'HTTP_URI_REG_EX' do
it 'matches given url' do
expect(url).to match(Inferno::Utils::IgDownloader::HTTP_URI_REG_EX)
end
end

describe 'FHIR_PACKAGE_NAME_REG_EX' do
it 'does not match given url' do
expect(url).to_not match(Inferno::Utils::IgDownloader::FHIR_PACKAGE_NAME_REG_EX)
end
end

describe '#ig_http_url' do
it 'normalizes to a package.tgz url' do
expect(ig_downloader.ig_http_url(url))
.to match(%r{https?://build.fhir.org/ig/HL7/fhir-udap-security-ig/package.tgz})
end
end

describe '#load_ig' do
it 'successfully downloads package' do
stub_request(:get, %r{https?://build.fhir.org}).to_return(body: package_binary)
with_temp_path("ig-downloader-#{url}") do |temp_path|
allow(ig_downloader).to receive(:ig_file).and_return(temp_path)
ig_downloader.load_ig(url, nil, { verbose: false })
expect(File.read(temp_path)).to eq(package_binary)
end
end
end
end
end

context 'with IG by absolute file path' do
let(:absolute_path) { "file://#{PACKAGE_FIXTURE}" }

describe 'FILE_URI_REG_EX' do
it 'matches given file uri' do
expect(absolute_path).to match(Inferno::Utils::IgDownloader::FILE_URI_REG_EX)
end
end

describe 'HTTP_URI_REG_EX' do
it 'does not match given file uri' do
expect(absolute_path).to_not match(Inferno::Utils::IgDownloader::HTTP_URI_REG_EX)
end
end

describe 'FHIR_PACKAGE_NAME_REG_EX' do
it 'does not match given file uri' do
expect(absolute_path).to_not match(Inferno::Utils::IgDownloader::FHIR_PACKAGE_NAME_REG_EX)
end
end

describe '#load_ig' do
it 'successfully downloads package' do
with_temp_path('ig-downloader-file') do |temp_path|
allow(ig_downloader).to receive(:ig_file).and_return(temp_path)
ig_downloader.load_ig(absolute_path, nil, { verbose: false })
expect(File.read(temp_path)).to eq(package_binary)
end
end
end
end

describe '#load_ig' do
it 'with bad input raises standard error' do
expect { ig_downloader.load_ig('bad') }.to raise_error(StandardError)
end

it 'with bad url raises http error' do
bad_url = 'http://bad.example.com/package.tgz'
stub_request(:get, bad_url).to_return(status: 404)
expect { ig_downloader.load_ig(bad_url) }.to raise_error(OpenURI::HTTPError)
end
end
end

0 comments on commit 123ab58

Please sign in to comment.