cqm-parsers 0.2.3 → 3.1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Gemfile +8 -4
- data/README.md +57 -5
- data/Rakefile +1 -0
- data/lib/{hqmf-parser.rb → cqm-parsers.rb} +13 -45
- data/lib/ext/data_element.rb +1 -1
- data/lib/hqmf-parser/2.0/document.rb +1 -1
- data/lib/hqmf-parser/2.0/population_criteria.rb +1 -1
- data/lib/hqmf-parser/cql/document_helpers/doc_population_helper.rb +11 -6
- data/lib/measure-loader/cql_loader.rb +165 -0
- data/lib/measure-loader/elm_dependency_finder.rb +72 -0
- data/lib/measure-loader/elm_parser.rb +67 -0
- data/lib/measure-loader/exceptions.rb +10 -0
- data/lib/measure-loader/helpers.rb +11 -0
- data/lib/measure-loader/hqmf_measure_loader.rb +170 -0
- data/lib/measure-loader/mat_measure_files.rb +138 -0
- data/lib/measure-loader/source_data_criteria_loader.rb +75 -0
- data/lib/measure-loader/value_set_helpers.rb +68 -0
- data/lib/measure-loader/vsac_value_set_loader.rb +97 -0
- data/lib/tasks/hqmf.rake +1 -1
- data/lib/util/util.rb +23 -0
- data/lib/util/vsac_api.rb +166 -103
- metadata +60 -127
- data/lib/ext/code.rb +0 -10
- data/lib/qrda-export/catI-r5/_code.mustache +0 -1
- data/lib/qrda-export/catI-r5/_codes.mustache +0 -10
- data/lib/qrda-export/catI-r5/_header.mustache +0 -28
- data/lib/qrda-export/catI-r5/_measure_section.mustache +0 -59
- data/lib/qrda-export/catI-r5/_reporting_period.mustache +0 -23
- data/lib/qrda-export/catI-r5/_values.mustache +0 -10
- data/lib/qrda-export/catI-r5/qrda1_r5.mustache +0 -137
- data/lib/qrda-export/catI-r5/qrda1_r5.rb +0 -125
- data/lib/qrda-export/catI-r5/qrda_header/_author.mustache +0 -24
- data/lib/qrda-export/catI-r5/qrda_header/_custodian.mustache +0 -43
- data/lib/qrda-export/catI-r5/qrda_header/_documentation_of_service_event.mustache +0 -82
- data/lib/qrda-export/catI-r5/qrda_header/_information_recipient.mustache +0 -7
- data/lib/qrda-export/catI-r5/qrda_header/_legal_authenticator.mustache +0 -25
- data/lib/qrda-export/catI-r5/qrda_header/_participant.mustache +0 -7
- data/lib/qrda-export/catI-r5/qrda_header/_record_target.mustache +0 -28
- data/lib/qrda-export/catI-r5/qrda_templates/adverse_event.mustache +0 -28
- data/lib/qrda-export/catI-r5/qrda_templates/allergy_intolerance.mustache +0 -28
- data/lib/qrda-export/catI-r5/qrda_templates/assessment_performed.mustache +0 -25
- data/lib/qrda-export/catI-r5/qrda_templates/communication_from_patient_to_provider.mustache +0 -29
- data/lib/qrda-export/catI-r5/qrda_templates/communication_from_provider_to_patient.mustache +0 -24
- data/lib/qrda-export/catI-r5/qrda_templates/communication_from_provider_to_provider.mustache +0 -31
- data/lib/qrda-export/catI-r5/qrda_templates/device_applied.mustache +0 -32
- data/lib/qrda-export/catI-r5/qrda_templates/device_ordered.mustache +0 -31
- data/lib/qrda-export/catI-r5/qrda_templates/diagnosis.mustache +0 -38
- data/lib/qrda-export/catI-r5/qrda_templates/diagnostic_study_ordered.mustache +0 -19
- data/lib/qrda-export/catI-r5/qrda_templates/diagnostic_study_performed.mustache +0 -32
- data/lib/qrda-export/catI-r5/qrda_templates/encounter_ordered.mustache +0 -24
- data/lib/qrda-export/catI-r5/qrda_templates/encounter_performed.mustache +0 -40
- data/lib/qrda-export/catI-r5/qrda_templates/immunization_administered.mustache +0 -29
- data/lib/qrda-export/catI-r5/qrda_templates/insurance_provider.mustache +0 -11
- data/lib/qrda-export/catI-r5/qrda_templates/intervention_ordered.mustache +0 -18
- data/lib/qrda-export/catI-r5/qrda_templates/intervention_performed.mustache +0 -25
- data/lib/qrda-export/catI-r5/qrda_templates/lab_test_ordered.mustache +0 -18
- data/lib/qrda-export/catI-r5/qrda_templates/lab_test_performed.mustache +0 -22
- data/lib/qrda-export/catI-r5/qrda_templates/medication_active.mustache +0 -35
- data/lib/qrda-export/catI-r5/qrda_templates/medication_administered.mustache +0 -31
- data/lib/qrda-export/catI-r5/qrda_templates/medication_discharge.mustache +0 -55
- data/lib/qrda-export/catI-r5/qrda_templates/medication_dispensed.mustache +0 -39
- data/lib/qrda-export/catI-r5/qrda_templates/medication_ordered.mustache +0 -38
- data/lib/qrda-export/catI-r5/qrda_templates/patient_characteristic_expired.mustache +0 -16
- data/lib/qrda-export/catI-r5/qrda_templates/physical_exam_performed.mustache +0 -25
- data/lib/qrda-export/catI-r5/qrda_templates/procedure_ordered.mustache +0 -19
- data/lib/qrda-export/catI-r5/qrda_templates/procedure_performed.mustache +0 -44
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_admission_source.mustache +0 -6
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_anatomical_location_site.mustache +0 -1
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_author.mustache +0 -7
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_author_participation.mustache +0 -7
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_component.mustache +0 -11
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_encounter_diagnosis.mustache +0 -19
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_encounter_facility_location.mustache +0 -16
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_mediation_frequency.mustache +0 -3
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_medication_details.mustache +0 -11
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_ordinality.mustache +0 -1
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_principal_diagnosis.mustache +0 -8
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_reason.mustache +0 -12
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_related_to.mustache +0 -6
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_results.mustache +0 -19
- data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_severity.mustache +0 -8
- data/lib/qrda-export/helper/cat_1_view_helper.rb +0 -146
- data/lib/qrda-export/helper/code_system_helper.rb +0 -77
- data/lib/qrda-export/helper/date_helper.rb +0 -89
- data/lib/qrda-import/base-importers/demographics_importer.rb +0 -49
- data/lib/qrda-import/base-importers/medication_importer.rb +0 -23
- data/lib/qrda-import/base-importers/section_importer.rb +0 -196
- data/lib/qrda-import/cda_identifier.rb +0 -19
- data/lib/qrda-import/data-element-importers/adverse_event_importer.rb +0 -24
- data/lib/qrda-import/data-element-importers/allergy_intolerance_importer.rb +0 -22
- data/lib/qrda-import/data-element-importers/assessment_performed_importer.rb +0 -24
- data/lib/qrda-import/data-element-importers/communication_from_patient_to_provider_importer.rb +0 -20
- data/lib/qrda-import/data-element-importers/communication_from_provider_to_patient_importer.rb +0 -20
- data/lib/qrda-import/data-element-importers/communication_from_provider_to_provider_importer.rb +0 -22
- data/lib/qrda-import/data-element-importers/device_applied_importer.rb +0 -24
- data/lib/qrda-import/data-element-importers/device_order_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/diagnosis_importer.rb +0 -24
- data/lib/qrda-import/data-element-importers/diagnostic_study_order_importer.rb +0 -21
- data/lib/qrda-import/data-element-importers/diagnostic_study_performed_importer.rb +0 -31
- data/lib/qrda-import/data-element-importers/encounter_order_importer.rb +0 -21
- data/lib/qrda-import/data-element-importers/encounter_performed_importer.rb +0 -42
- data/lib/qrda-import/data-element-importers/immunization_administered_importer.rb +0 -18
- data/lib/qrda-import/data-element-importers/intervention_order_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/intervention_performed_importer.rb +0 -23
- data/lib/qrda-import/data-element-importers/laboratory_test_order_importer.rb +0 -21
- data/lib/qrda-import/data-element-importers/laboratory_test_performed_importer.rb +0 -29
- data/lib/qrda-import/data-element-importers/medication_active_importer.rb +0 -17
- data/lib/qrda-import/data-element-importers/medication_administered_importer.rb +0 -17
- data/lib/qrda-import/data-element-importers/medication_discharge_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/medication_dispensed_importer.rb +0 -19
- data/lib/qrda-import/data-element-importers/medication_order_importer.rb +0 -16
- data/lib/qrda-import/data-element-importers/patient_characteristic_expired.rb +0 -22
- data/lib/qrda-import/data-element-importers/physical_exam_performed_importer.rb +0 -27
- data/lib/qrda-import/data-element-importers/procedure_order_importer.rb +0 -27
- data/lib/qrda-import/data-element-importers/procedure_performed_importer.rb +0 -35
- data/lib/qrda-import/data-element-importers/substance_administered_importer.rb +0 -17
- data/lib/qrda-import/entry_finder.rb +0 -20
- data/lib/qrda-import/entry_package.rb +0 -16
- data/lib/qrda-import/narrative_reference_handler.rb +0 -33
- data/lib/qrda-import/patient_importer.rb +0 -111
@@ -0,0 +1,68 @@
|
|
1
|
+
module Measures
|
2
|
+
module ValueSetHelpers
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# Adjusting value set version data. If version is profile, set the version to nil
|
6
|
+
def modify_value_set_versions(elm)
|
7
|
+
(elm.dig('library','valueSets','def') || []).each do |value_set|
|
8
|
+
# If value set has a version and it starts with 'urn:hl7:profile:' then set to nil
|
9
|
+
if value_set['version']&.include?('urn:hl7:profile:')
|
10
|
+
value_set['profile'] = URI.decode_www_form_component(value_set['version'].split('urn:hl7:profile:').last)
|
11
|
+
value_set['version'] = nil
|
12
|
+
# If value has a version and it starts with 'urn:hl7:version:' then strip that and keep the actual version value.
|
13
|
+
elsif value_set['version']&.include?('urn:hl7:version:')
|
14
|
+
value_set['version'] = URI.decode_www_form_component(value_set['version'].split('urn:hl7:version:').last)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Removes 'urn:oid:' from ELM
|
20
|
+
def remove_urnoid(json)
|
21
|
+
Utilities.deep_traverse_hash(json) { |hash, k, v| hash[k] = v.gsub('urn:oid:', '') if v.is_a?(String) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def unique_list_of_valuesets_referenced_by_elms(elms)
|
25
|
+
elm_value_sets = []
|
26
|
+
elms.each do |elm|
|
27
|
+
elm.dig('library','valueSets','def')&.each do |value_set|
|
28
|
+
elm_value_sets << {oid: value_set['id'], version: value_set['version'], profile: value_set['profile']}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
elm_value_sets.uniq!
|
32
|
+
return elm_value_sets
|
33
|
+
end
|
34
|
+
|
35
|
+
# Add single code references by finding the codes from the elm and creating new ValueSet objects
|
36
|
+
# With a generated GUID as a fake oid.
|
37
|
+
def make_fake_valuesets_from_single_code_references(elms, vs_model_cache)
|
38
|
+
value_sets_from_single_code_references = []
|
39
|
+
|
40
|
+
elms.each do |elm|
|
41
|
+
# Loops over all single codes and saves them as fake valuesets.
|
42
|
+
(elm.dig('library','codes','def') || []).each do |code_reference|
|
43
|
+
# look up the referenced code system
|
44
|
+
code_system_def = elm['library']['codeSystems']['def'].find { |code_sys| code_sys['name'] == code_reference['codeSystem']['name'] }
|
45
|
+
# Generate a unique number as our fake "oid" based on parameters that identify the DRC
|
46
|
+
code_hash = "drc-" + Digest::SHA2.hexdigest("#{code_system_def['id']}#{code_system_def['version']}#{code_reference['id']}")
|
47
|
+
|
48
|
+
cache_key = [code_hash, '']
|
49
|
+
if vs_model_cache[cache_key].nil?
|
50
|
+
concept = CQM::Concept.new(code: code_reference['id'],
|
51
|
+
code_system_name: code_system_def['name'],
|
52
|
+
code_system_version: code_system_def['version'],
|
53
|
+
code_system_oid: code_system_def['id'],
|
54
|
+
display_name: code_reference['name'])
|
55
|
+
vs_model_cache[cache_key] = CQM::ValueSet.new(oid: code_hash,
|
56
|
+
display_name: code_reference['name'],
|
57
|
+
version: '',
|
58
|
+
concepts: [concept])
|
59
|
+
end
|
60
|
+
value_sets_from_single_code_references << vs_model_cache[cache_key]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
return value_sets_from_single_code_references
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Measures
|
2
|
+
# Utility class for loading value sets
|
3
|
+
class VSACValueSetLoader
|
4
|
+
attr_accessor :vsac_options, :vs_model_cache
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
options.symbolize_keys!
|
8
|
+
@vsac_options = options[:options]
|
9
|
+
@vsac_ticket_granting_ticket = options[:ticket_granting_ticket]
|
10
|
+
@vsac_username = options[:username]
|
11
|
+
@vsac_password = options[:password]
|
12
|
+
@vs_model_cache = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def retrieve_and_modelize_value_sets_from_vsac(value_sets)
|
16
|
+
vs_models = []
|
17
|
+
needed_value_sets = []
|
18
|
+
|
19
|
+
value_sets.each do |value_set|
|
20
|
+
vs_vsac_options = make_specific_value_set_options(value_set)
|
21
|
+
query_version = determine_query_version(vs_vsac_options)
|
22
|
+
|
23
|
+
cache_key = [value_set[:oid], query_version]
|
24
|
+
vs_model = @vs_model_cache[cache_key]
|
25
|
+
if vs_model.present?
|
26
|
+
vs_models << vs_model
|
27
|
+
else
|
28
|
+
needed_value_sets << {value_set: value_set,
|
29
|
+
vs_vsac_options: vs_vsac_options,
|
30
|
+
query_version: query_version,
|
31
|
+
cache_key: cache_key}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
vs_responses = load_api.get_multiple_valuesets(needed_value_sets)
|
36
|
+
|
37
|
+
[needed_value_sets,vs_responses].transpose.each do |needed_vs,vs_data|
|
38
|
+
vs_model = modelize_value_set(vs_data, needed_vs[:query_version])
|
39
|
+
@vs_model_cache[needed_vs[:cache_key]] = vs_model
|
40
|
+
vs_models << vs_model
|
41
|
+
end
|
42
|
+
|
43
|
+
puts "\tloaded #{needed_value_sets.size} value sets from vsac, #{value_sets.size - needed_value_sets.size} from cache"
|
44
|
+
return vs_models
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def load_api
|
50
|
+
return @api if @api.present?
|
51
|
+
@api = Util::VSAC::VSACAPI.new(config: APP_CONFIG['vsac'], ticket_granting_ticket: @vsac_ticket_granting_ticket, username: @vsac_username, password: @vsac_password)
|
52
|
+
return @api
|
53
|
+
end
|
54
|
+
|
55
|
+
def determine_query_version(vs_vsac_options)
|
56
|
+
return "Draft" if vs_vsac_options[:include_draft] == true
|
57
|
+
return "Profile:#{vs_vsac_options[:profile]}" if vs_vsac_options[:profile]
|
58
|
+
return vs_vsac_options[:version] if vs_vsac_options[:version]
|
59
|
+
return "Release:#{vs_vsac_options[:release]}" if vs_vsac_options[:release]
|
60
|
+
return ""
|
61
|
+
end
|
62
|
+
|
63
|
+
def make_specific_value_set_options(value_set)
|
64
|
+
# If we are allowing measure_defined value sets, determine vsac_options for this value set based on elm info.
|
65
|
+
if @vsac_options[:measure_defined]
|
66
|
+
return { profile: value_set[:profile] } unless value_set[:profile].nil?
|
67
|
+
return { version: value_set[:version] } unless value_set[:version].nil?
|
68
|
+
end
|
69
|
+
return @vsac_options
|
70
|
+
end
|
71
|
+
|
72
|
+
def modelize_value_set(vsac_xml_response, query_version)
|
73
|
+
doc = Nokogiri::XML(vsac_xml_response)
|
74
|
+
doc.root.add_namespace_definition("vs","urn:ihe:iti:svs:2008")
|
75
|
+
vs_element = doc.at_xpath("/vs:RetrieveValueSetResponse/vs:ValueSet|/vs:RetrieveMultipleValueSetsResponse/vs:DescribedValueSet")
|
76
|
+
vs = CQM::ValueSet.new(
|
77
|
+
oid: vs_element["ID"],
|
78
|
+
display_name: vs_element["displayName"],
|
79
|
+
version: vs_element["version"] == "N/A" ? query_version : vs_element["version"],
|
80
|
+
concepts: extract_concepts(vs_element)
|
81
|
+
)
|
82
|
+
return vs
|
83
|
+
end
|
84
|
+
|
85
|
+
def extract_concepts(vs_element)
|
86
|
+
concepts = vs_element.xpath("//vs:Concept").collect do |con|
|
87
|
+
CQM::Concept.new(code: con["code"],
|
88
|
+
code_system_name: con["codeSystemName"],
|
89
|
+
code_system_version: con["codeSystemVersion"],
|
90
|
+
code_system_oid: con["codeSystem"],
|
91
|
+
display_name: con["displayName"])
|
92
|
+
end
|
93
|
+
return concepts
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
data/lib/tasks/hqmf.rake
CHANGED
data/lib/util/util.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Utilities
|
2
|
+
# Traverse each key, value of the hash and any nested hashes (including those in arrays)
|
3
|
+
# E.G. to deep transfrom do:
|
4
|
+
# deep_traverse_hash(obj) { |hash, k, v| hash[k] = v.upcase if v.is_a?(String) }
|
5
|
+
def self.deep_traverse_hash(obj, &block)
|
6
|
+
if obj.is_a? Array
|
7
|
+
obj.each { |val| deep_traverse_hash(val, &block) }
|
8
|
+
elsif obj.is_a?(Hash)
|
9
|
+
obj.each_pair do |k,v|
|
10
|
+
deep_traverse_hash(v, &block)
|
11
|
+
block.call(obj, k, v)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.remove_enclosing_quotes(str)
|
17
|
+
quote_chars = ['"', '\'']
|
18
|
+
quote_chars.each do |quote_char|
|
19
|
+
return str[1..-2] if str.start_with?(quote_char) && str.end_with?(quote_char)
|
20
|
+
end
|
21
|
+
return str
|
22
|
+
end
|
23
|
+
end
|
data/lib/util/vsac_api.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'typhoeus'
|
2
2
|
require 'uri'
|
3
3
|
|
4
4
|
module Util
|
@@ -16,6 +16,13 @@ module Util
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
# When VSAC responds with a 404
|
20
|
+
class VSACNotFoundError < VSACError
|
21
|
+
def initialize
|
22
|
+
super('Resource not found.')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
19
26
|
# Error represnting a program not found response from the API.
|
20
27
|
class VSACProgramNotFoundError < VSACError
|
21
28
|
attr_reader :oid
|
@@ -80,11 +87,10 @@ module Util
|
|
80
87
|
def initialize(options)
|
81
88
|
# check that :config exists and has needed fields
|
82
89
|
raise VSACArgumentError.new("Required param :config is missing or empty.") if options[:config].nil?
|
83
|
-
|
84
|
-
unless check_config
|
90
|
+
@config = options[:config].symbolize_keys
|
91
|
+
unless check_config @config
|
85
92
|
raise VSACArgumentError.new("Required param :config is missing required URLs.")
|
86
93
|
end
|
87
|
-
@config = symbolized_config
|
88
94
|
|
89
95
|
# if a ticket_granting_ticket was passed in, check it and raise errors if found
|
90
96
|
# username and password will be ignored
|
@@ -94,12 +100,8 @@ module Util
|
|
94
100
|
raise VSACArgumentError.new("Optional param :ticket_granting_ticket is missing :ticket or :expires")
|
95
101
|
end
|
96
102
|
|
97
|
-
|
98
|
-
if Time.now > provided_ticket_granting_ticket[:expires]
|
99
|
-
raise VSACTicketExpiredError.new
|
100
|
-
end
|
103
|
+
raise VSACTicketExpiredError.new if Time.now > provided_ticket_granting_ticket[:expires]
|
101
104
|
|
102
|
-
# ticket granting ticket looks good
|
103
105
|
@ticket_granting_ticket = { ticket: provided_ticket_granting_ticket[:ticket],
|
104
106
|
expires: provided_ticket_granting_ticket[:expires] }
|
105
107
|
|
@@ -114,17 +116,12 @@ module Util
|
|
114
116
|
#
|
115
117
|
# Returns a list of profile names. These are kept in the order that VSAC provides them in.
|
116
118
|
def get_profile_names
|
117
|
-
profiles_response =
|
118
|
-
profiles = []
|
119
|
+
profiles_response = http_get("#{@config[:utility_url]}/profiles")
|
119
120
|
|
120
121
|
# parse xml response and get text content of each profile element
|
121
122
|
doc = Nokogiri::XML(profiles_response)
|
122
123
|
profile_list = doc.at_xpath("/ProfileList")
|
123
|
-
profile_list.xpath("//profile").
|
124
|
-
profiles << profile.text
|
125
|
-
end
|
126
|
-
|
127
|
-
return profiles
|
124
|
+
return profile_list.xpath("//profile").map(&:text)
|
128
125
|
end
|
129
126
|
|
130
127
|
##
|
@@ -132,38 +129,25 @@ module Util
|
|
132
129
|
#
|
133
130
|
# Returns a list of program names. These are kept in the order that VSAC provides them in.
|
134
131
|
def get_program_names
|
135
|
-
programs_response =
|
136
|
-
|
137
|
-
|
138
|
-
# parse json response and return the names of the programs
|
139
|
-
programs_info = JSON.parse(programs_response)['Program']
|
140
|
-
programs_info.each do |program|
|
141
|
-
program_names << program['name']
|
142
|
-
end
|
143
|
-
|
144
|
-
return program_names
|
132
|
+
programs_response = http_get_json("#{@config[:utility_url]}/programs")
|
133
|
+
programs_info = programs_response['Program']
|
134
|
+
return programs_info.map { |program| program['name'] }
|
145
135
|
end
|
146
136
|
|
147
137
|
##
|
148
138
|
# Gets the details for a program. This may be used without credentials.
|
149
139
|
#
|
150
140
|
# Optional parameter program is the program to request from the API. If it is not provided it will look for
|
151
|
-
# a :program in the config passed in during construction. If there is no :program in the config it will use
|
141
|
+
# a :program in the config passed in during construction. If there is no :program in the config it will use
|
152
142
|
# the DEFAULT_PROGRAM constant for the program.
|
153
143
|
#
|
154
144
|
# Returns the JSON parsed response for program details.
|
155
145
|
def get_program_details(program = nil)
|
156
146
|
# if no program was provided use the one in the config or default in constant
|
157
|
-
if program.nil?
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
begin
|
162
|
-
# parse json response and return it
|
163
|
-
return JSON.parse(RestClient.get("#{@config[:utility_url]}/program/#{ERB::Util.url_encode(program)}"))
|
164
|
-
rescue RestClient::ResourceNotFound
|
165
|
-
raise VSACProgramNotFoundError.new(program)
|
166
|
-
end
|
147
|
+
program = @config.fetch(:program, DEFAULT_PROGRAM) if program.nil?
|
148
|
+
return http_get_json("#{@config[:utility_url]}/program/#{ERB::Util.url_encode(program)}")
|
149
|
+
rescue VSACNotFoundError
|
150
|
+
raise VSACProgramNotFoundError.new(program)
|
167
151
|
end
|
168
152
|
|
169
153
|
##
|
@@ -175,29 +159,21 @@ module Util
|
|
175
159
|
# }
|
176
160
|
#
|
177
161
|
# Optional parameter program is the program to request from the API. If it is not provided it will look for
|
178
|
-
# a :program in the config passed in during construction. If there is no :program in the config it will use
|
162
|
+
# a :program in the config passed in during construction. If there is no :program in the config it will use
|
179
163
|
# the DEFAULT_PROGRAM constant for the program.
|
180
164
|
#
|
181
165
|
# Returns the name of the latest profile for the given program.
|
182
166
|
def get_latest_profile_for_program(program = nil)
|
183
167
|
# if no program was provided use the one in the config or default in constant
|
184
|
-
if program.nil?
|
185
|
-
program = @config.fetch(:program, DEFAULT_PROGRAM)
|
186
|
-
end
|
168
|
+
program = @config.fetch(:program, DEFAULT_PROGRAM) if program.nil?
|
187
169
|
|
188
|
-
|
189
|
-
|
190
|
-
parsed_response = JSON.parse(RestClient.get("#{@config[:utility_url]}/program/#{ERB::Util.url_encode(program)}/latest%20profile"))
|
170
|
+
# parse json response and return it
|
171
|
+
parsed_response = http_get_json("#{@config[:utility_url]}/program/#{ERB::Util.url_encode(program)}/latest%20profile")
|
191
172
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
# keeping this rescue block in case the API is changed to return 404 for invalid profile
|
198
|
-
rescue RestClient::ResourceNotFound
|
199
|
-
raise VSACProgramNotFoundError.new(program)
|
200
|
-
end
|
173
|
+
# As of 5/17/18 VSAC does not return 404 when an invalid profile is provided. It just doesnt fill the name
|
174
|
+
# attribute in the 200 response. We need to check this.
|
175
|
+
raise VSACProgramNotFoundError.new(program) if parsed_response['name'].nil?
|
176
|
+
return parsed_response['name']
|
201
177
|
end
|
202
178
|
|
203
179
|
##
|
@@ -210,81 +186,168 @@ module Util
|
|
210
186
|
# Returns a list of release names in a program. These are kept in the order that VSAC provides them in.
|
211
187
|
def get_program_release_names(program = nil)
|
212
188
|
program_details = get_program_details(program)
|
213
|
-
|
214
|
-
|
215
|
-
# pull just the release names out
|
216
|
-
program_details['release'].each do |release|
|
217
|
-
releases << release['name']
|
218
|
-
end
|
219
|
-
|
220
|
-
return releases
|
189
|
+
return program_details['release'].map { |release| release['name'] }
|
221
190
|
end
|
222
191
|
|
223
192
|
##
|
224
193
|
# Gets a valueset. This requires credentials.
|
225
194
|
#
|
226
195
|
def get_valueset(oid, options = {})
|
227
|
-
|
228
|
-
|
196
|
+
needed_vs = {value_set: {oid: oid}, vs_vsac_options: options}
|
197
|
+
return get_multiple_valuesets([needed_vs])[0]
|
198
|
+
end
|
229
199
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
200
|
+
##
|
201
|
+
# Get multiple valuesets (executed in parallel). Requires credentials.
|
202
|
+
#
|
203
|
+
# Parameter needed_value_sets is an array of hashes, each hash should have at least:
|
204
|
+
# hash = {vs_vsac_options: ___, value_set: {oid: ___} }
|
205
|
+
#
|
206
|
+
def get_multiple_valuesets(needed_value_sets)
|
207
|
+
raise VSACNoCredentialsError.new unless @ticket_granting_ticket
|
208
|
+
raise VSACTicketExpiredError.new if Time.now > @ticket_granting_ticket[:expires]
|
234
209
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
params[:includeDraft] = !options[:include_draft].nil? ? 'yes' : 'no'
|
240
|
-
end
|
241
|
-
else
|
242
|
-
unless options[:include_draft].nil?
|
243
|
-
raise VSACArgumentError.new("Option :include_draft requires :profile to be provided.")
|
244
|
-
end
|
210
|
+
vs_responses = get_multiple_valueset_raw_responses(needed_value_sets)
|
211
|
+
vs_datas = [needed_value_sets,vs_responses].transpose.map do |needed_vs,vs_response|
|
212
|
+
expected_oid = needed_vs[:value_set][:oid]
|
213
|
+
process_and_validate_vsac_response(vs_response, expected_oid)
|
245
214
|
end
|
246
215
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
216
|
+
return vs_datas
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
251
220
|
|
252
|
-
|
253
|
-
|
221
|
+
# Given a raw valueset response, process and validate
|
222
|
+
def process_and_validate_vsac_response(vs_response, expected_oid)
|
223
|
+
raise VSNotFoundError.new(expected_oid) if vs_response.response_code == 404
|
224
|
+
validate_http_status_for_ticket_based_request(vs_response.response_code)
|
254
225
|
|
255
|
-
|
226
|
+
vs_data = vs_response.body.force_encoding("utf-8")
|
256
227
|
begin
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
228
|
+
doc = Nokogiri::XML(vs_data)
|
229
|
+
doc.root.add_namespace_definition("vs","urn:ihe:iti:svs:2008")
|
230
|
+
vs_element = doc.at_xpath("/vs:RetrieveValueSetResponse/vs:ValueSet|/vs:RetrieveMultipleValueSetsResponse/vs:DescribedValueSet")
|
231
|
+
concepts = vs_element.xpath("//vs:Concept")
|
232
|
+
rescue StandardError
|
233
|
+
raise VSACError.new("Could not parse VSAC response for #{expected_oid}. Body: #{vs_data}")
|
262
234
|
end
|
235
|
+
|
236
|
+
raise Util::VSAC::VSNotFoundError.new(expected_oid) unless (vs_element && vs_element['ID'] == expected_oid)
|
237
|
+
raise Util::VSAC::VSEmptyError.new(expected_oid) if concepts.empty?
|
238
|
+
return vs_data
|
263
239
|
end
|
264
240
|
|
265
|
-
|
241
|
+
# Execute bulk requests for valuesets, return the raw Typheous responses (requests executed in parallel)
|
242
|
+
def get_multiple_valueset_raw_responses(needed_value_sets)
|
243
|
+
service_tickets = get_service_tickets(needed_value_sets.size)
|
266
244
|
|
267
|
-
|
268
|
-
|
245
|
+
hydra = Typhoeus::Hydra.new(max_concurrency: 1) # Hydra executes multiple HTTP requests at once
|
246
|
+
requests = needed_value_sets.map do |n|
|
247
|
+
request = create_valueset_request(n[:value_set][:oid], service_tickets.pop, n[:vs_vsac_options])
|
248
|
+
hydra.queue(request)
|
249
|
+
request
|
250
|
+
end
|
251
|
+
|
252
|
+
hydra.run
|
253
|
+
responses = requests.map(&:response)
|
254
|
+
return responses
|
255
|
+
end
|
256
|
+
|
257
|
+
# Bulk get an amount of service tickets (requests executed in parallel)
|
258
|
+
def get_service_tickets(amount)
|
269
259
|
raise VSACNoCredentialsError.new unless @ticket_granting_ticket
|
270
|
-
# if the ticket granting ticket has expired, throw an error
|
271
260
|
raise VSACTicketExpiredError.new if Time.now > @ticket_granting_ticket[:expires]
|
272
261
|
|
273
|
-
#
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
262
|
+
hydra = Typhoeus::Hydra.new # Hydra executes multiple HTTP requests at once
|
263
|
+
requests = amount.times.map do
|
264
|
+
request = create_service_ticket_request
|
265
|
+
hydra.queue(request)
|
266
|
+
request
|
267
|
+
end
|
268
|
+
|
269
|
+
hydra.run
|
270
|
+
tickets = requests.map do |request|
|
271
|
+
validate_http_status_for_ticket_based_request(request.response.response_code)
|
272
|
+
request.response.body
|
273
|
+
end
|
274
|
+
return tickets
|
275
|
+
end
|
276
|
+
|
277
|
+
# Create a typheous request for a valueset (this must be executed later)
|
278
|
+
def create_valueset_request(oid, ticket, options = {})
|
279
|
+
# base parameter oid is always needed
|
280
|
+
params = { id: oid }
|
281
|
+
# release parameter, should be used moving forward
|
282
|
+
params[:release] = options[:release] unless options[:release].nil?
|
283
|
+
|
284
|
+
# profile parameter, may be needed for getting draft value sets
|
285
|
+
if options[:profile].present?
|
286
|
+
params[:profile] = options[:profile]
|
287
|
+
params[:includeDraft] = options[:include_draft] ? 'yes' : 'no' unless options[:include_draft].nil?
|
288
|
+
end
|
289
|
+
if !options[:include_draft].nil? && options[:profile].nil?
|
290
|
+
raise VSACArgumentError.new("Option :include_draft requires :profile to be provided.")
|
291
|
+
end
|
292
|
+
|
293
|
+
# version parameter, rarely used
|
294
|
+
params[:version] = options[:version] unless options[:version].nil?
|
295
|
+
params[:ticket] = ticket
|
296
|
+
|
297
|
+
return Typhoeus::Request.new("#{@config[:content_url]}/RetrieveMultipleValueSets", params: params)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Create a typheous request for a service ticket (this must be executed later)
|
301
|
+
def create_service_ticket_request
|
302
|
+
return Typhoeus::Request.new("#{@config[:auth_url]}/Ticket/#{@ticket_granting_ticket[:ticket]}",
|
303
|
+
method: :post,
|
304
|
+
params: { service: TICKET_SERVICE_PARAM})
|
305
|
+
end
|
306
|
+
|
307
|
+
# Use your username and password to retrive a ticket granting ticket from VSAC
|
308
|
+
def get_ticket_granting_ticket(username, password)
|
309
|
+
response = Typhoeus.post(
|
310
|
+
"#{@config[:auth_url]}/Ticket",
|
311
|
+
# looks like typheous sometimes switches the order of username/password when encoding
|
312
|
+
# which vsac cant handle (!?), so encode first
|
313
|
+
body: URI.encode_www_form(username: username, password: password)
|
314
|
+
)
|
315
|
+
raise VSACInvalidCredentialsError.new if response.response_code == 401
|
316
|
+
validate_http_status(response.response_code)
|
317
|
+
return { ticket: String.new(response.body), expires: Time.now + 8.hours }
|
318
|
+
end
|
319
|
+
|
320
|
+
# Raise errors if http_status is not OK (200), and expire TGT if auth fails
|
321
|
+
def validate_http_status_for_ticket_based_request(http_status)
|
322
|
+
if http_status == 401
|
278
323
|
@ticket_granting_ticket[:expires] = Time.now
|
279
324
|
raise VSACTicketExpiredError.new
|
280
325
|
end
|
326
|
+
validate_http_status(http_status)
|
281
327
|
end
|
282
328
|
|
283
|
-
|
284
|
-
|
285
|
-
return
|
286
|
-
|
287
|
-
|
329
|
+
# Raise errors if http_status is not OK (200)
|
330
|
+
def validate_http_status(http_status)
|
331
|
+
return if http_status == 200
|
332
|
+
if http_status == 0
|
333
|
+
raise VSACError.new("Error communicating with VSAC.")
|
334
|
+
elsif http_status == 404
|
335
|
+
raise VSACNotFoundError.new
|
336
|
+
else
|
337
|
+
raise VSACError.new("HTTP Error code #{http_status} received from VSAC.")
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Convenience function to perform an http get request (raises errors on failure)
|
342
|
+
def http_get(url)
|
343
|
+
response = Typhoeus.get(url)
|
344
|
+
validate_http_status(response.response_code)
|
345
|
+
return response.body
|
346
|
+
end
|
347
|
+
|
348
|
+
# Convenience function to perform an http get request and convert to JSON (raises errors on failure)
|
349
|
+
def http_get_json(url)
|
350
|
+
return JSON.parse(http_get(url))
|
288
351
|
end
|
289
352
|
|
290
353
|
# Checks to ensure the API config has all necessary fields
|