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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +6 -6
  4. data/bonnie-bundler.gemspec +1 -1
  5. data/lib/bonnie_bundler.rb +1 -0
  6. data/lib/measures/loading/cql_loader.rb +25 -12
  7. data/lib/measures/loading/exceptions.rb +0 -2
  8. data/lib/measures/loading/value_set_loader.rb +77 -97
  9. data/lib/util/vsac_api.rb +274 -0
  10. data/test/fixtures/PVC2_v5_4_Unused_Support_Libraries.zip +0 -0
  11. data/test/fixtures/vcr_cassettes/valid_vsac_response_pvc_unused_libraries.yml +4286 -0
  12. data/test/fixtures/vcr_cassettes/vs_loading_500_response.yml +633 -0
  13. data/test/fixtures/vcr_cassettes/vs_loading_empty_concept_list.yml +174 -0
  14. data/test/fixtures/vcr_cassettes/vs_loading_meausre_defined_no_backup_profile.yml +1237 -0
  15. data/test/fixtures/vcr_cassettes/vs_loading_release.yml +17035 -0
  16. data/test/fixtures/vcr_cassettes/vsac_auth_bad_credentials.yml +57 -0
  17. data/test/fixtures/vcr_cassettes/vsac_auth_bad_ticket.yml +57 -0
  18. data/test/fixtures/vcr_cassettes/vsac_auth_good_credentials.yml +57 -0
  19. data/test/fixtures/vcr_cassettes/vsac_auth_good_credentials_and_simple_call.yml +177 -0
  20. data/test/fixtures/vcr_cassettes/vsac_util_get_profiles.yml +74 -0
  21. data/test/fixtures/vcr_cassettes/vsac_util_get_program_details_CMS_Hybrid.yml +57 -0
  22. data/test/fixtures/vcr_cassettes/vsac_util_get_program_details_CMS_eCQM.yml +64 -0
  23. data/test/fixtures/vcr_cassettes/vsac_util_get_program_details_HL7_C-CDA.yml +58 -0
  24. data/test/fixtures/vcr_cassettes/vsac_util_get_program_details_invalid.yml +53 -0
  25. data/test/fixtures/vcr_cassettes/vsac_util_get_programs.yml +64 -0
  26. data/test/fixtures/vcr_cassettes/vsac_vs_include_draft_no_profile.yml +57 -0
  27. data/test/fixtures/vcr_cassettes/vsac_vs_include_draft_with_profile.yml +177 -0
  28. data/test/fixtures/vcr_cassettes/vsac_vs_no_options.yml +177 -0
  29. data/test/fixtures/vcr_cassettes/vsac_vs_not_found.yml +164 -0
  30. data/test/fixtures/vcr_cassettes/vsac_vs_release.yml +4802 -0
  31. data/test/fixtures/vcr_cassettes/vsac_vs_version.yml +370 -0
  32. data/test/test_helper.rb +9 -4
  33. data/test/unit/cql_loader_test.rb +42 -15
  34. data/test/unit/load_mat_export_test.rb +10 -13
  35. data/test/unit/measure_complexity_test.rb +1 -1
  36. data/test/unit/measure_diff_test.rb +4 -4
  37. data/test/unit/storing_mat_export_package_test.rb +1 -1
  38. data/test/unit/value_set_loading_test.rb +58 -16
  39. data/test/unit/vsac_api_auth_test.rb +129 -0
  40. data/test/unit/vsac_api_test.rb +77 -0
  41. data/test/unit/vsac_api_util_test.rb +139 -0
  42. metadata +29 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ee582288be3cfd05d88916295b4cddd2002350c0
4
- data.tar.gz: 052e03dc984ea038e5469c45f60bc44307befc3c
3
+ metadata.gz: 61b0e2dce988134311e1747adf13ea68859a5799
4
+ data.tar.gz: 7e8638a383ead0635f662fa55da51baedf4ae0cd
5
5
  SHA512:
6
- metadata.gz: b228407edd054902045f684c4ed9e1faa96c34a2d68f15f532a4035fa0dbffe33e0398e19e3f6493252aa0db8ffa5a5223408232ee62f916cf4459c2e57bd9ee
7
- data.tar.gz: c2885dbe838270ee02367978e472fbef37b570ffbdfe7b5d3da5d164ed3e364012b362f68e6465dadac53a4411d53bccbcff37fd756b61d7c7bbd685835b63c7
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 => 'cql4bonnie'
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'
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- bonnie_bundler (2.0.3)
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.2)
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.1.1)
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.3)
187
- loofah (~> 2.0)
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.15.4
267
+ 1.16.1
@@ -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.3'
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'
@@ -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, vsac_user, vsac_password, overwrite_valuesets=false, cache=false, includeDraft=false, ticket_granting_ticket=nil)
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, vsac_user, vsac_password, overwrite_valuesets, cache, includeDraft, ticket_granting_ticket, hqmf_model.hqmf_set_id)
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, vsac_user=nil, vsac_password=nil, overwrite_valuesets=false, cache=false, includeDraft=false, ticket_granting_ticket=nil)
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, vsac_user, vsac_password, overwrite_valuesets, cache, includeDraft, ticket_granting_ticket)
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, vsac_user=nil, vsac_password=nil, overwrite_valuesets=nil, cache=nil, includeDraft=nil, ticket_granting_ticket=nil, measure_id=nil)
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 (vsac_user && vsac_password) || ticket_granting_ticket
126
- begin
127
- value_set_models = Measures::ValueSetLoader.load_value_sets_from_vsac(elm_value_sets, vsac_user, vsac_password, user, overwrite_valuesets, includeDraft, ticket_granting_ticket, cache, measure_id)
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)
@@ -1,8 +1,6 @@
1
1
  module Measures
2
2
  class ValueSetException < Exception
3
3
  end
4
- class VSACException < Exception
5
- end
6
4
  class HQMFException < Exception
7
5
  end
8
6
  class MeasureLoadingException < Exception
@@ -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, username, password, user=nil, overwrite=false, includeDraft=false, ticket_granting_ticket=nil, use_cache=false, measure_id=nil)
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
- begin
29
- backup_vs = []
30
- if overwrite
31
- backup_vs = get_existing_vs(user, value_set_oids).to_a
32
- delete_existing_vs(user, value_set_oids)
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
- errors = {}
37
- api = HealthDataStandards::Util::VSApiV2.new(nlm_config["ticket_url"],nlm_config["api_url"],username, password, ticket_granting_ticket)
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 use_cache
40
- codeset_base_dir = Measures::Loader::VALUE_SET_PATH
41
- FileUtils.mkdir_p(codeset_base_dir)
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
- RestClient.proxy = ENV["http_proxy"]
45
- value_sets.each do |value_set|
46
- value_set_version = value_set[:version] ? value_set[:version] : "N/A"
47
- #When querying vsac via profile, the version is always set to N/A
48
- #As such, we can set the version to the profile.
49
- #However, a value_set can have a version and profile that are identical, as such the versions that are profiles are denoted as such.
50
- value_set_profile = (value_set[:profile] && !includeDraft) ? value_set[:profile] : nlm_config["profile"]
51
- value_set_profile = "Profile:#{value_set_profile}"
52
-
53
- query_version = ""
54
- if includeDraft
55
- query_version = "Draft-#{measure_id}"
56
- elsif value_set[:profile]
57
- query_version = value_set_profile
58
- else
59
- query_version = value_set_version
60
- end
61
- # only access the database if we don't intend on using cached values
62
- set = HealthDataStandards::SVS::ValueSet.where({user_id: user.id, oid: value_set[:oid], version: query_version}).first() unless use_cache
63
- if (includeDraft && set)
64
- set.delete
65
- set = nil
66
- end
67
- if (set)
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
- vs_data = nil
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