bonnie_bundler 2.0.3 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +6 -6
- data/bonnie-bundler.gemspec +1 -1
- data/lib/bonnie_bundler.rb +1 -0
- data/lib/measures/loading/cql_loader.rb +25 -12
- data/lib/measures/loading/exceptions.rb +0 -2
- data/lib/measures/loading/value_set_loader.rb +77 -97
- data/lib/util/vsac_api.rb +274 -0
- data/test/fixtures/PVC2_v5_4_Unused_Support_Libraries.zip +0 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_pvc_unused_libraries.yml +4286 -0
- data/test/fixtures/vcr_cassettes/vs_loading_500_response.yml +633 -0
- data/test/fixtures/vcr_cassettes/vs_loading_empty_concept_list.yml +174 -0
- data/test/fixtures/vcr_cassettes/vs_loading_meausre_defined_no_backup_profile.yml +1237 -0
- data/test/fixtures/vcr_cassettes/vs_loading_release.yml +17035 -0
- data/test/fixtures/vcr_cassettes/vsac_auth_bad_credentials.yml +57 -0
- data/test/fixtures/vcr_cassettes/vsac_auth_bad_ticket.yml +57 -0
- data/test/fixtures/vcr_cassettes/vsac_auth_good_credentials.yml +57 -0
- data/test/fixtures/vcr_cassettes/vsac_auth_good_credentials_and_simple_call.yml +177 -0
- data/test/fixtures/vcr_cassettes/vsac_util_get_profiles.yml +74 -0
- data/test/fixtures/vcr_cassettes/vsac_util_get_program_details_CMS_Hybrid.yml +57 -0
- data/test/fixtures/vcr_cassettes/vsac_util_get_program_details_CMS_eCQM.yml +64 -0
- data/test/fixtures/vcr_cassettes/vsac_util_get_program_details_HL7_C-CDA.yml +58 -0
- data/test/fixtures/vcr_cassettes/vsac_util_get_program_details_invalid.yml +53 -0
- data/test/fixtures/vcr_cassettes/vsac_util_get_programs.yml +64 -0
- data/test/fixtures/vcr_cassettes/vsac_vs_include_draft_no_profile.yml +57 -0
- data/test/fixtures/vcr_cassettes/vsac_vs_include_draft_with_profile.yml +177 -0
- data/test/fixtures/vcr_cassettes/vsac_vs_no_options.yml +177 -0
- data/test/fixtures/vcr_cassettes/vsac_vs_not_found.yml +164 -0
- data/test/fixtures/vcr_cassettes/vsac_vs_release.yml +4802 -0
- data/test/fixtures/vcr_cassettes/vsac_vs_version.yml +370 -0
- data/test/test_helper.rb +9 -4
- data/test/unit/cql_loader_test.rb +42 -15
- data/test/unit/load_mat_export_test.rb +10 -13
- data/test/unit/measure_complexity_test.rb +1 -1
- data/test/unit/measure_diff_test.rb +4 -4
- data/test/unit/storing_mat_export_package_test.rb +1 -1
- data/test/unit/value_set_loading_test.rb +58 -16
- data/test/unit/vsac_api_auth_test.rb +129 -0
- data/test/unit/vsac_api_test.rb +77 -0
- data/test/unit/vsac_api_util_test.rb +139 -0
- metadata +29 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61b0e2dce988134311e1747adf13ea68859a5799
|
4
|
+
data.tar.gz: 7e8638a383ead0635f662fa55da51baedf4ae0cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac0422c09d3a74be3c68bccd8cc595ca02f44d64c5bc9b3ab64f46c88dbc96c9d5b4f1e360c4c4b04d6f108fe3f30f8749757581a9aec7d376779aab08a7cc85
|
7
|
+
data.tar.gz: d1ffdbdd9a4bf57ee3def0d184960164c77074c18a65cbee955e464cc0d7fa76ab73929c328ee04b4446e3ae0ff21f9062b2241dcce3689cceb0d91f23bedf58
|
data/Gemfile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
2
|
gemspec
|
3
3
|
|
4
|
-
# gem 'health-data-standards', :git => 'https://github.com/projectcypress/health-data-standards.git', :branch => '
|
4
|
+
# gem 'health-data-standards', :git => 'https://github.com/projectcypress/health-data-standards.git', :branch => 'master_bonnie'
|
5
5
|
# gem 'quality-measure-engine', :git => 'https://github.com/projectcypress/quality-measure-engine.git', :branch => 'master'
|
6
6
|
# gem 'hqmf2js', :git => 'https://github.com/projecttacoma/hqmf2js.git', :branch => 'master'
|
7
7
|
# gem 'hquery-patient-api', :git => 'https://github.com/projecttacoma/patientapi.git', :branch => 'master'
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
bonnie_bundler (2.0
|
4
|
+
bonnie_bundler (2.1.0)
|
5
5
|
diffy (~> 3.0.0)
|
6
6
|
health-data-standards (~> 4.0)
|
7
7
|
hqmf2js (~> 1.4)
|
@@ -84,7 +84,7 @@ GEM
|
|
84
84
|
globalid (0.4.1)
|
85
85
|
activesupport (>= 4.2.0)
|
86
86
|
hashdiff (0.3.6)
|
87
|
-
health-data-standards (4.0.
|
87
|
+
health-data-standards (4.0.4)
|
88
88
|
activesupport (~> 4.2.0)
|
89
89
|
builder (~> 3.1)
|
90
90
|
erubis (~> 2.7.0)
|
@@ -119,7 +119,7 @@ GEM
|
|
119
119
|
json (2.1.0)
|
120
120
|
libv8 (3.16.14.19)
|
121
121
|
log4r (1.1.10)
|
122
|
-
loofah (2.
|
122
|
+
loofah (2.2.2)
|
123
123
|
crass (~> 1.0.2)
|
124
124
|
nokogiri (>= 1.5.9)
|
125
125
|
macaddr (1.7.1)
|
@@ -183,8 +183,8 @@ GEM
|
|
183
183
|
activesupport (>= 4.2.0.beta, < 5.0)
|
184
184
|
nokogiri (~> 1.6)
|
185
185
|
rails-deprecated_sanitizer (>= 1.0.1)
|
186
|
-
rails-html-sanitizer (1.0.
|
187
|
-
loofah (~> 2.
|
186
|
+
rails-html-sanitizer (1.0.4)
|
187
|
+
loofah (~> 2.2, >= 2.2.2)
|
188
188
|
railties (4.2.10)
|
189
189
|
actionpack (= 4.2.10)
|
190
190
|
activesupport (= 4.2.10)
|
@@ -264,4 +264,4 @@ DEPENDENCIES
|
|
264
264
|
webmock
|
265
265
|
|
266
266
|
BUNDLED WITH
|
267
|
-
1.
|
267
|
+
1.16.1
|
data/bonnie-bundler.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.email = "pophealth-talk@googlegroups.com"
|
8
8
|
s.homepage = "http://github.com/projecttacoma/bonnie_bundler"
|
9
9
|
s.authors = ["The MITRE Corporation"]
|
10
|
-
s.version = '2.0
|
10
|
+
s.version = '2.1.0'
|
11
11
|
s.license = 'Apache-2.0'
|
12
12
|
|
13
13
|
s.add_dependency 'health-data-standards', '~> 4.0'
|
data/lib/bonnie_bundler.rb
CHANGED
@@ -24,6 +24,7 @@ require_relative 'ext/valueset.rb'
|
|
24
24
|
require_relative 'measures/elm_parser.rb'
|
25
25
|
require_relative 'measures/cql_to_elm_helper.rb'
|
26
26
|
require_relative '../config/initializers/mongo.rb'
|
27
|
+
require_relative 'util/vsac_api.rb'
|
27
28
|
|
28
29
|
module BonnieBundler
|
29
30
|
class << self
|
@@ -22,7 +22,7 @@ module Measures
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
def self.load_mat_cql_exports(user, zip_file, out_dir, measure_details,
|
25
|
+
def self.load_mat_cql_exports(user, zip_file, out_dir, measure_details, vsac_options, vsac_ticket_granting_ticket)
|
26
26
|
measure = nil
|
27
27
|
cql = nil
|
28
28
|
hqmf_path = nil
|
@@ -35,7 +35,7 @@ module Measures
|
|
35
35
|
# Get main measure from hqmf parser
|
36
36
|
main_cql_library = hqmf_model.cql_measure_library
|
37
37
|
|
38
|
-
cql_artifacts = process_cql(files, main_cql_library, user,
|
38
|
+
cql_artifacts = process_cql(files, main_cql_library, user, vsac_options, vsac_ticket_granting_ticket, hqmf_model.hqmf_set_id)
|
39
39
|
|
40
40
|
# Create CQL Measure
|
41
41
|
hqmf_model.backfill_patient_characteristics_with_codes(cql_artifacts[:all_codes_and_code_names])
|
@@ -83,16 +83,16 @@ module Measures
|
|
83
83
|
return json['source_data_criteria'], json['data_criteria']
|
84
84
|
end
|
85
85
|
|
86
|
-
def self.load(file, user, measure_details,
|
86
|
+
def self.load(file, user, measure_details, vsac_options, vsac_ticket_granting_ticket)
|
87
87
|
measure = nil
|
88
88
|
Dir.mktmpdir do |dir|
|
89
|
-
measure = load_mat_cql_exports(user, file, dir, measure_details,
|
89
|
+
measure = load_mat_cql_exports(user, file, dir, measure_details, vsac_options, vsac_ticket_granting_ticket)
|
90
90
|
end
|
91
91
|
measure
|
92
92
|
end
|
93
93
|
|
94
94
|
# Manages all of the CQL processing that is not related to the HQMF.
|
95
|
-
def self.process_cql(files, main_cql_library, user,
|
95
|
+
def self.process_cql(files, main_cql_library, user, vsac_options, vsac_ticket_granting_ticket, measure_id=nil)
|
96
96
|
elm_strings = files[:ELM_JSON]
|
97
97
|
# Removes 'urn:oid:' from ELM for Bonnie and Parse the JSON
|
98
98
|
elm_strings.each { |elm_string| elm_string.gsub! 'urn:oid:', '' }
|
@@ -103,8 +103,11 @@ module Measures
|
|
103
103
|
cql_definition_dependency_structure = populate_cql_definition_dependency_structure(main_cql_library, elms)
|
104
104
|
# Go back for the library statements
|
105
105
|
cql_definition_dependency_structure = populate_used_library_dependencies(cql_definition_dependency_structure, main_cql_library, elms)
|
106
|
+
# Add unused libraries to structure and set the value to empty hash
|
107
|
+
cql_definition_dependency_structure = populate_unused_included_libraries(cql_definition_dependency_structure, elms)
|
106
108
|
|
107
|
-
# fix up statement names in cql_statement_dependencies to not use periods
|
109
|
+
# fix up statement names in cql_statement_dependencies to not use periods <<WRAP 1>>
|
110
|
+
# this is matched with an UNWRAP in MeasuresController in the bonnie project
|
108
111
|
Measures::MongoHashKeyWrapper::wrapKeys cql_definition_dependency_structure
|
109
112
|
|
110
113
|
# Depening on the value of the value set version, change it to null, strip out a substring or leave it alone.
|
@@ -122,12 +125,9 @@ module Measures
|
|
122
125
|
end
|
123
126
|
# Get Value Sets
|
124
127
|
value_set_models = []
|
125
|
-
if
|
126
|
-
|
127
|
-
|
128
|
-
rescue Exception => e
|
129
|
-
raise VSACException.new "Error Loading Value Sets from VSAC: #{e.message}"
|
130
|
-
end
|
128
|
+
# Only load value sets from VSAC if there is a ticket_granting_ticket.
|
129
|
+
if !vsac_ticket_granting_ticket.nil?
|
130
|
+
value_set_models = Measures::ValueSetLoader.load_value_sets_from_vsac(elm_value_sets, vsac_options, vsac_ticket_granting_ticket, user, measure_id)
|
131
131
|
else
|
132
132
|
# No vsac credentials were provided grab the valueset and valueset versions from the 'value_set_oid_version_object' on the existing measure
|
133
133
|
db_measure = CqlMeasure.by_user(user).where(hqmf_set_id: measure_id).first
|
@@ -422,6 +422,19 @@ module Measures
|
|
422
422
|
starting_hash
|
423
423
|
end
|
424
424
|
|
425
|
+
# add the unused libraries and set them to have empty hashes.
|
426
|
+
def self.populate_unused_included_libraries(cql_definition_dependency_structure, elms)
|
427
|
+
if elms.count > cql_definition_dependency_structure.keys.count
|
428
|
+
elms.each do |elm|
|
429
|
+
# If the number of libraries included in the elm is greater than
|
430
|
+
# the number of libraries included in the dependency structure
|
431
|
+
library_name = elm['library']['identifier']['id']
|
432
|
+
cql_definition_dependency_structure[library_name] = {} if cql_definition_dependency_structure[library_name].nil?
|
433
|
+
end
|
434
|
+
end
|
435
|
+
cql_definition_dependency_structure
|
436
|
+
end
|
437
|
+
|
425
438
|
# Traverse list, create keys and drill down for each key.
|
426
439
|
# If key is already in place, skip.
|
427
440
|
def self.create_hash_for_all(starting_hash, key_statement, elms)
|
@@ -19,119 +19,99 @@ module Measures
|
|
19
19
|
HealthDataStandards::SVS::ValueSet.by_user(user).in(oid: value_set_oids)
|
20
20
|
end
|
21
21
|
|
22
|
-
def self.load_value_sets_from_vsac(value_sets,
|
23
|
-
# Get a list of just the oids
|
24
|
-
value_set_oids = value_sets.map {|value_set| value_set[:oid]}
|
22
|
+
def self.load_value_sets_from_vsac(value_sets, vsac_options, vsac_ticket_granting_ticket, user=nil, measure_id=nil)
|
25
23
|
value_set_models = []
|
26
24
|
from_vsac = 0
|
27
25
|
existing_value_set_map = {}
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
26
|
+
|
27
|
+
errors = {}
|
28
|
+
api = Util::VSAC::VSACAPI.new(config: APP_CONFIG['vsac'], ticket_granting_ticket: vsac_ticket_granting_ticket)
|
29
|
+
|
30
|
+
RestClient.proxy = ENV["http_proxy"]
|
31
|
+
value_sets.each do |value_set|
|
32
|
+
# The vsac_options that will be used for this specific value set. Default to using passed in options from measures controller
|
33
|
+
vs_vsac_options = vsac_options
|
34
|
+
|
35
|
+
# If we are allowing measure_defined value sets, determine vsac_options for this value set based on elm info.
|
36
|
+
if vsac_options[:measure_defined] == true
|
37
|
+
if !value_set[:profile].nil?
|
38
|
+
vs_vsac_options = { profile: value_set[:profile] }
|
39
|
+
elsif !value_set[:version].nil?
|
40
|
+
vs_vsac_options = { version: value_set[:version] }
|
41
|
+
end
|
42
|
+
# if no parseable options in the ELM were found, we stick with the passed in options from measures controller
|
33
43
|
end
|
34
|
-
nlm_config = APP_CONFIG["nlm"]
|
35
44
|
|
36
|
-
|
37
|
-
|
45
|
+
# Determine version to store value sets as after parsing and to use to looking for existing set.
|
46
|
+
query_version = ""
|
47
|
+
if vs_vsac_options[:include_draft] == true
|
48
|
+
query_version = "Draft-#{measure_id}" # Unique draft version based on measure id
|
49
|
+
elsif vs_vsac_options[:profile]
|
50
|
+
query_version = "Profile:#{vs_vsac_options[:profile]}" # Profile calls return 'N/A' so note profile use.
|
51
|
+
elsif vs_vsac_options[:version]
|
52
|
+
query_version = vs_vsac_options[:version]
|
53
|
+
elsif vs_vsac_options[:release]
|
54
|
+
query_version = "Release:#{vs_vsac_options[:release]}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# TODO: remove the usage of existing value sets. future work will be always fetching value sets from VSAC and
|
58
|
+
# associating them with the new measure.
|
59
|
+
|
60
|
+
# check if we already have this valuset loaded for this user
|
61
|
+
set = HealthDataStandards::SVS::ValueSet.where({user_id: user.id, oid: value_set[:oid], version: query_version}).first()
|
38
62
|
|
39
|
-
if
|
40
|
-
|
41
|
-
|
63
|
+
# delete existing if we are doing include_draft option sinc the existing may be stale. note that this may delete
|
64
|
+
# and effectively replace value sets for a measure load that may fail. Unintentionally updating the value sets.
|
65
|
+
if (vs_vsac_options[:include_draft] && set)
|
66
|
+
set.delete
|
67
|
+
set = nil
|
42
68
|
end
|
43
69
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
set.
|
65
|
-
set
|
66
|
-
|
67
|
-
|
70
|
+
# use the existing value set if it exists
|
71
|
+
if (set)
|
72
|
+
existing_value_set_map[set.oid] = set
|
73
|
+
|
74
|
+
# load this value set from VSAC
|
75
|
+
else
|
76
|
+
vs_data = api.get_valueset(value_set[:oid], vs_vsac_options)
|
77
|
+
|
78
|
+
# there are some funky unicodes coming out of the vs response that are not in ASCII as the string reports to be
|
79
|
+
vs_data.force_encoding("utf-8")
|
80
|
+
from_vsac += 1
|
81
|
+
|
82
|
+
doc = Nokogiri::XML(vs_data)
|
83
|
+
|
84
|
+
doc.root.add_namespace_definition("vs","urn:ihe:iti:svs:2008")
|
85
|
+
|
86
|
+
vs_element = doc.at_xpath("/vs:RetrieveValueSetResponse/vs:ValueSet|/vs:RetrieveMultipleValueSetsResponse/vs:DescribedValueSet")
|
87
|
+
|
88
|
+
if vs_element && vs_element['ID'] == value_set[:oid]
|
89
|
+
vs_element['id'] = value_set[:oid]
|
90
|
+
set = HealthDataStandards::SVS::ValueSet.load_from_xml(doc)
|
91
|
+
# make sure this value set has concepts, delete it and raise error if it is empty
|
92
|
+
if set.concepts.empty?
|
93
|
+
set.delete
|
94
|
+
raise Util::VSAC::VSEmptyError.new(value_set[:oid])
|
95
|
+
end
|
96
|
+
set.user = user
|
97
|
+
|
98
|
+
# bundle id for user should always be the same 1 user to 1 bundle
|
99
|
+
# using this to allow cat I generation without extensive modification to HDS
|
100
|
+
set.bundle = user.bundle if (user && user.respond_to?(:bundle))
|
101
|
+
|
102
|
+
# As of 9/7/2017, when valuesets are retrieved from VSAC via profile, their version defaults to N/A
|
103
|
+
# As such, we set the version to the profile with an indicator.
|
104
|
+
set.version = query_version
|
105
|
+
set.save!
|
68
106
|
existing_value_set_map[set.oid] = set
|
69
107
|
else
|
70
|
-
|
71
|
-
|
72
|
-
# try to access the cached result for the value set if it exists.
|
73
|
-
cached_service_result = File.join(codeset_base_dir,"#{value_set[:oid]}.xml") if use_cache
|
74
|
-
if (cached_service_result && File.exists?(cached_service_result))
|
75
|
-
vs_data = File.read cached_service_result
|
76
|
-
else
|
77
|
-
# If includeDraft is true the latest vs are required, so the latest profile should be used.
|
78
|
-
if includeDraft
|
79
|
-
vs_data = api.get_valueset(value_set[:oid], include_draft: includeDraft, profile: nlm_config["profile"])
|
80
|
-
elsif value_set[:version]
|
81
|
-
vs_data = api.get_valueset(value_set[:oid], version: value_set[:version])
|
82
|
-
else
|
83
|
-
# If no version, call with profile.
|
84
|
-
# If a profile is specified, use it. Otherwise, use default.
|
85
|
-
profile = value_set[:profile] ? value_set[:profile] : nlm_config["profile"]
|
86
|
-
vs_data = api.get_valueset(value_set[:oid], profile: profile)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
vs_data.force_encoding("utf-8") # there are some funky unicodes coming out of the vs response that are not in ASCII as the string reports to be
|
90
|
-
from_vsac += 1
|
91
|
-
# write all valueset data retrieved if using a cache
|
92
|
-
File.open(cached_service_result, 'w') {|f| f.write(vs_data) } if use_cache
|
93
|
-
|
94
|
-
doc = Nokogiri::XML(vs_data)
|
95
|
-
|
96
|
-
doc.root.add_namespace_definition("vs","urn:ihe:iti:svs:2008")
|
97
|
-
|
98
|
-
vs_element = doc.at_xpath("/vs:RetrieveValueSetResponse/vs:ValueSet|/vs:RetrieveMultipleValueSetsResponse/vs:DescribedValueSet")
|
99
|
-
|
100
|
-
if vs_element && vs_element['ID'] == value_set[:oid]
|
101
|
-
vs_element['id'] = value_set[:oid]
|
102
|
-
set = HealthDataStandards::SVS::ValueSet.load_from_xml(doc)
|
103
|
-
set.user = user
|
104
|
-
#bundle id for user should always be the same 1 user to 1 bundle
|
105
|
-
#using this to allow cat I generation without extensive modification to HDS
|
106
|
-
set.bundle = user.bundle if (user && user.respond_to?(:bundle))
|
107
|
-
# As of t9/7/2017, when valuesets are retrieved from VSAC via profile, their version defaults to N/A
|
108
|
-
# As such, we set the version to the profile with an indicator.
|
109
|
-
set.version = query_version
|
110
|
-
set.save!
|
111
|
-
existing_value_set_map[set.oid] = set
|
112
|
-
else
|
113
|
-
raise "Value set not found: #{oid}"
|
114
|
-
end
|
108
|
+
raise Util::VSAC::VSNotFoundError.new(value_set[:oid])
|
115
109
|
end
|
116
110
|
end
|
117
|
-
rescue Exception => e
|
118
|
-
if (overwrite)
|
119
|
-
delete_existing_vs(user, value_set_oids)
|
120
|
-
backup_vs.each {|vs| HealthDataStandards::SVS::ValueSet.new(vs.attributes).save }
|
121
|
-
end
|
122
|
-
raise VSACException.new "#{e.message}"
|
123
111
|
end
|
124
112
|
|
125
113
|
puts "\tloaded #{from_vsac} value sets from vsac" if from_vsac > 0
|
126
114
|
existing_value_set_map.values
|
127
115
|
end
|
128
|
-
|
129
|
-
def self.get_existing_vs(user, value_set_oids)
|
130
|
-
HealthDataStandards::SVS::ValueSet.by_user(user).where(oid: {'$in'=>value_set_oids})
|
131
|
-
end
|
132
|
-
def self.delete_existing_vs(user, value_set_oids)
|
133
|
-
get_existing_vs(user, value_set_oids).delete_all()
|
134
|
-
end
|
135
|
-
|
136
116
|
end
|
137
117
|
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Util
|
5
|
+
module VSAC
|
6
|
+
|
7
|
+
# Generic VSAC related exception.
|
8
|
+
class VSACError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Error represnting a not found response from the API. Includes OID for reporting to user.
|
12
|
+
class VSNotFoundError < VSACError
|
13
|
+
attr_reader :oid
|
14
|
+
def initialize(oid)
|
15
|
+
super("Value Set (#{oid}) was not found.")
|
16
|
+
@oid = oid
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Error represnting a program not found response from the API.
|
21
|
+
class VSACProgramNotFoundError < VSACError
|
22
|
+
attr_reader :oid
|
23
|
+
def initialize(program)
|
24
|
+
super("VSAC Program #{program} does not exist.")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Error represnting a response from the API that had no concepts.
|
29
|
+
class VSEmptyError < VSACError
|
30
|
+
attr_reader :oid
|
31
|
+
def initialize(oid)
|
32
|
+
super("Value Set (#{oid}) is empty.")
|
33
|
+
@oid = oid
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Raised when the ticket granting ticket has expired.
|
38
|
+
class VSACTicketExpiredError < VSACError
|
39
|
+
def initialize
|
40
|
+
super('VSAC session expired. Please re-enter credentials and try again.')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Raised when the user credentials were invalid.
|
45
|
+
class VSACInvalidCredentialsError < VSACError
|
46
|
+
def initialize
|
47
|
+
super('VSAC ULMS credentials are invalid.')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Raised when a call requiring auth is attempted when no ticket_granting_ticket or credentials were provided.
|
52
|
+
class VSACNoCredentialsError < VSACError
|
53
|
+
def initialize
|
54
|
+
super('VSAC ULMS credentials were not provided.')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Raised when the arguments passed in are bad.
|
59
|
+
class VSACArgumentError < VSACError
|
60
|
+
end
|
61
|
+
|
62
|
+
class VSACAPI
|
63
|
+
# The default program to use for get_program_details and get_program_release_names calls.
|
64
|
+
# This can be overriden by providing a :program in the config or by the single optional parameter for those
|
65
|
+
# methods.
|
66
|
+
DEFAULT_PROGRAM = "CMS eCQM"
|
67
|
+
|
68
|
+
# This is the value of the service parameter passed when getting a ticket. This never changes.
|
69
|
+
TICKET_SERVICE_PARAM = "http://umlsks.nlm.nih.gov"
|
70
|
+
|
71
|
+
# The ticket granting that will be obtained if needed. Accessible so it may be stored in user session.
|
72
|
+
# Is a hash of the :ticket and time it :expires.
|
73
|
+
attr_reader :ticket_granting_ticket
|
74
|
+
|
75
|
+
##
|
76
|
+
# Creates a new VSACAPI. If credentials were provided they are checked now. If no credentials
|
77
|
+
# are provided then the API can still be used for utility methods.
|
78
|
+
#
|
79
|
+
# Options for the API are passed in as a hash.
|
80
|
+
# * config -
|
81
|
+
def initialize(options)
|
82
|
+
# check that :config exists and has needed fields
|
83
|
+
if options[:config].nil?
|
84
|
+
raise VSACArgumentError.new("Required param :config is missing or empty.")
|
85
|
+
else
|
86
|
+
symbolized_config = options[:config].symbolize_keys
|
87
|
+
if check_config symbolized_config
|
88
|
+
@config = symbolized_config
|
89
|
+
else
|
90
|
+
raise VSACArgumentError.new("Required param :config is missing required URLs.")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# if a ticket_granting_ticket was passed in, check it and raise errors if found
|
95
|
+
# username and password will be ignored
|
96
|
+
if !options[:ticket_granting_ticket].nil?
|
97
|
+
provided_ticket_granting_ticket = options[:ticket_granting_ticket]
|
98
|
+
if provided_ticket_granting_ticket[:ticket].nil? || provided_ticket_granting_ticket[:expires].nil?
|
99
|
+
raise VSACArgumentError.new("Optional param :ticket_granting_ticket is missing :ticket or :expires")
|
100
|
+
end
|
101
|
+
|
102
|
+
# check if it has expired
|
103
|
+
if Time.now > provided_ticket_granting_ticket[:expires]
|
104
|
+
raise VSACTicketExpiredError.new
|
105
|
+
end
|
106
|
+
|
107
|
+
# ticket granting ticket looks good
|
108
|
+
@ticket_granting_ticket = { ticket: provided_ticket_granting_ticket[:ticket],
|
109
|
+
expires: provided_ticket_granting_ticket[:expires] }
|
110
|
+
|
111
|
+
# if username and password were provided use them to get a ticket granting ticket
|
112
|
+
elsif !options[:username].nil? && !options[:password].nil?
|
113
|
+
@ticket_granting_ticket = get_ticket_granting_ticket(options[:username], options[:password])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# Gets the list of profiles. This may be used without credentials.
|
119
|
+
#
|
120
|
+
# Returns a list of profile names. These are kept in the order that VSAC provides them in.
|
121
|
+
def get_profile_names
|
122
|
+
profiles_response = RestClient.get("#{@config[:utility_url]}/profiles")
|
123
|
+
profiles = []
|
124
|
+
|
125
|
+
# parse xml response and get text content of each profile element
|
126
|
+
doc = Nokogiri::XML(profiles_response)
|
127
|
+
profile_list = doc.at_xpath("/ProfileList")
|
128
|
+
profile_list.xpath("//profile").each do |profile|
|
129
|
+
profiles << profile.text
|
130
|
+
end
|
131
|
+
|
132
|
+
return profiles
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Gets the list of programs. This may be used without credentials.
|
137
|
+
#
|
138
|
+
# Returns a list of program names. These are kept in the order that VSAC provides them in.
|
139
|
+
def get_program_names
|
140
|
+
programs_response = RestClient.get("#{@config[:utility_url]}/programs")
|
141
|
+
program_names = []
|
142
|
+
|
143
|
+
# parse json response and return the names of the programs
|
144
|
+
programs_info = JSON.parse(programs_response)['Program']
|
145
|
+
programs_info.each do |program|
|
146
|
+
program_names << program['name']
|
147
|
+
end
|
148
|
+
|
149
|
+
return program_names
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Gets the details for a program. This may be used without credentials.
|
154
|
+
#
|
155
|
+
# Optional parameter program is the program to request from the API. If it is not provided it will look for
|
156
|
+
# a :program in the config passed in during construction. If there is no :program in the config it will use
|
157
|
+
# the DEFAULT_PROGRAM constant for the program.
|
158
|
+
#
|
159
|
+
# Returns the JSON parsed response for program details.
|
160
|
+
def get_program_details(program = nil)
|
161
|
+
# if no program was provided use the one in the config or default in constant
|
162
|
+
if program.nil?
|
163
|
+
program = @config.fetch(:program, DEFAULT_PROGRAM)
|
164
|
+
end
|
165
|
+
|
166
|
+
begin
|
167
|
+
# parse json response and return it
|
168
|
+
return JSON.parse(RestClient.get("#{@config[:utility_url]}/program/#{ERB::Util.url_encode(program)}"))
|
169
|
+
rescue RestClient::ResourceNotFound
|
170
|
+
raise VSACProgramNotFoundError.new(program)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Gets the releases for a program. This may be used without credentials.
|
176
|
+
#
|
177
|
+
# 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
|
179
|
+
# the DEFAULT_PROGRAM constant for the program.
|
180
|
+
#
|
181
|
+
# Returns a list of release names in a program. These are kept in the order that VSAC provides them in.
|
182
|
+
def get_program_release_names(program = nil)
|
183
|
+
program_details = get_program_details(program)
|
184
|
+
releases = []
|
185
|
+
|
186
|
+
# pull just the release names out
|
187
|
+
program_details['release'].each do |release|
|
188
|
+
releases << release['name']
|
189
|
+
end
|
190
|
+
|
191
|
+
return releases
|
192
|
+
end
|
193
|
+
|
194
|
+
##
|
195
|
+
# Gets a valueset. This requires credentials.
|
196
|
+
#
|
197
|
+
def get_valueset(oid, options = {})
|
198
|
+
# base parameter oid is always needed
|
199
|
+
params = { id: oid }
|
200
|
+
|
201
|
+
# release parameter, should be used moving forward
|
202
|
+
if !options[:release].nil?
|
203
|
+
params[:release] = options[:release]
|
204
|
+
end
|
205
|
+
|
206
|
+
# profile parameter, may be needed for getting draft value sets
|
207
|
+
if !options[:profile].nil?
|
208
|
+
params[:profile] = options[:profile]
|
209
|
+
if !options[:include_draft].nil?
|
210
|
+
params[:includeDraft] = if !!options[:include_draft] then 'yes' else 'no' end
|
211
|
+
end
|
212
|
+
else
|
213
|
+
if !options[:include_draft].nil?
|
214
|
+
raise VSACArgumentError.new("Option :include_draft requires :profile to be provided.")
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# version parameter, rarely used
|
219
|
+
if !options[:version].nil?
|
220
|
+
params[:version] = options[:version]
|
221
|
+
end
|
222
|
+
|
223
|
+
# get a new service ticket
|
224
|
+
params[:ticket] = get_ticket
|
225
|
+
|
226
|
+
# run request
|
227
|
+
begin
|
228
|
+
return RestClient.get("#{@config[:content_url]}/RetrieveMultipleValueSets", params: params)
|
229
|
+
rescue RestClient::ResourceNotFound
|
230
|
+
raise VSNotFoundError.new(oid)
|
231
|
+
rescue RestClient::InternalServerError
|
232
|
+
raise VSACError.new("Server error response from VSAC for (#{oid}).")
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
|
238
|
+
def get_ticket
|
239
|
+
# if there is no ticket granting ticket then we should raise an error
|
240
|
+
raise VSACNoCredentialsError.new unless @ticket_granting_ticket
|
241
|
+
# if the ticket granting ticket has expired, throw an error
|
242
|
+
raise VSACTicketExpiredError.new if Time.now > @ticket_granting_ticket[:expires]
|
243
|
+
|
244
|
+
# attempt to get a ticket
|
245
|
+
begin
|
246
|
+
ticket = RestClient.post("#{@config[:auth_url]}/Ticket/#{@ticket_granting_ticket[:ticket]}", service: TICKET_SERVICE_PARAM)
|
247
|
+
return ticket.to_s
|
248
|
+
rescue RestClient::Unauthorized
|
249
|
+
@ticket_granting_ticket[:expires] = Time.now
|
250
|
+
raise VSACTicketExpiredError.new
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def get_ticket_granting_ticket(username, password)
|
255
|
+
begin
|
256
|
+
ticket = RestClient.post("#{@config[:auth_url]}/Ticket", username: username, password: password)
|
257
|
+
return { ticket: String.new(ticket), expires: Time.now + 8.hours }
|
258
|
+
rescue RestClient::Unauthorized
|
259
|
+
raise VSACInvalidCredentialsError.new
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Checks to ensure the API config has all necessary fields
|
264
|
+
def check_config(config)
|
265
|
+
return config != nil &&
|
266
|
+
!config[:auth_url].nil? &&
|
267
|
+
!config[:content_url].nil? &&
|
268
|
+
!config[:utility_url].nil?
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
274
|
+
end
|