Skip to content

Commit f150cd0

Browse files
committed
FI-3440 review feedback, part 1
1 parent a904f15 commit f150cd0

File tree

7 files changed

+186
-102
lines changed

7 files changed

+186
-102
lines changed

lib/inferno/apps/cli/evaluate.rb

+28-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require_relative '../../../inferno/dsl/fhir_evaluation/evaluator'
2-
require_relative '../../../inferno/dsl/ig'
2+
require_relative '../../../inferno/entities'
33
require_relative '../../utils/ig_downloader'
44

55
require 'tempfile'
@@ -12,15 +12,7 @@ class Evaluate < Thor::Group
1212

1313
def evaluate(ig_path, data_path, _log_level)
1414
validate_args(ig_path, data_path)
15-
16-
if File.file?(ig_path)
17-
ig = Inferno::DSL::IG.from_file(ig_path)
18-
else
19-
Tempfile.create('package.tgz') do |temp_file|
20-
load_ig(ig_path, nil, { force: true }, temp_file.path)
21-
ig = Inferno::DSL::IG.from_file(temp_file.path)
22-
end
23-
end
15+
_ig = get_ig(ig_path)
2416

2517
# Rule execution, and result output below will be integrated soon.
2618

@@ -45,6 +37,32 @@ def validate_args(ig_path, data_path)
4537
raise "Provided path '#{data_path}' is not a directory"
4638
end
4739

40+
def get_ig(ig_path)
41+
if File.exist?(ig_path)
42+
ig = Inferno::Entities::IG.from_file(ig_path)
43+
elsif in_user_package_cache?(ig_path.sub('@', '#'))
44+
# NPM syntax for a package identifier is id@version (eg, hl7.fhir.us.core@3.1.1)
45+
# but in the cache the separator is # (hl7.fhir.us.core#3.1.1)
46+
cache_directory = File.join(user_package_cache, ig_path.sub('@', '#'))
47+
ig = Inferno::Entities::IG.from_file(cache_directory)
48+
else
49+
Tempfile.create('package.tgz') do |temp_file|
50+
load_ig(ig_path, nil, { force: true }, temp_file.path)
51+
ig = Inferno::Entities::IG.from_file(temp_file.path)
52+
end
53+
end
54+
ig.add_self_to_repository
55+
ig
56+
end
57+
58+
def user_package_cache
59+
File.join(Dir.home, '.fhir', 'packages')
60+
end
61+
62+
def in_user_package_cache?(ig_identifier)
63+
File.directory?(File.join(user_package_cache, ig_identifier))
64+
end
65+
4866
def output_results(results, output)
4967
if output&.end_with?('json')
5068
oo = FhirEvaluator::EvaluationResult.to_operation_outcome(results)

lib/inferno/dsl/ig.rb

-90
This file was deleted.

lib/inferno/entities.rb

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require_relative 'entities/has_runnable'
33
require_relative 'entities/entity'
44
require_relative 'entities/header'
5+
require_relative 'entities/ig'
56
require_relative 'entities/message'
67
require_relative 'entities/request'
78
require_relative 'entities/result'

lib/inferno/entities/ig.rb

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# frozen_string_literal: true
2+
3+
require 'fhir_models'
4+
require 'rubygems/package'
5+
require 'zlib'
6+
7+
require_relative '../repositories/igs'
8+
9+
module Inferno
10+
module Entities
11+
# IG is a wrapper class around the relevant concepts inside an IG.
12+
# Not everything within an IG is currently used by Inferno.
13+
class IG < Entity
14+
ATTRIBUTES = [
15+
:id,
16+
:profiles,
17+
:extensions,
18+
:value_sets,
19+
:search_params,
20+
:examples
21+
].freeze
22+
23+
include Inferno::Entities::Attributes
24+
25+
def initialize(params)
26+
super(params, ATTRIBUTES)
27+
28+
@profiles = []
29+
@extensions = []
30+
@value_sets = []
31+
@examples = []
32+
@search_params = []
33+
end
34+
35+
def self.from_file(ig_path)
36+
raise "#{ig_path} does not exist" unless File.exist?(ig_path)
37+
38+
# fhir_models by default logs the entire content of non-FHIR files
39+
# which could be things like a package.json
40+
original_logger = FHIR.logger
41+
FHIR.logger = Logger.new('/dev/null')
42+
43+
if File.directory?(ig_path)
44+
from_directory(ig_path)
45+
elsif ig_path.end_with? '.tgz'
46+
from_tgz(ig_path)
47+
else
48+
raise "Unable to load #{ig_path} as it does not appear to be a directory or a .tgz file"
49+
end
50+
ensure
51+
FHIR.logger = original_logger if defined? original_logger
52+
end
53+
54+
def self.from_tgz(ig_path)
55+
tar = Gem::Package::TarReader.new(
56+
Zlib::GzipReader.open(ig_path)
57+
)
58+
59+
ig = IG.new({})
60+
61+
tar.each do |entry|
62+
next if skip_item?(entry.full_name, entry.directory?)
63+
64+
begin
65+
resource = FHIR::Json.from_json(entry.read)
66+
next if resource.nil?
67+
68+
ig.handle_resource(resource, entry.full_name)
69+
rescue StandardError
70+
next
71+
end
72+
end
73+
ig
74+
end
75+
76+
def self.from_directory(ig_path)
77+
ig = IG.new({})
78+
79+
Dir.glob("#{ig_path}/**/*") do |f|
80+
next if skip_item?(f, File.directory?(f))
81+
82+
begin
83+
resource = FHIR::Json.from_json(File.read(f))
84+
next if resource.nil?
85+
86+
ig.handle_resource(resource, f)
87+
rescue StandardError
88+
next
89+
end
90+
end
91+
ig
92+
end
93+
94+
# These files aren't FHIR resources
95+
FILES_TO_SKIP = ['package.json', 'validation-summary.json'].freeze
96+
97+
def self.skip_item?(relative_path, is_directory)
98+
return true if is_directory
99+
100+
file_name = relative_path.split('/').last
101+
102+
# TODO: consider making these regexes we can iterate over in a single loop
103+
return true unless file_name.end_with? '.json'
104+
return true unless relative_path.start_with? 'package/'
105+
106+
return true if file_name.start_with? '.' # ignore hidden files
107+
return true if file_name.end_with? '.openapi.json'
108+
return true if FILES_TO_SKIP.include? file_name
109+
110+
false
111+
end
112+
113+
def handle_resource(resource, relative_path)
114+
case resource.resourceType
115+
when 'StructureDefinition'
116+
if resource.type == 'Extension'
117+
extensions.push resource
118+
else
119+
profiles.push resource
120+
end
121+
when 'ValueSet'
122+
value_sets.push resource
123+
when 'SearchParameter'
124+
search_params.push resource
125+
when 'ImplementationGuide'
126+
@id = extract_package_id(resource)
127+
else
128+
examples.push(resource) if relative_path.start_with? 'package/example'
129+
end
130+
end
131+
132+
def extract_package_id(ig_resource)
133+
"#{ig_resource.id}##{ig_resource.version || 'current'}"
134+
end
135+
136+
# @private
137+
def add_self_to_repository
138+
repository.insert(self)
139+
end
140+
141+
# @private
142+
def repository
143+
Inferno::Repositories::IGs.new
144+
end
145+
end
146+
end
147+
end

lib/inferno/repositories/igs.rb

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require_relative 'in_memory_repository'
2+
3+
module Inferno
4+
module Repositories
5+
# Repository that deals with persistence for the `IG` entity.
6+
class IGs < InMemoryRepository
7+
end
8+
end
9+
end

lib/inferno/utils/ig_downloader.rb

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ def load_ig(ig_input, idx = nil, thor_config = { verbose: true }, output_path =
2525
else
2626
raise StandardError, <<~FAILED_TO_LOAD
2727
Could not find implementation guide: #{ig_input}
28-
Put its package.tgz file directly in #{ig_path}
2928
FAILED_TO_LOAD
3029
end
3130

spec/inferno/dsl/ig_spec.rb spec/inferno/entities/ig_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
RSpec.describe Inferno::DSL::IG do
1+
RSpec.describe Inferno::Entities::IG do
22
let(:uscore3_package) { File.expand_path('../../fixtures/uscore311.tgz', __dir__) }
33

44
describe '#from_file' do

0 commit comments

Comments
 (0)