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.
Files changed (121) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +8 -4
  3. data/README.md +57 -5
  4. data/Rakefile +1 -0
  5. data/lib/{hqmf-parser.rb → cqm-parsers.rb} +13 -45
  6. data/lib/ext/data_element.rb +1 -1
  7. data/lib/hqmf-parser/2.0/document.rb +1 -1
  8. data/lib/hqmf-parser/2.0/population_criteria.rb +1 -1
  9. data/lib/hqmf-parser/cql/document_helpers/doc_population_helper.rb +11 -6
  10. data/lib/measure-loader/cql_loader.rb +165 -0
  11. data/lib/measure-loader/elm_dependency_finder.rb +72 -0
  12. data/lib/measure-loader/elm_parser.rb +67 -0
  13. data/lib/measure-loader/exceptions.rb +10 -0
  14. data/lib/measure-loader/helpers.rb +11 -0
  15. data/lib/measure-loader/hqmf_measure_loader.rb +170 -0
  16. data/lib/measure-loader/mat_measure_files.rb +138 -0
  17. data/lib/measure-loader/source_data_criteria_loader.rb +75 -0
  18. data/lib/measure-loader/value_set_helpers.rb +68 -0
  19. data/lib/measure-loader/vsac_value_set_loader.rb +97 -0
  20. data/lib/tasks/hqmf.rake +1 -1
  21. data/lib/util/util.rb +23 -0
  22. data/lib/util/vsac_api.rb +166 -103
  23. metadata +60 -127
  24. data/lib/ext/code.rb +0 -10
  25. data/lib/qrda-export/catI-r5/_code.mustache +0 -1
  26. data/lib/qrda-export/catI-r5/_codes.mustache +0 -10
  27. data/lib/qrda-export/catI-r5/_header.mustache +0 -28
  28. data/lib/qrda-export/catI-r5/_measure_section.mustache +0 -59
  29. data/lib/qrda-export/catI-r5/_reporting_period.mustache +0 -23
  30. data/lib/qrda-export/catI-r5/_values.mustache +0 -10
  31. data/lib/qrda-export/catI-r5/qrda1_r5.mustache +0 -137
  32. data/lib/qrda-export/catI-r5/qrda1_r5.rb +0 -125
  33. data/lib/qrda-export/catI-r5/qrda_header/_author.mustache +0 -24
  34. data/lib/qrda-export/catI-r5/qrda_header/_custodian.mustache +0 -43
  35. data/lib/qrda-export/catI-r5/qrda_header/_documentation_of_service_event.mustache +0 -82
  36. data/lib/qrda-export/catI-r5/qrda_header/_information_recipient.mustache +0 -7
  37. data/lib/qrda-export/catI-r5/qrda_header/_legal_authenticator.mustache +0 -25
  38. data/lib/qrda-export/catI-r5/qrda_header/_participant.mustache +0 -7
  39. data/lib/qrda-export/catI-r5/qrda_header/_record_target.mustache +0 -28
  40. data/lib/qrda-export/catI-r5/qrda_templates/adverse_event.mustache +0 -28
  41. data/lib/qrda-export/catI-r5/qrda_templates/allergy_intolerance.mustache +0 -28
  42. data/lib/qrda-export/catI-r5/qrda_templates/assessment_performed.mustache +0 -25
  43. data/lib/qrda-export/catI-r5/qrda_templates/communication_from_patient_to_provider.mustache +0 -29
  44. data/lib/qrda-export/catI-r5/qrda_templates/communication_from_provider_to_patient.mustache +0 -24
  45. data/lib/qrda-export/catI-r5/qrda_templates/communication_from_provider_to_provider.mustache +0 -31
  46. data/lib/qrda-export/catI-r5/qrda_templates/device_applied.mustache +0 -32
  47. data/lib/qrda-export/catI-r5/qrda_templates/device_ordered.mustache +0 -31
  48. data/lib/qrda-export/catI-r5/qrda_templates/diagnosis.mustache +0 -38
  49. data/lib/qrda-export/catI-r5/qrda_templates/diagnostic_study_ordered.mustache +0 -19
  50. data/lib/qrda-export/catI-r5/qrda_templates/diagnostic_study_performed.mustache +0 -32
  51. data/lib/qrda-export/catI-r5/qrda_templates/encounter_ordered.mustache +0 -24
  52. data/lib/qrda-export/catI-r5/qrda_templates/encounter_performed.mustache +0 -40
  53. data/lib/qrda-export/catI-r5/qrda_templates/immunization_administered.mustache +0 -29
  54. data/lib/qrda-export/catI-r5/qrda_templates/insurance_provider.mustache +0 -11
  55. data/lib/qrda-export/catI-r5/qrda_templates/intervention_ordered.mustache +0 -18
  56. data/lib/qrda-export/catI-r5/qrda_templates/intervention_performed.mustache +0 -25
  57. data/lib/qrda-export/catI-r5/qrda_templates/lab_test_ordered.mustache +0 -18
  58. data/lib/qrda-export/catI-r5/qrda_templates/lab_test_performed.mustache +0 -22
  59. data/lib/qrda-export/catI-r5/qrda_templates/medication_active.mustache +0 -35
  60. data/lib/qrda-export/catI-r5/qrda_templates/medication_administered.mustache +0 -31
  61. data/lib/qrda-export/catI-r5/qrda_templates/medication_discharge.mustache +0 -55
  62. data/lib/qrda-export/catI-r5/qrda_templates/medication_dispensed.mustache +0 -39
  63. data/lib/qrda-export/catI-r5/qrda_templates/medication_ordered.mustache +0 -38
  64. data/lib/qrda-export/catI-r5/qrda_templates/patient_characteristic_expired.mustache +0 -16
  65. data/lib/qrda-export/catI-r5/qrda_templates/physical_exam_performed.mustache +0 -25
  66. data/lib/qrda-export/catI-r5/qrda_templates/procedure_ordered.mustache +0 -19
  67. data/lib/qrda-export/catI-r5/qrda_templates/procedure_performed.mustache +0 -44
  68. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_admission_source.mustache +0 -6
  69. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_anatomical_location_site.mustache +0 -1
  70. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_author.mustache +0 -7
  71. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_author_participation.mustache +0 -7
  72. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_component.mustache +0 -11
  73. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_encounter_diagnosis.mustache +0 -19
  74. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_encounter_facility_location.mustache +0 -16
  75. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_mediation_frequency.mustache +0 -3
  76. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_medication_details.mustache +0 -11
  77. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_ordinality.mustache +0 -1
  78. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_principal_diagnosis.mustache +0 -8
  79. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_reason.mustache +0 -12
  80. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_related_to.mustache +0 -6
  81. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_results.mustache +0 -19
  82. data/lib/qrda-export/catI-r5/qrda_templates/template_partials/_severity.mustache +0 -8
  83. data/lib/qrda-export/helper/cat_1_view_helper.rb +0 -146
  84. data/lib/qrda-export/helper/code_system_helper.rb +0 -77
  85. data/lib/qrda-export/helper/date_helper.rb +0 -89
  86. data/lib/qrda-import/base-importers/demographics_importer.rb +0 -49
  87. data/lib/qrda-import/base-importers/medication_importer.rb +0 -23
  88. data/lib/qrda-import/base-importers/section_importer.rb +0 -196
  89. data/lib/qrda-import/cda_identifier.rb +0 -19
  90. data/lib/qrda-import/data-element-importers/adverse_event_importer.rb +0 -24
  91. data/lib/qrda-import/data-element-importers/allergy_intolerance_importer.rb +0 -22
  92. data/lib/qrda-import/data-element-importers/assessment_performed_importer.rb +0 -24
  93. data/lib/qrda-import/data-element-importers/communication_from_patient_to_provider_importer.rb +0 -20
  94. data/lib/qrda-import/data-element-importers/communication_from_provider_to_patient_importer.rb +0 -20
  95. data/lib/qrda-import/data-element-importers/communication_from_provider_to_provider_importer.rb +0 -22
  96. data/lib/qrda-import/data-element-importers/device_applied_importer.rb +0 -24
  97. data/lib/qrda-import/data-element-importers/device_order_importer.rb +0 -19
  98. data/lib/qrda-import/data-element-importers/diagnosis_importer.rb +0 -24
  99. data/lib/qrda-import/data-element-importers/diagnostic_study_order_importer.rb +0 -21
  100. data/lib/qrda-import/data-element-importers/diagnostic_study_performed_importer.rb +0 -31
  101. data/lib/qrda-import/data-element-importers/encounter_order_importer.rb +0 -21
  102. data/lib/qrda-import/data-element-importers/encounter_performed_importer.rb +0 -42
  103. data/lib/qrda-import/data-element-importers/immunization_administered_importer.rb +0 -18
  104. data/lib/qrda-import/data-element-importers/intervention_order_importer.rb +0 -19
  105. data/lib/qrda-import/data-element-importers/intervention_performed_importer.rb +0 -23
  106. data/lib/qrda-import/data-element-importers/laboratory_test_order_importer.rb +0 -21
  107. data/lib/qrda-import/data-element-importers/laboratory_test_performed_importer.rb +0 -29
  108. data/lib/qrda-import/data-element-importers/medication_active_importer.rb +0 -17
  109. data/lib/qrda-import/data-element-importers/medication_administered_importer.rb +0 -17
  110. data/lib/qrda-import/data-element-importers/medication_discharge_importer.rb +0 -19
  111. data/lib/qrda-import/data-element-importers/medication_dispensed_importer.rb +0 -19
  112. data/lib/qrda-import/data-element-importers/medication_order_importer.rb +0 -16
  113. data/lib/qrda-import/data-element-importers/patient_characteristic_expired.rb +0 -22
  114. data/lib/qrda-import/data-element-importers/physical_exam_performed_importer.rb +0 -27
  115. data/lib/qrda-import/data-element-importers/procedure_order_importer.rb +0 -27
  116. data/lib/qrda-import/data-element-importers/procedure_performed_importer.rb +0 -35
  117. data/lib/qrda-import/data-element-importers/substance_administered_importer.rb +0 -17
  118. data/lib/qrda-import/entry_finder.rb +0 -20
  119. data/lib/qrda-import/entry_package.rb +0 -16
  120. data/lib/qrda-import/narrative_reference_handler.rb +0 -33
  121. 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
@@ -1,7 +1,7 @@
1
1
  require 'pathname'
2
2
  require 'fileutils'
3
3
  require 'json'
4
- require 'hqmf-parser'
4
+ require 'cqm-parsers'
5
5
 
6
6
  namespace :hqmf do
7
7
 
@@ -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
@@ -1,4 +1,4 @@
1
- require 'rest_client'
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
- symbolized_config = options[:config].symbolize_keys
84
- unless check_config symbolized_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
- # check if it has expired
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 = RestClient.get("#{@config[:utility_url]}/profiles")
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").each do |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 = RestClient.get("#{@config[:utility_url]}/programs")
136
- program_names = []
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
- program = @config.fetch(:program, DEFAULT_PROGRAM)
159
- end
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
- begin
189
- # parse json response and return it
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
- # As of 5/17/18 VSAC does not return 404 when an invalid profile is provided. It just doesnt fill the name
193
- # attribute in the 200 response. We need to check this.
194
- raise VSACProgramNotFoundError.new(program) if parsed_response['name'].nil?
195
- return parsed_response['name']
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
- releases = []
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
- # base parameter oid is always needed
228
- params = { id: oid }
196
+ needed_vs = {value_set: {oid: oid}, vs_vsac_options: options}
197
+ return get_multiple_valuesets([needed_vs])[0]
198
+ end
229
199
 
230
- # release parameter, should be used moving forward
231
- unless options[:release].nil?
232
- params[:release] = options[:release]
233
- end
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
- # profile parameter, may be needed for getting draft value sets
236
- if !options[:profile].nil?
237
- params[:profile] = options[:profile]
238
- unless options[:include_draft].nil?
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
- # version parameter, rarely used
248
- unless options[:version].nil?
249
- params[:version] = options[:version]
250
- end
216
+ return vs_datas
217
+ end
218
+
219
+ private
251
220
 
252
- # get a new service ticket
253
- params[:ticket] = get_ticket
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
- # run request
226
+ vs_data = vs_response.body.force_encoding("utf-8")
256
227
  begin
257
- return RestClient.get("#{@config[:content_url]}/RetrieveMultipleValueSets", params: params)
258
- rescue RestClient::ResourceNotFound
259
- raise VSNotFoundError.new(oid)
260
- rescue RestClient::InternalServerError
261
- raise VSACError.new("Server error response from VSAC for (#{oid}).")
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
- private
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
- def get_ticket
268
- # if there is no ticket granting ticket then we should raise an error
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
- # attempt to get a ticket
274
- begin
275
- ticket = RestClient.post("#{@config[:auth_url]}/Ticket/#{@ticket_granting_ticket[:ticket]}", service: TICKET_SERVICE_PARAM)
276
- return ticket.to_s
277
- rescue RestClient::Unauthorized
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
- def get_ticket_granting_ticket(username, password)
284
- ticket = RestClient.post("#{@config[:auth_url]}/Ticket", username: username, password: password)
285
- return { ticket: String.new(ticket), expires: Time.now + 8.hours }
286
- rescue RestClient::Unauthorized
287
- raise VSACInvalidCredentialsError.new
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