Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FI-3376 Migrate Evaluator CLI into inferno core CLI #557

Merged
merged 38 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cdc5dfb
Copied evaluator files
jhlee-mitre Nov 15, 2024
c8618cf
Rubocop
jhlee-mitre Nov 15, 2024
18c0c06
Robocop
jhlee-mitre Nov 15, 2024
58446f2
Rubocop temporarily addressed
jhlee-mitre Nov 15, 2024
674983d
Merge branch 'main' into FI-3376-evaluator-firststep
jhlee-mitre Nov 15, 2024
57b6e0c
Remove old comment
jhlee-mitre Nov 19, 2024
f363efc
Removed internal valiadator method
jhlee-mitre Nov 19, 2024
be8db9f
Leave out DataSummary
jhlee-mitre Nov 20, 2024
b73eaf3
Remove evaluator version
jhlee-mitre Nov 21, 2024
c7ddf37
Long description
jhlee-mitre Nov 22, 2024
2a81a63
rubocop
jhlee-mitre Nov 22, 2024
1d4662d
WIP
jhlee-mitre Nov 26, 2024
bd9aaa1
WIP
jhlee-mitre Nov 26, 2024
d1f2e83
Data set loader
jhlee-mitre Nov 26, 2024
e3b2d3a
IG path resolved
jhlee-mitre Nov 26, 2024
2120d40
WIP
jhlee-mitre Nov 29, 2024
7e35a76
WIP
jhlee-mitre Nov 29, 2024
4a983f2
rubocop
jhlee-mitre Nov 29, 2024
7f3d0e3
Rubocop
jhlee-mitre Nov 29, 2024
5d1ef09
Update lib/inferno/dsl/fhir_evaluator/evaluator.rb
jhlee-mitre Dec 4, 2024
9569ff9
Remove rule files
jhlee-mitre Dec 4, 2024
936cafb
Remove data_summary
jhlee-mitre Dec 4, 2024
fd78251
WIP
jhlee-mitre Dec 6, 2024
6b7bb8b
Clean up
jhlee-mitre Dec 6, 2024
e372fcb
config
jhlee-mitre Dec 6, 2024
fb239df
Rename evaluator
jhlee-mitre Dec 7, 2024
f5f76c8
Args
jhlee-mitre Dec 9, 2024
f1a95d9
Update lib/inferno/dsl/fhir_evaluator/evaluator.rb
jhlee-mitre Dec 10, 2024
3314367
ig initialize, missing_method
jhlee-mitre Dec 10, 2024
2c08f76
Merge branch 'main' into FI-3376-evaluator-firststep
jhlee-mitre Dec 10, 2024
3d1feff
WIP
jhlee-mitre Dec 10, 2024
34e46ed
the_ig
jhlee-mitre Dec 10, 2024
58830ae
Disable rubocop for IG
jhlee-mitre Dec 11, 2024
a0fd36b
Remove print
jhlee-mitre Dec 11, 2024
a5dcc19
Folder file names
jhlee-mitre Dec 11, 2024
013a42c
Removed config folder
jhlee-mitre Dec 11, 2024
1758e64
rubocop:disable Naming/MethodParameterName
jhlee-mitre Dec 12, 2024
796f383
Merge branch 'main' into FI-3376-evaluator-firststep
jhlee-mitre Dec 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions lib/inferno/apps/cli/evaluate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require_relative '../../../inferno/dsl/fhir_evaluation/evaluator'

module Inferno
module CLI
class Evaluate
def run(ig_path, data_path, _log_level)
validate_args(ig_path, data_path)

# IG Import, rule execution, and result output below will be integrated at phase 2 and 3.

# @ig = File.join(__dir__, 'ig', ig_path)
# if data_path
# DatasetLoader.from_path(File.join(__dir__, data_path))
# else
# ig.examples
# end

# config = Config.new
# evaluator = Inferno::DSL::FHIREvaluation::Evaluator.new(data, config)

# results = evaluate()
# output_results(results, options[:output])
end

def validate_args(ig_path, data_path)
raise 'A path to an IG is required!' unless ig_path

return unless data_path && (!File.directory? data_path)

raise "Provided path '#{data_path}' is not a directory"
end

def output_results(results, output)
if output&.end_with?('json')
oo = FhirEvaluator::EvaluationResult.to_operation_outcome(results)
File.write(output, oo.to_json)
puts "Results written to #{output}"
else
counts = results.group_by(&:severity).transform_values(&:count)
print(counts, 'Result Count')
puts "\n"
puts results
end
end

def print(output_fields, title)
puts("╔══════════════ #{title} ═══════════════╗")
puts('║ ╭────────────────┬──────────────────────╮ ║')
output_fields.each_with_index do |(key, value), i|
field_name = pad(key, 14)
field_value = pad(value.to_s, 20)
puts("║ │ #{field_name} │ #{field_value} │ ║")
puts('║ ├────────────────┼──────────────────────┤ ║') unless i == output_fields.length - 1
end
puts('║ ╰────────────────┴──────────────────────╯ ║')
puts('╚═══════════════════════════════════════════╝')
end

def pad(string, length)
format("%#{length}.#{length}s", string)
end
end
end
end
38 changes: 38 additions & 0 deletions lib/inferno/apps/cli/main.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require_relative 'console'
require_relative 'evaluate'
require_relative 'migration'
require_relative 'services'
require_relative 'suite'
Expand All @@ -10,6 +11,43 @@
module Inferno
module CLI
class Main < Thor
desc 'evaluate', 'Run a FHIR Data Evaluator.'
long_desc <<-LONGDESC
Evaluate FHIR data in the context of a given Implementation Guide,
by applying a set of predefined rules designed to check that datasets are comprehensive.
Issues identified will be printed to console or to a json file.

You must have background services running: `bundle exec inferno services start`

Run the evaluation CLI with

`bundle exec inferno evaluate ig_path`

Examples:

# Load the us core ig and evaluate the data in the provided example folder. If there are examples in the IG already, they will be ignored.
`bundle exec inferno evaluate ./uscore.tgz -d ./package/example`

# Loads the us core ig and evaluate the data included in the IG's example folder
`bundle exec inferno evaluate ./uscore.tgz`

# Loads the us core ig and evaluate the data included in the IG's example folder, with results redirected to outcome.json as an OperationOutcome
`bundle exec inferno evaluate ./uscore.tgz --output outcome.json`
LONGDESC
# TODO: Add options below as arguments
option :data_path,
aliases: ['-d'],
type: :string,
desc: 'Example FHIR data path'
# TODO: implement option of exporting result as OperationOutcome
option :output,
aliases: ['-o'],
type: :string,
desc: 'Export evaluation result to outcome.json as an OperationOutcome'
def evaluate(ig_path)
Evaluate.new.run(ig_path, options[:data_path], Logger::INFO)
end

desc 'console', 'Start an interactive console session with Inferno'
def console
Migration.new.run(Logger::INFO)
Expand Down
2 changes: 2 additions & 0 deletions lib/inferno/dsl.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require_relative 'dsl/assertions'
require_relative 'dsl/fhir_client'
require_relative 'dsl/fhir_validation'
require_relative 'dsl/fhir_evaluation/evaluator'
require_relative 'dsl/fhir_resource_validation'
require_relative 'dsl/fhirpath_evaluation'
require_relative 'dsl/http_client'
Expand All @@ -18,6 +19,7 @@ module DSL
HTTPClient,
Results,
FHIRValidation,
FHIREvaluation,
FHIRResourceValidation,
FhirpathEvaluation,
Messages
Expand Down
21 changes: 21 additions & 0 deletions lib/inferno/dsl/fhir_evaluation/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Inferno
module DSL
module FHIREvaluation
class Config
DEFAULT_FILE = File.join(__dir__, 'default.yml')
attr_accessor :data

# To-do: add config_file as arguments
def initialize(config_file = nil)
@data = if config_file.nil?
YAML.load_file(File.absolute_path(DEFAULT_FILE))
else
YAML.load_file(File.absolute_path(config_file))
end

raise(TypeError, 'Malformed configuration') unless @data.is_a?(Hash)
end
end
end
end
end
33 changes: 33 additions & 0 deletions lib/inferno/dsl/fhir_evaluation/dataset_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Inferno
module DSL
module FHIREvaluation
module DatasetLoader
def self.from_contents(source_array)
dataset = []

source_array.each do |json|
resource = FHIR::Json.from_json(json)
next if resource.nil?

dataset.push resource
end

dataset
end

def self.from_path(path)
dataset = []

Dir["#{path}/*.json"].each do |f|
resource = FHIR::Json.from_json(File.read(f))
next if resource.nil?

dataset.push resource
end

dataset
end
end
end
end
end
9 changes: 9 additions & 0 deletions lib/inferno/dsl/fhir_evaluation/default.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Environment:
ExternalValidator:
Enabled: false
Url: ''
VSAC:
Apikey: ''
Url: 'https://cts.nlm.nih.gov'

Rule:
27 changes: 27 additions & 0 deletions lib/inferno/dsl/fhir_evaluation/evaluation_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Inferno
module DSL
module FHIREvaluation
# EvaluationContext is a wrapper class around the concepts needed to perform an evaluation:
# - The IG used as the basis for evaluation
# - The data being evaluated
# - A summary/characterization of the data
# - Evaluation results
class EvaluationContext
attr_reader :ig, :data, :results, :config

# rubocop:disable Naming/MethodParameterName
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using a disable and an enable, just put the disable at the end of the relevant line, and then it will only affect that line and you don't have to reenable the rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tips. Modified.

def initialize(ig, data, config)
@ig = ig
@data = data
@results = []
@config = config
end
# rubocop:enable Naming/MethodParameterName

def add_result(result)
results.push result
end
end
end
end
end
62 changes: 62 additions & 0 deletions lib/inferno/dsl/fhir_evaluation/evaluation_result.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Inferno
module DSL
module FHIREvaluation
# The result of a Rule evaluating a data set.
class EvaluationResult
attr_accessor :message,
:severity, # fatal | error | warning | information | success
:issue_type, # https://www.hl7.org/fhir/valueset-issue-type.html
:threshold, # quantitative value that a rule checks for
:value, # actual observed value
:rule # Rule that produced this result

def initialize(message, severity: 'warning', issue_type: 'business-rule', threshold: nil, value: nil, rule: nil)
@message = message
@severity = severity
@issue_type = issue_type
@threshold = threshold
@value = value
@rule = rule
end

def to_s
"#{severity.upcase}: #{message}"
end

def to_oo_issue
issue = {
severity:,
code: issue_type,
details: { text: message }
}

if threshold
issue[:extension] ||= []
issue[:extension].push({
# TODO: pick real extension for this
url: 'https://inferno-framework.github.io/fhir_evaluator/StructureDefinition/operationoutcome-issue-threshold',
valueDecimal: threshold
})
end

if value
issue[:extension] ||= []
issue[:extension].push({
# TODO: pick real extension for this
url: 'https://inferno-framework.github.io/fhir_evaluator/StructureDefinition/operationoutcome-issue-value',
valueDecimal: value
})
end

issue
end

def self.to_operation_outcome(results)
FHIR::OperationOutcome.new({
issue: results.map(&:to_oo_issue)
})
end
end
end
end
end
38 changes: 38 additions & 0 deletions lib/inferno/dsl/fhir_evaluation/evaluator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

require_relative 'config'
require_relative 'rule'
require_relative 'evaluation_context'
require_relative 'evaluation_result'
require_relative 'dataset_loader'

module Inferno
module DSL
module FHIREvaluation
class Evaluator
attr_accessor :ig

# rubocop:disable Naming/MethodParameterName
def initialize(ig)
@ig = ig
end
# rubocop:enable Naming/MethodParameterName

def evaluate(data, config = Config.new)
context = EvaluationContext.new(@ig, data, config)

active_rules = []
config.data['Rule'].each do |rulename, rule_details|
active_rules << rulename if rule_details['Enabled']
end

Rule.descendants.each do |rule|
rule.new.check(context) if active_rules.include?(rule.name.demodulize)
end

context.results
end
end
end
end
end
13 changes: 13 additions & 0 deletions lib/inferno/dsl/fhir_evaluation/rule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module Inferno
module DSL
module FHIREvaluation
class Rule
def check(_context)
raise 'not implemented'
end
end
end
end
end
Loading